on master help sign in
Add :character-info command (#4000)
commitf7bd7b5eafac9f016a470cc77c780922f83e690b authorWilliam Etheredge <me@wetheredge.com> dateFri 3 Feb 2023, 08:24 (4 years ago) parentd8f482e
3 files changed+192
book/src/generated/typable-cmd.md+1 helix-term/src/commands/typed.rs+133 helix-term/tests/test/commands.rs+58
book/src/generated/typable-cmd.md+1
@@ -43,6 +43,7 @@
4343| `:change-current-directory`, `:cd` | Change the current working directory. |
4444| `:show-directory`, `:pwd` | Show the current working directory. |
4545| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
46+| `:character-info`, `:char` | Get info about the character under the primary cursor. |
4647| `:reload` | Discard changes and reload from the source file. |
4748| `:reload-all` | Discard changes and reload all documents from the source files. |
4849| `:update` | Write changes only if the file has been modified. |
helix-term/src/commands/typed.rs+133
@@ -4,6 +4,7 @@ use crate::job::Job;
44
55use super::*;
66
7+use helix_core::encoding;
78use helix_view::editor::{Action, CloseError, ConfigEvent};
89use ui::completers::{self, Completer};
910
@@ -1033,6 +1034,131 @@ fn set_encoding(
10331034 }
10341035}
10351036
1037+/// Shows info about the character under the primary cursor.
1038+fn get_character_info(
1039+ cx: &mut compositor::Context,
1040+ _args: &[Cow<str>],
1041+ event: PromptEvent,
1042+) -> anyhow::Result<()> {
1043+ if event != PromptEvent::Validate {
1044+ return Ok(());
1045+ }
1046+
1047+ let (view, doc) = current_ref!(cx.editor);
1048+ let text = doc.text().slice(..);
1049+
1050+ let grapheme_start = doc.selection(view.id).primary().cursor(text);
1051+ let grapheme_end = graphemes::next_grapheme_boundary(text, grapheme_start);
1052+
1053+ if grapheme_start == grapheme_end {
1054+ return Ok(());
1055+ }
1056+
1057+ let grapheme = text.slice(grapheme_start..grapheme_end).to_string();
1058+ let encoding = doc.encoding();
1059+
1060+ let printable = grapheme.chars().fold(String::new(), |mut s, c| {
1061+ match c {
1062+ '\0' => s.push_str("\\0"),
1063+ '\t' => s.push_str("\\t"),
1064+ '\n' => s.push_str("\\n"),
1065+ '\r' => s.push_str("\\r"),
1066+ _ => s.push(c),
1067+ }
1068+
1069+ s
1070+ });
1071+
1072+ // Convert to Unicode codepoints if in UTF-8
1073+ let unicode = if encoding == encoding::UTF_8 {
1074+ let mut unicode = " (".to_owned();
1075+
1076+ for (i, char) in grapheme.chars().enumerate() {
1077+ if i != 0 {
1078+ unicode.push(' ');
1079+ }
1080+
1081+ unicode.push_str("U+");
1082+
1083+ let codepoint: u32 = if char.is_ascii() {
1084+ char.into()
1085+ } else {
1086+ // Not ascii means it will be multi-byte, so strip out the extra
1087+ // bits that encode the length & mark continuation bytes
1088+
1089+ let s = String::from(char);
1090+ let bytes = s.as_bytes();
1091+
1092+ // First byte starts with 2-4 ones then a zero, so strip those off
1093+ let first = bytes[0];
1094+ let codepoint = first & (0xFF >> (first.leading_ones() + 1));
1095+ let mut codepoint = u32::from(codepoint);
1096+
1097+ // Following bytes start with 10
1098+ for byte in bytes.iter().skip(1) {
1099+ codepoint <<= 6;
1100+ codepoint += u32::from(*byte) & 0x3F;
1101+ }
1102+
1103+ codepoint
1104+ };
1105+
1106+ unicode.push_str(&format!("{codepoint:0>4x}"));
1107+ }
1108+
1109+ unicode.push(')');
1110+ unicode
1111+ } else {
1112+ String::new()
1113+ };
1114+
1115+ // Give the decimal value for ascii characters
1116+ let dec = if encoding.is_ascii_compatible() && grapheme.len() == 1 {
1117+ format!(" Dec {}", grapheme.as_bytes()[0])
1118+ } else {
1119+ String::new()
1120+ };
1121+
1122+ let hex = {
1123+ let mut encoder = encoding.new_encoder();
1124+ let max_encoded_len = encoder
1125+ .max_buffer_length_from_utf8_without_replacement(grapheme.len())
1126+ .unwrap();
1127+ let mut bytes = Vec::with_capacity(max_encoded_len);
1128+ let mut current_byte = 0;
1129+ let mut hex = String::new();
1130+
1131+ for (i, char) in grapheme.chars().enumerate() {
1132+ if i != 0 {
1133+ hex.push_str(" +");
1134+ }
1135+
1136+ let (result, _input_bytes_read) = encoder.encode_from_utf8_to_vec_without_replacement(
1137+ &char.to_string(),
1138+ &mut bytes,
1139+ true,
1140+ );
1141+
1142+ if let encoding::EncoderResult::Unmappable(char) = result {
1143+ bail!("{char:?} cannot be mapped to {}", encoding.name());
1144+ }
1145+
1146+ for byte in &bytes[current_byte..] {
1147+ hex.push_str(&format!(" {byte:0>2x}"));
1148+ }
1149+
1150+ current_byte = bytes.len();
1151+ }
1152+
1153+ hex
1154+ };
1155+
1156+ cx.editor
1157+ .set_status(format!("\"{printable}\"{unicode}{dec} Hex{hex}"));
1158+
1159+ Ok(())
1160+}
1161+
10361162/// Reload the [`Document`] from its source file.
10371163fn reload(
10381164 cx: &mut compositor::Context,
@@ -2131,6 +2257,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
21312257 fun: set_encoding,
21322258 completer: None,
21332259 },
2260+ TypableCommand {
2261+ name: "character-info",
2262+ aliases: &["char"],
2263+ doc: "Get info about the character under the primary cursor.",
2264+ fun: get_character_info,
2265+ completer: None,
2266+ },
21342267 TypableCommand {
21352268 name: "reload",
21362269 aliases: &[],
helix-term/tests/test/commands.rs+58
@@ -354,3 +354,61 @@ async fn test_extend_line() -> anyhow::Result<()> {
354354
355355 Ok(())
356356}
357+
358+#[tokio::test(flavor = "multi_thread")]
359+async fn test_character_info() -> anyhow::Result<()> {
360+ // UTF-8, single byte
361+ test_key_sequence(
362+ &mut helpers::AppBuilder::new().build()?,
363+ Some("ih<esc>h:char<ret>"),
364+ Some(&|app| {
365+ assert_eq!(
366+ r#""h" (U+0068) Dec 104 Hex 68"#,
367+ app.editor.get_status().unwrap().0
368+ );
369+ }),
370+ false,
371+ )
372+ .await?;
373+
374+ // UTF-8, multi-byte
375+ test_key_sequence(
376+ &mut helpers::AppBuilder::new().build()?,
377+ Some("ië<esc>h:char<ret>"),
378+ Some(&|app| {
379+ assert_eq!(
380+ r#""ë" (U+0065 U+0308) Hex 65 + cc 88"#,
381+ app.editor.get_status().unwrap().0
382+ );
383+ }),
384+ false,
385+ )
386+ .await?;
387+
388+ // Multiple characters displayed as one, escaped characters
389+ test_key_sequence(
390+ &mut helpers::AppBuilder::new().build()?,
391+ Some(":line<minus>ending crlf<ret>:char<ret>"),
392+ Some(&|app| {
393+ assert_eq!(
394+ r#""\r\n" (U+000d U+000a) Hex 0d + 0a"#,
395+ app.editor.get_status().unwrap().0
396+ );
397+ }),
398+ false,
399+ )
400+ .await?;
401+
402+ // Non-UTF-8
403+ test_key_sequence(
404+ &mut helpers::AppBuilder::new().build()?,
405+ Some(":encoding ascii<ret>ih<esc>h:char<ret>"),
406+ Some(&|app| {
407+ assert_eq!(r#""h" Dec 104 Hex 68"#, app.editor.get_status().unwrap().0);
408+ }),
409+ false,
410+ )
411+ .await?;
412+
413+ Ok(())
414+}