Browse Source

Merge pull request #92476 from AThousandShips/string_remove_char

Add `String::remove_char(s)` methods for performance and convenience
Thaddeus Crews 5 months ago
parent
commit
8d1c1c5867

+ 1 - 1
core/config/project_settings.cpp

@@ -1141,7 +1141,7 @@ Error ProjectSettings::save_custom(const String &p_path, const CustomMap &p_cust
 			save_features += ",";
 		}
 
-		String f = p_custom_features[i].strip_edges().replace("\"", "");
+		String f = p_custom_features[i].strip_edges().remove_char('\"');
 		save_features += f;
 	}
 

+ 1 - 2
core/extension/extension_api_dump.cpp

@@ -96,8 +96,7 @@ static String fix_doc_description(const String &p_bbcode) {
 	// Based on what EditorHelp does.
 
 	return p_bbcode.dedent()
-			.replace("\t", "")
-			.replace("\r", "")
+			.remove_chars("\t\r")
 			.strip_edges();
 }
 

+ 2 - 2
core/input/input.cpp

@@ -1594,8 +1594,8 @@ void Input::parse_mapping(const String &p_mapping) {
 			continue;
 		}
 
-		String output = entry[idx].get_slicec(':', 0).replace(" ", "");
-		String input = entry[idx].get_slicec(':', 1).replace(" ", "");
+		String output = entry[idx].get_slicec(':', 0).remove_char(' ');
+		String input = entry[idx].get_slicec(':', 1).remove_char(' ');
 		if (output.length() < 1 || input.length() < 2) {
 			continue;
 		}

+ 1 - 1
core/io/dir_access.cpp

@@ -336,7 +336,7 @@ Ref<DirAccess> DirAccess::create_temp(const String &p_prefix, bool p_keep, Error
 	uint32_t suffix_i = 0;
 	String path;
 	while (true) {
-		String datetime = Time::get_singleton()->get_datetime_string_from_system().replace("-", "").replace("T", "").replace(":", "");
+		String datetime = Time::get_singleton()->get_datetime_string_from_system().remove_chars("-T:");
 		datetime += itos(Time::get_singleton()->get_ticks_usec());
 		String suffix = datetime + (suffix_i > 0 ? itos(suffix_i) : "");
 		path = (p_prefix.is_empty() ? "" : p_prefix + "-") + suffix;

+ 1 - 1
core/io/file_access.cpp

@@ -104,7 +104,7 @@ Ref<FileAccess> FileAccess::create_temp(int p_mode_flags, const String &p_prefix
 	uint32_t suffix_i = 0;
 	String path;
 	while (true) {
-		String datetime = Time::get_singleton()->get_datetime_string_from_system().replace("-", "").replace("T", "").replace(":", "");
+		String datetime = Time::get_singleton()->get_datetime_string_from_system().remove_chars("-T:");
 		datetime += itos(Time::get_singleton()->get_ticks_usec());
 		String suffix = datetime + (suffix_i > 0 ? itos(suffix_i) : "");
 		path = TEMP_DIR.path_join((p_prefix.is_empty() ? "" : p_prefix + "-") + suffix + (extension.is_empty() ? "" : "." + extension));

+ 2 - 6
core/math/color.cpp

@@ -415,18 +415,14 @@ Color Color::named(const String &p_name, const Color &p_default) {
 int Color::find_named_color(const String &p_name) {
 	String name = p_name;
 	// Normalize name.
-	name = name.replace(" ", "");
-	name = name.replace("-", "");
-	name = name.replace("_", "");
-	name = name.replace("'", "");
-	name = name.replace(".", "");
+	name = name.remove_chars(" -_'.");
 	name = name.to_upper();
 
 	static HashMap<String, int> named_colors_hashmap;
 	if (unlikely(named_colors_hashmap.is_empty())) {
 		const int named_color_count = get_named_color_count();
 		for (int i = 0; i < named_color_count; i++) {
-			named_colors_hashmap[String(named_colors[i].name).replace("_", "")] = i;
+			named_colors_hashmap[String(named_colors[i].name).remove_char('_')] = i;
 		}
 	}
 

+ 1 - 1
core/os/os.cpp

@@ -214,7 +214,7 @@ String OS::get_locale() const {
 // Non-virtual helper to extract the 2 or 3-letter language code from
 // `get_locale()` in a way that's consistent for all platforms.
 String OS::get_locale_language() const {
-	return get_locale().left(3).replace("_", "");
+	return get_locale().left(3).remove_char('_');
 }
 
 // Embedded PCK offset.

+ 125 - 1
core/string/ustring.cpp

@@ -1007,7 +1007,7 @@ String String::to_camel_case() const {
 }
 
 String String::to_pascal_case() const {
-	return capitalize().replace(" ", "");
+	return capitalize().remove_char(' ');
 }
 
 String String::to_snake_case() const {
@@ -3102,6 +3102,130 @@ String String::erase(int p_pos, int p_chars) const {
 	return left(p_pos) + substr(p_pos + p_chars);
 }
 
+template <class T>
+static bool _contains_char(char32_t p_c, const T *p_chars, int p_chars_len) {
+	for (int i = 0; i < p_chars_len; ++i) {
+		if (p_c == (char32_t)p_chars[i]) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+String String::remove_char(char32_t p_char) const {
+	if (p_char == 0) {
+		return *this;
+	}
+
+	int len = length();
+	if (len == 0) {
+		return *this;
+	}
+
+	int index = 0;
+	const char32_t *old_ptr = ptr();
+	for (; index < len; ++index) {
+		if (old_ptr[index] == p_char) {
+			break;
+		}
+	}
+
+	// If no occurrence of `char` was found, return this.
+	if (index == len) {
+		return *this;
+	}
+
+	// If we found at least one occurrence of `char`, create new string, allocating enough space for the current length minus one.
+	String new_string;
+	new_string.resize(len);
+	char32_t *new_ptr = new_string.ptrw();
+
+	// Copy part of input before `char`.
+	memcpy(new_ptr, old_ptr, index * sizeof(char32_t));
+
+	int new_size = index;
+
+	// Copy rest, skipping `char`.
+	for (++index; index < len; ++index) {
+		const char32_t old_char = old_ptr[index];
+		if (old_char != p_char) {
+			new_ptr[new_size] = old_char;
+			++new_size;
+		}
+	}
+
+	new_ptr[new_size] = _null;
+
+	// Shrink new string to fit.
+	new_string.resize(new_size + 1);
+
+	return new_string;
+}
+
+template <class T>
+static String _remove_chars_common(const String &p_this, const T *p_chars, int p_chars_len) {
+	// Delegate if p_chars has a single element.
+	if (p_chars_len == 1) {
+		return p_this.remove_char(*p_chars);
+	} else if (p_chars_len == 0) {
+		return p_this;
+	}
+
+	int len = p_this.length();
+
+	if (len == 0) {
+		return p_this;
+	}
+
+	int index = 0;
+	const char32_t *old_ptr = p_this.ptr();
+	for (; index < len; ++index) {
+		if (_contains_char(old_ptr[index], p_chars, p_chars_len)) {
+			break;
+		}
+	}
+
+	// If no occurrence of `chars` was found, return this.
+	if (index == len) {
+		return p_this;
+	}
+
+	// If we found at least one occurrence of `chars`, create new string, allocating enough space for the current length minus one.
+	String new_string;
+	new_string.resize(len);
+	char32_t *new_ptr = new_string.ptrw();
+
+	// Copy part of input before `char`.
+	memcpy(new_ptr, old_ptr, index * sizeof(char32_t));
+
+	int new_size = index;
+
+	// Copy rest, skipping `chars`.
+	for (++index; index < len; ++index) {
+		const char32_t old_char = old_ptr[index];
+		if (!_contains_char(old_char, p_chars, p_chars_len)) {
+			new_ptr[new_size] = old_char;
+			++new_size;
+		}
+	}
+
+	new_ptr[new_size] = 0;
+
+	// Shrink new string to fit.
+	new_string.resize(new_size + 1);
+
+	return new_string;
+}
+
+String String::remove_chars(const String &p_chars) const {
+	return _remove_chars_common(*this, p_chars.ptr(), p_chars.length());
+}
+
+String String::remove_chars(const char *p_chars) const {
+	return _remove_chars_common(*this, p_chars, strlen(p_chars));
+}
+
 String String::substr(int p_from, int p_chars) const {
 	if (p_chars == -1) {
 		p_chars = length() - p_from;

+ 3 - 0
core/string/ustring.h

@@ -417,6 +417,9 @@ public:
 	String reverse() const;
 	String insert(int p_at_pos, const String &p_string) const;
 	String erase(int p_pos, int p_chars = 1) const;
+	String remove_char(char32_t p_what) const;
+	String remove_chars(const String &p_chars) const;
+	String remove_chars(const char *p_chars) const;
 	String pad_decimals(int p_digits) const;
 	String pad_zeros(int p_digits) const;
 	String trim_prefix(const String &p_prefix) const;

+ 2 - 0
core/variant/variant_call.cpp

@@ -1731,6 +1731,8 @@ static void _register_variant_builtin_methods_string() {
 	bind_string_method(format, sarray("values", "placeholder"), varray("{_}"));
 	bind_string_methodv(replace, static_cast<String (String::*)(const String &, const String &) const>(&String::replace), sarray("what", "forwhat"), varray());
 	bind_string_methodv(replacen, static_cast<String (String::*)(const String &, const String &) const>(&String::replacen), sarray("what", "forwhat"), varray());
+	bind_string_method(remove_char, sarray("what"), varray());
+	bind_string_methodv(remove_chars, static_cast<String (String::*)(const String &) const>(&String::remove_chars), sarray("chars"), varray());
 	bind_string_method(repeat, sarray("count"), varray());
 	bind_string_method(reverse, sarray(), varray());
 	bind_string_method(insert, sarray("position", "what"), varray());

+ 14 - 0
doc/classes/String.xml

@@ -763,6 +763,20 @@
 				[b]Example:[/b] [code]"this/is".path_join("path") == "this/is/path"[/code].
 			</description>
 		</method>
+		<method name="remove_char" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="what" type="int" />
+			<description>
+				Removes all occurrences of the Unicode character with code [param what]. Faster version of [method replace] when the key is only one character long and the replacement is [code]""[/code].
+			</description>
+		</method>
+		<method name="remove_chars" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="chars" type="String" />
+			<description>
+				Removes any occurrence of the characters in [param chars]. See also [method remove_char].
+			</description>
+		</method>
 		<method name="repeat" qualifiers="const">
 			<return type="String" />
 			<param index="0" name="count" type="int" />

+ 14 - 0
doc/classes/StringName.xml

@@ -671,6 +671,20 @@
 				[b]Example:[/b] [code]"this/is".path_join("path") == "this/is/path"[/code].
 			</description>
 		</method>
+		<method name="remove_char" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="what" type="int" />
+			<description>
+				Removes all occurrences of the Unicode character with code [param what]. Faster version of [method replace] when the key is only one character long and the replacement is [code]""[/code].
+			</description>
+		</method>
+		<method name="remove_chars" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="chars" type="String" />
+			<description>
+				Removes any occurrence of the characters in [param chars]. See also [method remove_char].
+			</description>
+		</method>
 		<method name="repeat" qualifiers="const">
 			<return type="String" />
 			<param index="0" name="count" type="int" />

+ 1 - 1
drivers/gles3/shader_gles3.cpp

@@ -73,7 +73,7 @@ void ShaderGLES3::_add_stage(const char *p_code, StageType p_stage_type) {
 		} else if (l.begins_with("#CODE")) {
 			chunk.type = StageTemplate::Chunk::TYPE_CODE;
 			push_chunk = true;
-			chunk.code = l.replace_first("#CODE", String()).replace(":", "").strip_edges().to_upper();
+			chunk.code = l.replace_first("#CODE", String()).remove_char(':').strip_edges().to_upper();
 		} else {
 			text += l + "\n";
 		}

+ 1 - 1
editor/doc_tools.cpp

@@ -1632,7 +1632,7 @@ Error DocTools::save_classes(const String &p_default_path, const HashMap<String,
 		}
 
 		Error err;
-		String save_file = save_path.path_join(c.name.replace("\"", "").replace("/", "--") + ".xml");
+		String save_file = save_path.path_join(c.name.remove_char('\"').replace("/", "--") + ".xml");
 		Ref<FileAccess> f = FileAccess::open(save_file, FileAccess::WRITE, &err);
 
 		ERR_CONTINUE_MSG(err != OK, "Can't write doc file: " + save_file + ".");

+ 2 - 2
editor/editor_help.cpp

@@ -1906,7 +1906,7 @@ void EditorHelp::_update_doc() {
 				_push_code_font();
 
 				if (constant.value.begins_with("Color(") && constant.value.ends_with(")")) {
-					String stripped = constant.value.replace(" ", "").replace("Color(", "").replace(")", "");
+					String stripped = constant.value.remove_char(' ').replace("Color(", "").remove_char(')');
 					PackedFloat64Array color = stripped.split_floats(",");
 					if (color.size() >= 3) {
 						class_desc->push_color(Color(color[0], color[1], color[2]));
@@ -2439,7 +2439,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const C
 	const Color kbd_bg_color = p_owner_node->get_theme_color(SNAME("kbd_bg_color"), SNAME("EditorHelp"));
 	const Color param_bg_color = p_owner_node->get_theme_color(SNAME("param_bg_color"), SNAME("EditorHelp"));
 
-	String bbcode = p_bbcode.dedent().replace("\t", "").replace("\r", "").strip_edges();
+	String bbcode = p_bbcode.dedent().remove_chars("\t\r").strip_edges();
 
 	// Select the correct code examples.
 	switch ((int)EDITOR_GET("text_editor/help/class_reference_examples")) {

+ 5 - 5
editor/editor_node.cpp

@@ -3291,7 +3291,7 @@ void EditorNode::_request_screenshot() {
 }
 
 void EditorNode::_screenshot(bool p_use_utc) {
-	String name = "editor_screenshot_" + Time::get_singleton()->get_datetime_string_from_system(p_use_utc).replace(":", "") + ".png";
+	String name = "editor_screenshot_" + Time::get_singleton()->get_datetime_string_from_system(p_use_utc).remove_char(':') + ".png";
 	NodePath path = String("user://") + name;
 	_save_screenshot(path);
 	if (EDITOR_GET("interface/editor/automatically_open_screenshots")) {
@@ -5070,8 +5070,8 @@ String EditorNode::_get_system_info() const {
 
 	String display_session_type;
 #ifdef LINUXBSD_ENABLED
-	// `replace` is necessary, because `capitalize` introduces a whitespace between "x" and "11".
-	display_session_type = OS::get_singleton()->get_environment("XDG_SESSION_TYPE").capitalize().replace(" ", "");
+	// `remove_char` is necessary, because `capitalize` introduces a whitespace between "x" and "11".
+	display_session_type = OS::get_singleton()->get_environment("XDG_SESSION_TYPE").capitalize().remove_char(' ');
 #endif // LINUXBSD_ENABLED
 	String driver_name = OS::get_singleton()->get_current_rendering_driver_name().to_lower();
 	String rendering_method = OS::get_singleton()->get_current_rendering_method().to_lower();
@@ -5143,8 +5143,8 @@ String EditorNode::_get_system_info() const {
 
 	String display_driver_window_mode;
 #ifdef LINUXBSD_ENABLED
-	// `replace` is necessary, because `capitalize` introduces a whitespace between "x" and "11".
-	display_driver_window_mode = DisplayServer::get_singleton()->get_name().capitalize().replace(" ", "") + " display driver";
+	// `remove_char` is necessary, because `capitalize` introduces a whitespace between "x" and "11".
+	display_driver_window_mode = DisplayServer::get_singleton()->get_name().capitalize().remove_char(' ') + " display driver";
 #endif // LINUXBSD_ENABLED
 	if (!display_driver_window_mode.is_empty()) {
 		display_driver_window_mode += ", ";

+ 2 - 2
editor/editor_properties_array_dict.cpp

@@ -401,7 +401,7 @@ void EditorPropertyArray::update_property() {
 	}
 
 	if (preview_value) {
-		String ctr_str = array.get_construct_string().trim_prefix(array_type_name + "(").trim_suffix(")").replace("\n", "");
+		String ctr_str = array.get_construct_string().trim_prefix(array_type_name + "(").trim_suffix(")").remove_char('\n');
 		if (array_type == Variant::ARRAY && subtype != Variant::NIL) {
 			int type_end = ctr_str.find("](");
 			if (type_end > 0) {
@@ -1174,7 +1174,7 @@ void EditorPropertyDictionary::update_property() {
 	object->set_dict(updated_val);
 
 	if (preview_value) {
-		String ctr_str = updated_val.get_construct_string().replace("\n", "");
+		String ctr_str = updated_val.get_construct_string().remove_char('\n');
 		if (key_subtype != Variant::NIL || value_subtype != Variant::NIL) {
 			int type_end = ctr_str.find("](");
 			if (type_end > 0) {

+ 2 - 2
editor/plugins/script_text_editor.cpp

@@ -2249,7 +2249,7 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
 			switch (expression_pattern) {
 				case RGBA_PARAMETER: {
 					color_args = line.substr(begin, end - begin);
-					String stripped = color_args.replace(" ", "").replace("\t", "").replace("(", "").replace(")", "");
+					String stripped = color_args.remove_chars(" \t()");
 					PackedFloat64Array color = stripped.split_floats(",");
 					if (color.size() > 2) {
 						float alpha = color.size() > 3 ? color[3] : 1.0f;
@@ -2261,7 +2261,7 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
 						end = line.length();
 					}
 					color_args = line.substr(begin, end - begin);
-					const String color_name = color_args.replace(" ", "").replace("\t", "").replace(".", "");
+					const String color_name = color_args.remove_chars(" \t.");
 					const int color_index = Color::find_named_color(color_name);
 					if (0 <= color_index) {
 						const Color color_constant = Color::get_named_color(color_index);

+ 5 - 5
editor/project_converter_3_to_4.cpp

@@ -1990,9 +1990,9 @@ void ProjectConverter3To4::process_gdscript_line(String &line, const RegExContai
 			Vector<String> parts = parse_arguments(line.substr(start, end));
 			if (parts.size() == 2) {
 				if (builtin) {
-					line = line.substr(0, start) + "await " + parts[0] + "." + parts[1].replace("\\\"", "").replace("\\'", "").replace(" ", "") + line.substr(end + start);
+					line = line.substr(0, start) + "await " + parts[0] + "." + parts[1].replace("\\\"", "").replace("\\'", "").remove_char(' ') + line.substr(end + start);
 				} else {
-					line = line.substr(0, start) + "await " + parts[0] + "." + parts[1].replace("\"", "").replace("\'", "").replace(" ", "") + line.substr(end + start);
+					line = line.substr(0, start) + "await " + parts[0] + "." + parts[1].remove_chars("\"' ") + line.substr(end + start);
 				}
 			}
 		}
@@ -2920,7 +2920,7 @@ String ProjectConverter3To4::line_formatter(int current_line, String from, Strin
 
 	from = from.strip_escapes();
 	to = to.strip_escapes();
-	line = line.replace("\r", "").replace("\n", "").strip_edges();
+	line = line.remove_chars("\r\n").strip_edges();
 
 	return vformat("Line(%d), %s -> %s  -  LINE \"\"\" %s \"\"\"", current_line, from, to, line);
 }
@@ -2935,8 +2935,8 @@ String ProjectConverter3To4::simple_line_formatter(int current_line, String old_
 		new_line = new_line.substr(0, 997) + "...";
 	}
 
-	old_line = old_line.replace("\r", "").replace("\n", "").strip_edges();
-	new_line = new_line.replace("\r", "").replace("\n", "").strip_edges();
+	old_line = old_line.remove_chars("\r\n").strip_edges();
+	new_line = new_line.remove_chars("\r\n").strip_edges();
 
 	return vformat("Line (%d) - FULL LINES - \"\"\" %s \"\"\"  =====>  \"\"\" %s \"\"\"", current_line, old_line, new_line);
 }

+ 1 - 1
editor/property_selector.cpp

@@ -226,7 +226,7 @@ void PropertySelector::_update_search() {
 
 				Ref<Texture2D> icon;
 				script_methods = false;
-				String rep = mi.name.replace("*", "");
+				String rep = mi.name.remove_char('*');
 				if (mi.name == "*Script Methods") {
 					icon = search_options->get_editor_theme_icon(SNAME("Script"));
 					script_methods = true;

+ 1 - 1
modules/gdscript/gdscript_tokenizer.cpp

@@ -863,7 +863,7 @@ GDScriptTokenizer::Token GDScriptTokenizerText::number() {
 
 	// Create a string with the whole number.
 	int len = _current - _start;
-	String number = String(_start, len).replace("_", "");
+	String number = String(_start, len).remove_char('_');
 
 	// Convert to the appropriate literal type.
 	if (base == 16) {

+ 1 - 1
modules/gdscript/language_server/gdscript_workspace.cpp

@@ -695,7 +695,7 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu
 
 			} else {
 				ScriptLanguage::LookupResult ret;
-				if (symbol_identifier == "new" && parser->get_lines()[p_doc_pos.position.line].replace(" ", "").replace("\t", "").contains("new(")) {
+				if (symbol_identifier == "new" && parser->get_lines()[p_doc_pos.position.line].remove_chars(" \t").contains("new(")) {
 					symbol_identifier = "_init";
 				}
 				if (OK == GDScriptLanguage::get_singleton()->lookup_code(parser->get_text_for_lookup_symbol(pos, symbol_identifier, p_func_required), symbol_identifier, path, nullptr, ret)) {

+ 1 - 2
modules/mono/editor/bindings_generator.cpp

@@ -172,8 +172,7 @@ static String fix_doc_description(const String &p_bbcode) {
 	// This seems to be the correct way to do this. It's the same EditorHelp does.
 
 	return p_bbcode.dedent()
-			.replace("\t", "")
-			.replace("\r", "")
+			.remove_chars("\t\r")
 			.strip_edges();
 }
 

+ 2 - 2
modules/text_server_adv/text_server_adv.h

@@ -404,7 +404,7 @@ class TextServerAdvanced : public TextServerExtension {
 	_FORCE_INLINE_ Variant::Type _get_tag_type(int64_t p_tag) const;
 	_FORCE_INLINE_ bool _get_tag_hidden(int64_t p_tag) const;
 	_FORCE_INLINE_ int _font_get_weight_by_name(const String &p_sty_name) const {
-		String sty_name = p_sty_name.replace(" ", "").replace("-", "");
+		String sty_name = p_sty_name.remove_chars(" -");
 		if (sty_name.contains("thin") || sty_name.contains("hairline")) {
 			return 100;
 		} else if (sty_name.contains("extralight") || sty_name.contains("ultralight")) {
@@ -431,7 +431,7 @@ class TextServerAdvanced : public TextServerExtension {
 		return 400;
 	}
 	_FORCE_INLINE_ int _font_get_stretch_by_name(const String &p_sty_name) const {
-		String sty_name = p_sty_name.replace(" ", "").replace("-", "");
+		String sty_name = p_sty_name.remove_chars(" -");
 		if (sty_name.contains("ultracondensed")) {
 			return 50;
 		} else if (sty_name.contains("extracondensed")) {

+ 2 - 2
modules/text_server_fb/text_server_fb.h

@@ -341,7 +341,7 @@ class TextServerFallback : public TextServerExtension {
 	}
 
 	_FORCE_INLINE_ int _font_get_weight_by_name(const String &p_sty_name) const {
-		String sty_name = p_sty_name.replace(" ", "").replace("-", "");
+		String sty_name = p_sty_name.remove_chars(" -");
 		if (sty_name.contains("thin") || sty_name.contains("hairline")) {
 			return 100;
 		} else if (sty_name.contains("extralight") || sty_name.contains("ultralight")) {
@@ -368,7 +368,7 @@ class TextServerFallback : public TextServerExtension {
 		return 400;
 	}
 	_FORCE_INLINE_ int _font_get_stretch_by_name(const String &p_sty_name) const {
-		String sty_name = p_sty_name.replace(" ", "").replace("-", "");
+		String sty_name = p_sty_name.remove_chars(" -");
 		if (sty_name.contains("ultracondensed")) {
 			return 50;
 		} else if (sty_name.contains("extracondensed")) {

+ 1 - 2
platform/android/export/export_plugin.cpp

@@ -396,8 +396,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
 							// its format is "[property]: [value]" so changed it as like build.prop
 							String p = props[j];
 							p = p.replace("]: ", "=");
-							p = p.replace("[", "");
-							p = p.replace("]", "");
+							p = p.remove_chars("[]");
 
 							if (p.begins_with("ro.product.model=")) {
 								device = p.get_slicec('=', 1).strip_edges();

+ 1 - 1
platform/linuxbsd/freedesktop_portal_desktop.cpp

@@ -505,7 +505,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
 
 	String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(monitor_connection));
 	String token = String::hex_encode_buffer(uuid, 64);
-	String path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace(".", "_").replace(":", ""), token);
+	String path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace(".", "_").remove_char(':'), token);
 
 	fd.path = path;
 	fd.filter = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", path, dbus_unique_name);

+ 1 - 1
platform/linuxbsd/os_linuxbsd.cpp

@@ -173,7 +173,7 @@ String OS_LinuxBSD::get_unique_id() const {
 		memset(buf, 0, sizeof(buf));
 		size_t len = sizeof(buf) - 1;
 		if (sysctl(mib, 2, buf, &len, 0x0, 0) != -1) {
-			machine_id = String::utf8(buf).replace("-", "");
+			machine_id = String::utf8(buf).remove_char('-');
 		}
 #else
 		Ref<FileAccess> f = FileAccess::open("/etc/machine-id", FileAccess::READ);

+ 1 - 1
platform/windows/os_windows.cpp

@@ -132,7 +132,7 @@ static String format_error_message(DWORD id) {
 
 	LocalFree(messageBuffer);
 
-	return msg.replace("\r", "").replace("\n", "");
+	return msg.remove_chars("\r\n");
 }
 
 void RedirectStream(const char *p_file_name, const char *p_mode, FILE *p_cpp_stream, const DWORD p_std_handle) {

+ 1 - 1
scene/3d/xr/xr_face_modifier_3d.cpp

@@ -341,7 +341,7 @@ static int find_face_blend_shape(const StringName &p_name) {
 	};
 
 	// Convert the name to lower-case and strip non-alphanumeric characters.
-	const String name = String(p_name).to_lower().replace("_", "");
+	const String name = String(p_name).to_lower().remove_char('_');
 
 	// Iterate through the blend map.
 	for (const blend_map_entry &entry : blend_map) {

+ 1 - 1
scene/gui/code_edit.cpp

@@ -137,7 +137,7 @@ void CodeEdit::_notification(int p_what) {
 
 						Point2 round_ofs = hint_ofs + theme_cache.code_hint_style->get_offset() + Vector2(0, theme_cache.font->get_ascent(theme_cache.font_size) + font_height * i + yofs);
 						round_ofs = round_ofs.round();
-						draw_string(theme_cache.font, round_ofs, line.replace(String::chr(0xFFFF), ""), HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size, theme_cache.code_hint_color);
+						draw_string(theme_cache.font, round_ofs, line.remove_char(0xFFFF), HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size, theme_cache.code_hint_color);
 						if (end > 0) {
 							// Draw an underline for the currently edited function parameter.
 							const Vector2 b = hint_ofs + theme_cache.code_hint_style->get_offset() + Vector2(begin, font_height + font_height * i + yofs);

+ 2 - 2
scene/gui/color_picker.cpp

@@ -721,7 +721,7 @@ void ColorPicker::_update_presets() {
 			}
 
 			if (palette_edited) {
-				palette_name->set_text(vformat("%s*", palette_name->get_text().replace("*", "")));
+				palette_name->set_text(vformat("%s*", palette_name->get_text().remove_char('*')));
 				palette_name->set_tooltip_text(ETR("The changes to this palette have not been saved to a file."));
 			}
 		}
@@ -1086,7 +1086,7 @@ void ColorPicker::erase_preset(const Color &p_color) {
 			}
 		}
 
-		palette_name->set_text(vformat("%s*", palette_name->get_text().replace("*", "")));
+		palette_name->set_text(vformat("%s*", palette_name->get_text().remove_char('*')));
 		palette_name->set_tooltip_text(ETR("The changes to this palette have not been saved to a file."));
 		if (presets.is_empty()) {
 			palette_name->set_text("");

+ 2 - 2
scene/gui/text_edit.cpp

@@ -7337,7 +7337,7 @@ void TextEdit::_paste_internal(int p_caret) {
 	}
 
 	// Paste a full line. Ignore '\r' characters that may have been added to the clipboard by the OS.
-	if (get_caret_count() == 1 && !has_selection(0) && !cut_copy_line.is_empty() && cut_copy_line == clipboard.replace("\r", "")) {
+	if (get_caret_count() == 1 && !has_selection(0) && !cut_copy_line.is_empty() && cut_copy_line == clipboard.remove_char('\r')) {
 		insert_text(clipboard, get_caret_line(), 0);
 		return;
 	}
@@ -8584,7 +8584,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i
 	ERR_FAIL_COND(p_char < 0);
 
 	/* STEP 1: Remove \r from source text and separate in substrings. */
-	const String text_to_insert = p_text.replace("\r", "");
+	const String text_to_insert = p_text.remove_char('\r');
 	Vector<String> substrings = text_to_insert.split("\n");
 
 	// Is this just a new empty line?

+ 1 - 1
servers/rendering/renderer_rd/shader_rd.cpp

@@ -76,7 +76,7 @@ void ShaderRD::_add_stage(const char *p_code, StageType p_stage_type) {
 		} else if (l.begins_with("#CODE")) {
 			chunk.type = StageTemplate::Chunk::TYPE_CODE;
 			push_chunk = true;
-			chunk.code = l.replace_first("#CODE", String()).replace(":", "").strip_edges().to_upper();
+			chunk.code = l.replace_first("#CODE", String()).remove_char(':').strip_edges().to_upper();
 		} else if (l.begins_with("#include ")) {
 			String include_file = l.replace("#include ", "").strip_edges();
 			if (include_file[0] == '"') {

+ 17 - 0
tests/core/string/test_string.h

@@ -466,6 +466,23 @@ TEST_CASE("[String] Erasing") {
 	CHECK(s == "Josephine is such a girl!");
 }
 
+TEST_CASE("[String] remove_char") {
+	String s = "Banana";
+	CHECK(s.remove_char('a') == "Bnn");
+	CHECK(s.remove_char('\0') == "Banana");
+	CHECK(s.remove_char('x') == "Banana");
+}
+
+TEST_CASE("[String] remove_chars") {
+	String s = "Banana";
+	CHECK(s.remove_chars("Ba") == "nn");
+	CHECK(s.remove_chars(String("Ba")) == "nn");
+	CHECK(s.remove_chars("") == "Banana");
+	CHECK(s.remove_chars(String()) == "Banana");
+	CHECK(s.remove_chars("xy") == "Banana");
+	CHECK(s.remove_chars(String("xy")) == "Banana");
+}
+
 TEST_CASE("[String] Number to string") {
 	CHECK(String::num(0) == "0.0"); // The method takes double, so always add zeros.
 	CHECK(String::num(0.0) == "0.0");