Skip to content

feat: document abbreviations #3023

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

Closed
wants to merge 4 commits into from
Closed
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
4 changes: 4 additions & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@
| `:pipe` | Pipe each selection to the shell command. |
| `:pipe-to` | Pipe each selection to the shell command, ignoring output. |
| `:run-shell-command`, `:sh` | Run a shell command |
| `:insert-abbreviation`, `:abbr` | Insert a new abbreviation |
| `:delete-abbreviation`, `:rabbr` | Delete an abbreviation |
| `:load-abbreviation-from-file`, `:labbr` | Load abbreviations from a file that contains abbreviations such as:<br><br>nvm nevermind<br>brb be right back<br>... |
| `:write-abbreviation-to-file`, `:wabbr` | Write abbreviations to a file using the following format:<br><br>nvm nevermind<br>brb be right back<br>... |
91 changes: 91 additions & 0 deletions helix-core/src/abbreviations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::collections::HashMap;

use ropey::Rope;

use crate::{movement, Change, Range, Selection, Tendril, Transaction};
use serde::{Deserialize, Serialize};

/// The type that represents the collection of abbreviations,
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Abbreviations(HashMap<String, String>);

impl Abbreviations {
pub fn default() -> Self {
Self(HashMap::new())
}

/// Look up the word under the main cursor and trigger abbreviation for all selections if there is a match.
pub fn expand_or_insert(
&self,
doc: &Rope,
selection: &Selection,
c: char,
) -> Option<Transaction> {
// Default function to insert the original char when we should not expand an abbreviation
fn insert(c: char, cursor: usize) -> Change {
let mut t = Tendril::new();
t.push(c);
(cursor, cursor, Some(t))
}

let transaction = Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(doc.slice(..));

// Do not look for previous word at start of file
if cursor == 0 {
return insert(c, cursor);
}

// Do not look for previous word if previous char is non-alphanumeric (works for line returns too)
match doc.get_char(cursor - 1) {
Some(previous_char) => {
if !previous_char.is_alphanumeric() {
return insert(c, cursor);
}
}
None => return insert(c, cursor),
};

// Move 1 char left to be right on the previous word
let mut current_word_range = Range {
anchor: cursor - 1,
head: cursor - 1,
horiz: None,
};
current_word_range =
movement::move_prev_word_start(doc.slice(..), current_word_range, 1);

// Get current word and check if we know it as an abbreviation
let current_word = doc.slice(current_word_range.head..current_word_range.anchor);
let whole_word = self.0.get(&current_word.to_string());

// Expand abbreviation if needed, insert the original char otherwise
match whole_word {
Some(w) => {
let mut t = Tendril::new();
t.push_str(w);
t.push(c);
(current_word_range.cursor(doc.slice(..)), cursor, Some(t))
}
None => insert(c, cursor),
}
});
Some(transaction)
}

pub fn insert(&mut self, abbr: &str, whole_word: &str) {
self.0.insert(abbr.to_string(), whole_word.to_string());
}

pub fn map(&self) -> &HashMap<String, String> {
&self.0
}

pub fn map_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.0
}

pub fn remove(&mut self, key: &str) {
self.0.remove(key);
}
}
1 change: 1 addition & 0 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub use encoding_rs as encoding;

pub mod abbreviations;
pub mod auto_pairs;
pub mod chars;
pub mod comment;
Expand Down
11 changes: 9 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3078,12 +3078,19 @@ pub mod insert {
let text = doc.text();
let selection = doc.selection(view.id);
let auto_pairs = doc.auto_pairs(cx.editor);
let abbreviations = &doc.abbreviations;

// Autopairs and abbreviations
let transaction = auto_pairs
.as_ref()
.and_then(|ap| auto_pairs::hook(text, selection, c, ap))
.or_else(|| insert(text, selection, c));

.or_else(|| {
if !c.is_alphanumeric() {
abbreviations.expand_or_insert(text, selection, c)
} else {
insert(text, selection, c)
}
});
let (view, doc) = current!(cx.editor);
if let Some(t) = transaction {
apply_transaction(&t, doc, view);
Expand Down
126 changes: 126 additions & 0 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::io::Write;
use std::ops::Deref;

use crate::job::Job;
Expand Down Expand Up @@ -1808,6 +1809,103 @@ fn run_shell_command(
Ok(())
}

fn insert_abbreviation(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

if args.len() < 2 {
anyhow::bail!(
"Bad arguments. Usage: `:insert-abbreviation abbreviation abbreviated_expression`"
);
}
let abbreviated_expression = args[1..].join(" ");
let doc = doc_mut!(cx.editor);
doc.abbreviations.insert(&args[0], &abbreviated_expression);

Ok(())
}

fn delete_abbreviation(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

if args.len() != 1 {
anyhow::bail!("Bad arguments. Usage: `:delete-abbreviation abbreviation`");
}

let doc = doc_mut!(cx.editor);
doc.abbreviations.remove(&args[0]);
Ok(())
}

fn load_abbreviations_from_file(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

if args.len() != 1 {
anyhow::bail!("Bad arguments. Usage: `:load-abbreviations-from-file filename`");
}

let doc = doc_mut!(cx.editor);
let abbrs = doc.abbreviations.map_mut();
let file_path = PathBuf::from(&args[0].to_string());
if let Ok(abbr_file_content) = std::fs::read_to_string(file_path) {
// Each line should insert an abbr
for line in abbr_file_content.lines() {
if let Some(split) = line.split_once(' ') {
abbrs.insert(split.0.to_string(), split.1.to_string());
}
}
} else {
anyhow::bail!("Unable to read file {}", &args[0]);
}

Ok(())
}

fn write_abbreviations_to_file(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

if args.len() != 1 {
anyhow::bail!("Bad arguments. Usage: `:write-abbreviations-to-file filename`");
}

let doc = doc_mut!(cx.editor);
let abbrs = doc.abbreviations.map_mut();
let file_path = PathBuf::from(&args[0].to_string());
if let Ok(mut abbr_file) = std::fs::File::create(file_path) {
for (abbreviation, abbreviated) in abbrs {
if let Err(e) = writeln!(abbr_file, "{} {}", abbreviation, abbreviated) {
anyhow::bail!("Unable to write {}: {}", &args[0], e)
};
}
} else {
anyhow::bail!("Unable to write to file {}", &args[0]);
}

Ok(())
}
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
Expand Down Expand Up @@ -2323,6 +2421,34 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: run_shell_command,
completer: Some(completers::directory),
},
TypableCommand {
name: "insert-abbreviation",
aliases: &["abbr"],
doc: "Insert a new abbreviation",
fun: insert_abbreviation,
completer: Some(completers::abbreviations),
},
TypableCommand {
name: "delete-abbreviation",
aliases: &["rabbr"],
doc: "Delete an abbreviation",
fun: delete_abbreviation,
completer: Some(completers::abbreviations),
},
TypableCommand {
name: "load-abbreviation-from-file",
aliases: &["labbr"],
doc: "Load abbreviations from a file that contains abbreviations such as:\n\nnvm nevermind\nbrb be right back\n...",
fun: load_abbreviations_from_file,
completer: Some(completers::filename),
},
TypableCommand {
name: "write-abbreviation-to-file",
aliases: &["wabbr"],
doc: "Write abbreviations to a file using the following format:\n\nnvm nevermind\nbrb be right back\n...",
fun: write_abbreviations_to_file,
completer: Some(completers::filename),
},
];

pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
Expand Down
26 changes: 26 additions & 0 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,32 @@ pub mod completers {
.collect()
}

pub fn abbreviations(editor: &Editor, input: &str) -> Vec<Completion> {
let (_, doc) = current_ref!(editor);
let matcher = Matcher::default();

let mut matches: Vec<_> = doc
.abbreviations
.map()
.clone()
.into_iter()
.filter_map(|abbr| {
matcher
.fuzzy_match(&abbr.0, input)
.map(move |score| (abbr.0, score))
})
.collect();

matches.sort_unstable_by(|(abbr1, score1), (abbr2, score2)| {
(Reverse(*score1), abbr1).cmp(&(Reverse(*score2), abbr2))
});

matches
.into_iter()
.map(|(abbr, _score)| ((0..), abbr.into()))
.collect()
}

pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
filename_impl(editor, input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
Expand Down
2 changes: 1 addition & 1 deletion helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ impl<T: Item> FilePicker<T> {
}
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, None)
Document::open(path, None, None, None)
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
Expand Down
22 changes: 17 additions & 5 deletions helix-view/src/document.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{anyhow, bail, Context, Error};
use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::abbreviations::Abbreviations;
use helix_core::auto_pairs::AutoPairs;
use helix_core::Range;
use helix_vcs::{DiffHandle, DiffProviderRegistry};
Expand Down Expand Up @@ -138,6 +139,8 @@ pub struct Document {
language_server: Option<Arc<helix_lsp::Client>>,

diff_handle: Option<DiffHandle>,

pub abbreviations: Abbreviations,
}

use std::{fmt, mem};
Expand Down Expand Up @@ -351,7 +354,11 @@ use helix_lsp::lsp;
use url::Url;

impl Document {
pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self {
pub fn from(
text: Rope,
encoding: Option<&'static encoding::Encoding>,
abbreviations: Option<Abbreviations>,
) -> Self {
let encoding = encoding.unwrap_or(encoding::UTF_8);
let changes = ChangeSet::new(&text);
let old_state = None;
Expand All @@ -377,6 +384,10 @@ impl Document {
modified_since_accessed: false,
language_server: None,
diff_handle: None,
abbreviations: match abbreviations {
Some(a) => a,
None => Abbreviations::default(),
},
}
}

Expand All @@ -387,6 +398,7 @@ impl Document {
path: &Path,
encoding: Option<&'static encoding::Encoding>,
config_loader: Option<Arc<syntax::Loader>>,
abbreviations: Option<Abbreviations>,
) -> Result<Self, Error> {
// Open the file if it exists, otherwise assume it is a new file (and thus empty).
let (rope, encoding) = if path.exists() {
Expand All @@ -398,7 +410,7 @@ impl Document {
(Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
};

let mut doc = Self::from(rope, Some(encoding));
let mut doc = Self::from(rope, Some(encoding), abbreviations);

// set the path and try detecting the language
doc.set_path(Some(path))?;
Expand Down Expand Up @@ -1200,7 +1212,7 @@ impl Document {
impl Default for Document {
fn default() -> Self {
let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
Self::from(text, None)
Self::from(text, None, None)
}
}

Expand Down Expand Up @@ -1245,7 +1257,7 @@ mod test {
fn changeset_to_changes_ignore_line_endings() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello\r\nworld");
let mut doc = Document::from(text, None);
let mut doc = Document::from(text, None, None);
let view = ViewId::default();
doc.set_selection(view, Selection::single(0, 0));

Expand Down Expand Up @@ -1279,7 +1291,7 @@ mod test {
fn changeset_to_changes() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello");
let mut doc = Document::from(text, None);
let mut doc = Document::from(text, None, None);
let view = ViewId::default();
doc.set_selection(view, Selection::single(5, 5));

Expand Down
Loading