on master help sign in
nit: Do less allocations in `ui::menu::Item::label` implementations

This complicates the code a little but it often divides by two the number of allocations done by the functions. LSP labels especially can easily be called dozens of time in a single menu popup, when listing references for example.

commit34389e1d5472f458934b0af2a15192e2b8d83e1e authorAlexis (Poliorcetics) Bourget <alexis.bourget@gmail.com> dateSat 15 Oct 2022, 19:16 (4 years ago) parent3aea33a
2 files changed+48−43
helix-term/src/commands.rs+12−13 helix-term/src/commands/lsp.rs+36−30
helix-term/src/commands.rs+12−13
@@ -54,7 +54,7 @@ use crate::{
5454
5555use crate::job::{self, Jobs};
5656use futures_util::StreamExt;
57−use std::{collections::HashMap, fmt, future::Future};
57+use std::{collections::HashMap, fmt, fmt::Write, future::Future};
5858use std::{collections::HashSet, num::NonZeroUsize};
5959
6060use std::{
@@ -2416,19 +2416,18 @@ impl ui::menu::Item for MappableCommand {
24162416 type Data = ReverseKeymap;
24172417
24182418 fn label(&self, keymap: &Self::Data) -> Spans {
2419− // formats key bindings, multiple bindings are comma separated,
2420− // individual key presses are joined with `+`
24212419 let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
2422− bindings
2423− .iter()
2424− .map(|bind| {
2425− bind.iter()
2426− .map(|key| key.to_string())
2427− .collect::<Vec<String>>()
2428− .join("+")
2429− })
2430− .collect::<Vec<String>>()
2431− .join(", ")
2420+ bindings.iter().fold(String::new(), |mut acc, bind| {
2421+ if !acc.is_empty() {
2422+ acc.push_str(", ");
2423+ }
2424+ bind.iter().fold(false, |needs_plus, key| {
2425+ write!(&mut acc, "{}{}", if needs_plus { "+" } else { "" }, key)
2426+ .expect("Writing to a string can only fail on an Out-Of-Memory error");
2427+ true
2428+ });
2429+ acc
2430+ })
24322431 };
24332432
24342433 match self {
helix-term/src/commands/lsp.rs+36−30
@@ -18,7 +18,9 @@ use crate::{
1818 },
1919};
2020
21−use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, path::PathBuf, sync::Arc};
21+use std::{
22+ borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, path::PathBuf, sync::Arc,
23+};
2224
2325/// Gets the language server that is attached to a document, and
2426/// if it's not active displays a status message. Using this macro
@@ -43,23 +45,32 @@ impl ui::menu::Item for lsp::Location {
4345 type Data = PathBuf;
4446
4547 fn label(&self, cwdir: &Self::Data) -> Spans {
46− let file: Cow<'_, str> = (self.uri.scheme() == "file")
47− .then(|| {
48− self.uri
49− .to_file_path()
50− .map(|path| {
51− // strip root prefix
52− path.strip_prefix(&cwdir)
53− .map(|path| path.to_path_buf())
54− .unwrap_or(path)
55− })
56− .map(|path| Cow::from(path.to_string_lossy().into_owned()))
57− .ok()
58− })
59− .flatten()
60− .unwrap_or_else(|| self.uri.as_str().into());
61− let line = self.range.start.line;
62− format!("{}:{}", file, line).into()
48+ // The preallocation here will overallocate a few characters since it will account for the
49+ // URL's scheme, which is not used most of the time since that scheme will be "file://".
50+ // Those extra chars will be used to avoid allocating when writing the line number (in the
51+ // common case where it has 5 digits or less, which should be enough for a cast majority
52+ // of usages).
53+ let mut res = String::with_capacity(self.uri.as_str().len());
54+
55+ if self.uri.scheme() == "file" {
56+ // With the preallocation above and UTF-8 paths already, this closure will do one (1)
57+ // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
58+ let mut write_path_to_res = || -> Option<()> {
59+ let path = self.uri.to_file_path().ok()?;
60+ res.push_str(&path.strip_prefix(&cwdir).unwrap_or(&path).to_string_lossy());
61+ Some(())
62+ };
63+ write_path_to_res();
64+ } else {
65+ // Never allocates since we declared the string with this capacity already.
66+ res.push_str(self.uri.as_str());
67+ }
68+
69+ // Most commonly, this will not allocate, especially on Unix systems where the root prefix
70+ // is a simple `/` and not `C:\` (with whatever drive letter)
71+ write!(&mut res, ":{}", self.range.start.line)
72+ .expect("Will only failed if allocating fail");
73+ res.into()
6374 }
6475}
6576
@@ -73,10 +84,8 @@ impl ui::menu::Item for lsp::SymbolInformation {
7384 } else {
7485 match self.location.uri.to_file_path() {
7586 Ok(path) => {
76− let relative_path = helix_core::path::get_relative_path(path.as_path())
77− .to_string_lossy()
78− .into_owned();
79− format!("{} ({})", &self.name, relative_path).into()
87+ let get_relative_path = path::get_relative_path(path.as_path());
88+ format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
8089 }
8190 Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
8291 }
@@ -115,24 +124,21 @@ impl ui::menu::Item for PickerDiagnostic {
115124 // remove background as it is distracting in the picker list
116125 style.bg = None;
117126
118− let code = self
127+ let code: Cow<'_, str> = self
119128 .diag
120129 .code
121130 .as_ref()
122131 .map(|c| match c {
123− NumberOrString::Number(n) => n.to_string(),
124− NumberOrString::String(s) => s.to_string(),
132+ NumberOrString::Number(n) => n.to_string().into(),
133+ NumberOrString::String(s) => s.as_str().into(),
125134 })
126− .map(|code| format!(" ({})", code))
127135 .unwrap_or_default();
128136
129137 let path = match format {
130138 DiagnosticsFormat::HideSourcePath => String::new(),
131139 DiagnosticsFormat::ShowSourcePath => {
132− let path = path::get_truncated_path(self.url.path())
133− .to_string_lossy()
134− .into_owned();
135− format!("{}: ", path)
140+ let path = path::get_truncated_path(self.url.path());
141+ format!("{}: ", path.to_string_lossy())
136142 }
137143 };
138144