Enable `clippy::single_char_pattern` (#8727)

Marshall Bowers created

This PR enables the
[`clippy::single_char_pattern`](https://rust-lang.github.io/rust-clippy/master/index.html#/single_char_pattern)
rule and fixes the outstanding violations.

Release Notes:

- N/A

Change summary

crates/assistant/src/codegen.rs                   |  4 ++--
crates/channel/src/channel_store.rs               |  2 +-
crates/collab/src/db/tests.rs                     |  2 +-
crates/collab_ui/src/chat_panel/message_editor.rs |  2 +-
crates/editor/src/editor.rs                       |  8 ++++----
crates/editor/src/git/permalink.rs                | 16 ++++++++--------
crates/file_finder/src/file_finder.rs             |  2 +-
crates/gpui/src/app/test_context.rs               |  2 +-
crates/language/src/buffer_tests.rs               |  6 +++---
crates/languages/src/ocaml.rs                     |  4 ++--
crates/languages/src/terraform.rs                 |  2 +-
crates/lsp/src/lsp.rs                             |  2 +-
crates/project_panel/src/project_panel.rs         |  2 +-
crates/semantic_index/src/semantic_index_tests.rs |  4 ++--
crates/vim/src/command.rs                         |  8 ++++----
crates/vim/src/normal/paste.rs                    |  6 +++---
crates/vim/src/test/neovim_backed_test_context.rs |  6 +++---
crates/vim/src/test/neovim_connection.rs          |  2 +-
crates/workspace/src/pane.rs                      |  4 ++--
crates/workspace/src/workspace.rs                 |  2 +-
crates/zed/src/open_listener.rs                   |  4 ++--
tooling/xtask/src/main.rs                         |  1 -
22 files changed, 45 insertions(+), 46 deletions(-)

Detailed changes

crates/assistant/src/codegen.rs ๐Ÿ”—

@@ -297,7 +297,7 @@ fn strip_invalid_spans_from_codeblock(
         } else if buffer.starts_with("<|")
             || buffer.starts_with("<|S")
             || buffer.starts_with("<|S|")
-            || buffer.ends_with("|")
+            || buffer.ends_with('|')
             || buffer.ends_with("|E")
             || buffer.ends_with("|E|")
         {
@@ -335,7 +335,7 @@ fn strip_invalid_spans_from_codeblock(
                 .strip_suffix("|E|>")
                 .or_else(|| text.strip_suffix("E|>"))
                 .or_else(|| text.strip_prefix("|>"))
-                .or_else(|| text.strip_prefix(">"))
+                .or_else(|| text.strip_prefix('>'))
                 .unwrap_or(&text)
                 .to_string();
         };

crates/channel/src/channel_store.rs ๐Ÿ”—

@@ -592,7 +592,7 @@ impl ChannelStore {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ChannelId>> {
         let client = self.client.clone();
-        let name = name.trim_start_matches("#").to_owned();
+        let name = name.trim_start_matches('#').to_owned();
         cx.spawn(move |this, mut cx| async move {
             let response = client
                 .request(proto::CreateChannel {

crates/collab/src/db/tests.rs ๐Ÿ”—

@@ -168,7 +168,7 @@ async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
         email,
         false,
         NewUserParams {
-            github_login: email[0..email.find("@").unwrap()].to_string(),
+            github_login: email[0..email.find('@').unwrap()].to_string(),
             github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst),
         },
     )

crates/collab_ui/src/chat_panel/message_editor.rs ๐Ÿ”—

@@ -310,7 +310,7 @@ impl MessageEditor {
                 for range in ranges {
                     text.clear();
                     text.extend(buffer.text_for_range(range.clone()));
-                    if let Some(username) = text.strip_prefix("@") {
+                    if let Some(username) = text.strip_prefix('@') {
                         if let Some(user_id) = this.channel_members.get(username) {
                             let start = multi_buffer.anchor_after(range.start);
                             let end = multi_buffer.anchor_after(range.end);

crates/editor/src/editor.rs ๐Ÿ”—

@@ -4845,7 +4845,7 @@ impl Editor {
                 .text_for_range(start_point..end_point)
                 .collect::<String>();
 
-            let mut lines = text.split("\n").collect_vec();
+            let mut lines = text.split('\n').collect_vec();
 
             let lines_before = lines.len();
             callback(&mut lines);
@@ -4913,7 +4913,7 @@ impl Editor {
         self.manipulate_text(cx, |text| {
             // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
             // https://github.com/rutrum/convert-case/issues/16
-            text.split("\n")
+            text.split('\n')
                 .map(|line| line.to_case(Case::Title))
                 .join("\n")
         })
@@ -4935,7 +4935,7 @@ impl Editor {
         self.manipulate_text(cx, |text| {
             // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
             // https://github.com/rutrum/convert-case/issues/16
-            text.split("\n")
+            text.split('\n')
                 .map(|line| line.to_case(Case::UpperCamel))
                 .join("\n")
         })
@@ -9387,7 +9387,7 @@ impl Editor {
             let highlight = chunk
                 .syntax_highlight_id
                 .and_then(|id| id.name(&style.syntax));
-            let mut chunk_lines = chunk.text.split("\n").peekable();
+            let mut chunk_lines = chunk.text.split('\n').peekable();
             while let Some(text) = chunk_lines.next() {
                 let mut merged_with_last_token = false;
                 if let Some(last_token) = line.back_mut() {

crates/editor/src/git/permalink.rs ๐Ÿ”—

@@ -105,7 +105,7 @@ fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
             .trim_start_matches("https://github.com/")
             .trim_end_matches(".git");
 
-        let (owner, repo) = repo_with_owner.split_once("/")?;
+        let (owner, repo) = repo_with_owner.split_once('/')?;
 
         return Some(ParsedGitRemote {
             provider: GitHostingProvider::Github,
@@ -120,7 +120,7 @@ fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
             .trim_start_matches("https://gitlab.com/")
             .trim_end_matches(".git");
 
-        let (owner, repo) = repo_with_owner.split_once("/")?;
+        let (owner, repo) = repo_with_owner.split_once('/')?;
 
         return Some(ParsedGitRemote {
             provider: GitHostingProvider::Gitlab,
@@ -135,7 +135,7 @@ fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
             .trim_start_matches("https://gitee.com/")
             .trim_end_matches(".git");
 
-        let (owner, repo) = repo_with_owner.split_once("/")?;
+        let (owner, repo) = repo_with_owner.split_once('/')?;
 
         return Some(ParsedGitRemote {
             provider: GitHostingProvider::Gitee,
@@ -147,9 +147,9 @@ fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
     if url.contains("bitbucket.org") {
         let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?;
         let (owner, repo) = repo_with_owner
-            .trim_start_matches("/")
-            .trim_start_matches(":")
-            .split_once("/")?;
+            .trim_start_matches('/')
+            .trim_start_matches(':')
+            .split_once('/')?;
 
         return Some(ParsedGitRemote {
             provider: GitHostingProvider::Bitbucket,
@@ -166,7 +166,7 @@ fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
             .trim_start_matches("git@git.sr.ht:~")
             .trim_start_matches("https://git.sr.ht/~");
 
-        let (owner, repo) = repo_with_owner.split_once("/")?;
+        let (owner, repo) = repo_with_owner.split_once('/')?;
 
         return Some(ParsedGitRemote {
             provider: GitHostingProvider::Sourcehut,
@@ -181,7 +181,7 @@ fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
             .trim_start_matches("https://codeberg.org/")
             .trim_end_matches(".git");
 
-        let (owner, repo) = repo_with_owner.split_once("/")?;
+        let (owner, repo) = repo_with_owner.split_once('/')?;
 
         return Some(ParsedGitRemote {
             provider: GitHostingProvider::Codeberg,

crates/file_finder/src/file_finder.rs ๐Ÿ”—

@@ -701,7 +701,7 @@ impl PickerDelegate for FileFinderDelegate {
         raw_query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Task<()> {
-        let raw_query = raw_query.replace(" ", "");
+        let raw_query = raw_query.replace(' ', "");
         let raw_query = raw_query.trim();
         if raw_query.is_empty() {
             let project = self.project.read(cx);

crates/gpui/src/app/test_context.rs ๐Ÿ”—

@@ -331,7 +331,7 @@ impl TestAppContext {
     /// This will also run the background executor until it's parked.
     pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
         for keystroke in keystrokes
-            .split(" ")
+            .split(' ')
             .map(Keystroke::parse)
             .map(Result::unwrap)
         {

crates/language/src/buffer_tests.rs ๐Ÿ”—

@@ -1110,7 +1110,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
                 b();
                 |
             "
-            .replace("|", "") // marker to preserve trailing whitespace
+            .replace('|', "") // marker to preserve trailing whitespace
             .unindent(),
         )
         .with_language(Arc::new(rust_lang()), cx);
@@ -1787,7 +1787,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
 
         // In a JSX expression: use the default config.
         let expression_in_element_config = snapshot
-            .language_scope_at(text.find("{").unwrap() + 1)
+            .language_scope_at(text.find('{').unwrap() + 1)
             .unwrap();
         assert_eq!(
             expression_in_element_config
@@ -2321,7 +2321,7 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
         actual_ranges,
         expected_ranges,
         "wrong ranges for text lines:\n{:?}",
-        text.split("\n").collect::<Vec<_>>()
+        text.split('\n').collect::<Vec<_>>()
     );
 }
 

crates/languages/src/ocaml.rs ๐Ÿ”—

@@ -62,7 +62,7 @@ impl LspAdapter for OCamlLspAdapter {
         language: &Arc<language::Language>,
     ) -> Option<CodeLabel> {
         let name = &completion.label;
-        let detail = completion.detail.as_ref().map(|s| s.replace("\n", " "));
+        let detail = completion.detail.as_ref().map(|s| s.replace('\n', " "));
 
         match completion.kind.zip(detail) {
             // Error of 'b : ('a, 'b) result
@@ -124,7 +124,7 @@ impl LspAdapter for OCamlLspAdapter {
             // version : string
             // NOTE: (~|?) are omitted as we don't use them in the fuzzy filtering
             Some((CompletionItemKind::FIELD, detail))
-                if name.starts_with("~") || name.starts_with("?") =>
+                if name.starts_with('~') || name.starts_with('?') =>
             {
                 let label = name.trim_start_matches(&['~', '?']);
                 let text = format!("{} : {}", label, detail);

crates/languages/src/terraform.rs ๐Ÿ”—

@@ -129,7 +129,7 @@ impl LspAdapter for TerraformLspAdapter {
 }
 
 fn build_download_url(version: String) -> Result<String> {
-    let v = version.strip_prefix("v").unwrap_or(&version);
+    let v = version.strip_prefix('v').unwrap_or(&version);
     let os = match std::env::consts::OS {
         "linux" => "linux",
         "macos" => "darwin",

crates/lsp/src/lsp.rs ๐Ÿ”—

@@ -377,7 +377,7 @@ impl LanguageServer {
             let headers = std::str::from_utf8(&buffer)?;
 
             let message_len = headers
-                .split("\n")
+                .split('\n')
                 .find(|line| line.starts_with(CONTENT_LEN_HEADER))
                 .and_then(|line| line.strip_prefix(CONTENT_LEN_HEADER))
                 .ok_or_else(|| anyhow!("invalid LSP message header {headers:?}"))?

crates/project_panel/src/project_panel.rs ๐Ÿ”—

@@ -607,7 +607,7 @@ impl ProjectPanel {
                 worktree_id,
                 entry_id: NEW_ENTRY_ID,
             });
-            let new_path = entry.path.join(&filename.trim_start_matches("/"));
+            let new_path = entry.path.join(&filename.trim_start_matches('/'));
             if path_already_exists(new_path.as_path()) {
                 return None;
             }

crates/semantic_index/src/semantic_index_tests.rs ๐Ÿ”—

@@ -414,7 +414,7 @@ async fn test_code_context_retrieval_json() {
                     }
                 }"#
             .unindent(),
-            text.find("{").unwrap(),
+            text.find('{').unwrap(),
         )],
     );
 
@@ -443,7 +443,7 @@ async fn test_code_context_retrieval_json() {
                     "age": 42
                 }]"#
             .unindent(),
-            text.find("[").unwrap(),
+            text.find('[').unwrap(),
         )],
     );
 }

crates/vim/src/command.rs ๐Ÿ”—

@@ -41,7 +41,7 @@ pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option<CommandIn
     //
     // For now, you can only do a replace on the % range, and you can
     // only use a specific line number range to "go to line"
-    while query.starts_with(":") {
+    while query.starts_with(':') {
         query = &query[1..];
     }
 
@@ -321,16 +321,16 @@ pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option<CommandIn
         "0" => ("0", StartOfDocument.boxed_clone()),
 
         _ => {
-            if query.starts_with("/") || query.starts_with("?") {
+            if query.starts_with('/') || query.starts_with('?') {
                 (
                     query,
                     FindCommand {
                         query: query[1..].to_string(),
-                        backwards: query.starts_with("?"),
+                        backwards: query.starts_with('?'),
                     }
                     .boxed_clone(),
                 )
-            } else if query.starts_with("%") {
+            } else if query.starts_with('%') {
                 (
                     query,
                     ReplaceCommand {

crates/vim/src/normal/paste.rs ๐Ÿ”—

@@ -135,8 +135,8 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
                         } else {
                             (clipboard_text.to_string(), first_selection_indent_column)
                         };
-                    let line_mode = to_insert.ends_with("\n");
-                    let is_multiline = to_insert.contains("\n");
+                    let line_mode = to_insert.ends_with('\n');
+                    let is_multiline = to_insert.contains('\n');
 
                     if line_mode && !before {
                         if selection.is_empty() {
@@ -480,7 +480,7 @@ mod test {
                 the_
                 ห‡fox jumps over
                 _dog"}
-            .replace("_", " "), // Hack for trailing whitespace
+            .replace('_', " "), // Hack for trailing whitespace
         )
         .await;
         cx.assert_shared_clipboard("lazy").await;

crates/vim/src/test/neovim_backed_test_context.rs ๐Ÿ”—

@@ -72,7 +72,7 @@ impl NeovimBackedTestContext {
         let test_name = thread
             .name()
             .expect("thread is not named")
-            .split(":")
+            .split(':')
             .last()
             .unwrap()
             .to_string();
@@ -122,7 +122,7 @@ impl NeovimBackedTestContext {
     }
 
     pub async fn set_shared_state(&mut self, marked_text: &str) {
-        let mode = if marked_text.contains("ยป") {
+        let mode = if marked_text.contains('ยป') {
             Mode::Visual
         } else {
             Mode::Normal
@@ -188,7 +188,7 @@ impl NeovimBackedTestContext {
 
     pub async fn assert_shared_state(&mut self, marked_text: &str) {
         self.is_dirty = false;
-        let marked_text = marked_text.replace("โ€ข", " ");
+        let marked_text = marked_text.replace('โ€ข', " ");
         let neovim = self.neovim_state().await;
         let neovim_mode = self.neovim_mode().await;
         let editor = self.editor_state();

crates/vim/src/test/neovim_connection.rs ๐Ÿ”—

@@ -392,7 +392,7 @@ impl NeovimConnection {
                 // the content of the selection via the "a register to get the shape correctly.
                 self.nvim.input("\"aygv").await.unwrap();
                 let content = self.nvim.command_output("echo getreg('a')").await.unwrap();
-                let lines = content.split("\n").collect::<Vec<_>>();
+                let lines = content.split('\n').collect::<Vec<_>>();
                 let top = cmp::min(selection_row, cursor_row);
                 let left = cmp::min(selection_col, cursor_col);
                 for row in top..=cmp::max(selection_row, cursor_row) {

crates/workspace/src/pane.rs ๐Ÿ”—

@@ -2586,8 +2586,8 @@ mod tests {
 
             let mut index = 0;
             let items = labels.map(|mut label| {
-                if label.ends_with("*") {
-                    label = label.trim_end_matches("*");
+                if label.ends_with('*') {
+                    label = label.trim_end_matches('*');
                     active_item_index = index;
                 }
 

crates/workspace/src/workspace.rs ๐Ÿ”—

@@ -1260,7 +1260,7 @@ impl Workspace {
     fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
         let mut keystrokes: Vec<Keystroke> = action
             .0
-            .split(" ")
+            .split(' ')
             .flat_map(|k| Keystroke::parse(k).log_err())
             .collect();
         keystrokes.reverse();

crates/zed/src/open_listener.rs ๐Ÿ”—

@@ -95,10 +95,10 @@ impl OpenListener {
     }
 
     fn handle_zed_url_scheme(&self, request_path: &str) -> Option<OpenRequest> {
-        let mut parts = request_path.split("/");
+        let mut parts = request_path.split('/');
         if parts.next() == Some("channel") {
             if let Some(slug) = parts.next() {
-                if let Some(id_str) = slug.split("-").last() {
+                if let Some(id_str) = slug.split('-').last() {
                     if let Ok(channel_id) = id_str.parse::<u64>() {
                         let Some(next) = parts.next() else {
                             return Some(OpenRequest::JoinChannel { channel_id });

tooling/xtask/src/main.rs ๐Ÿ”—

@@ -124,7 +124,6 @@ fn run_clippy(args: ClippyArgs) -> Result<()> {
         "clippy::redundant_locals",
         "clippy::reversed_empty_ranges",
         "clippy::search_is_some",
-        "clippy::single_char_pattern",
         "clippy::single_range_in_vec_init",
         "clippy::suspicious_to_owned",
         "clippy::to_string_in_format_args",