Browse Source

Add remaning code edit unit tests

Paulb23 4 years ago
parent
commit
1c08719d09
3 changed files with 2292 additions and 2 deletions
  1. 1 1
      main/main.cpp
  2. 2255 0
      tests/test_code_edit.h
  3. 36 1
      tests/test_macros.h

+ 1 - 1
main/main.cpp

@@ -455,10 +455,10 @@ void Main::test_cleanup() {
 	ResourceLoader::remove_custom_loaders();
 	ResourceSaver::remove_custom_savers();
 
+	unregister_driver_types();
 #ifdef TOOLS_ENABLED
 	EditorNode::unregister_editor_types();
 #endif
-	unregister_driver_types();
 	unregister_module_types();
 	unregister_platform_apis();
 	unregister_scene_types();

+ 2255 - 0
tests/test_code_edit.h

@@ -808,6 +808,2261 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") {
 	memdelete(code_edit);
 }
 
+TEST_CASE("[SceneTree][CodeEdit] delimiters") {
+	CodeEdit *code_edit = memnew(CodeEdit);
+	SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+	const Point2 OUTSIDE_DELIMETER = Point2(-1, -1);
+
+	code_edit->clear_string_delimiters();
+	code_edit->clear_comment_delimiters();
+
+	SUBCASE("[CodeEdit] add and remove delimiters") {
+		SUBCASE("[CodeEdit] add and remove string delimiters") {
+			/* Add a delimiter.*/
+			code_edit->add_string_delimiter("\"", "\"", false);
+			CHECK(code_edit->has_string_delimiter("\""));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			ERR_PRINT_OFF;
+
+			/* Adding a duplicate start key is not allowed. */
+			code_edit->add_string_delimiter("\"", "\'", false);
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* Adding a duplicate end key is allowed. */
+			code_edit->add_string_delimiter("'", "\"", false);
+			CHECK(code_edit->has_string_delimiter("'"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			/* Both start and end keys have to be symbols. */
+			code_edit->add_string_delimiter("f", "\"", false);
+			CHECK_FALSE(code_edit->has_string_delimiter("f"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			code_edit->add_string_delimiter("f", "\"", false);
+			CHECK_FALSE(code_edit->has_string_delimiter("f"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			code_edit->add_string_delimiter("@", "f", false);
+			CHECK_FALSE(code_edit->has_string_delimiter("@"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			code_edit->add_string_delimiter("f", "f", false);
+			CHECK_FALSE(code_edit->has_string_delimiter("f"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			/* Blank start keys are not allowed */
+			code_edit->add_string_delimiter("", "#", false);
+			CHECK_FALSE(code_edit->has_string_delimiter("#"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			ERR_PRINT_ON;
+
+			/* Blank end keys are allowed. */
+			code_edit->add_string_delimiter("#", "", false);
+			CHECK(code_edit->has_string_delimiter("#"));
+			CHECK(code_edit->get_string_delimiters().size() == 3);
+
+			/* Remove a delimiter. */
+			code_edit->remove_string_delimiter("#");
+			CHECK_FALSE(code_edit->has_string_delimiter("#"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			/* Set should override existing, and test multiline */
+			TypedArray<String> delimiters;
+			delimiters.push_back("^^ ^^");
+
+			code_edit->set_string_delimiters(delimiters);
+			CHECK_FALSE(code_edit->has_string_delimiter("\""));
+			CHECK(code_edit->has_string_delimiter("^^"));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* clear should remove all. */
+			code_edit->clear_string_delimiters();
+			CHECK_FALSE(code_edit->has_string_delimiter("^^"));
+			CHECK(code_edit->get_string_delimiters().size() == 0);
+		}
+
+		SUBCASE("[CodeEdit] add and remove comment delimiters") {
+			/* Add a delimiter.*/
+			code_edit->add_comment_delimiter("\"", "\"", false);
+			CHECK(code_edit->has_comment_delimiter("\""));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			ERR_PRINT_OFF;
+
+			/* Adding a duplicate start key is not allowed. */
+			code_edit->add_comment_delimiter("\"", "\'", false);
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* Adding a duplicate end key is allowed. */
+			code_edit->add_comment_delimiter("'", "\"", false);
+			CHECK(code_edit->has_comment_delimiter("'"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			/* Both start and end keys have to be symbols. */
+			code_edit->add_comment_delimiter("f", "\"", false);
+			CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			code_edit->add_comment_delimiter("f", "\"", false);
+			CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			code_edit->add_comment_delimiter("@", "f", false);
+			CHECK_FALSE(code_edit->has_comment_delimiter("@"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			code_edit->add_comment_delimiter("f", "f", false);
+			CHECK_FALSE(code_edit->has_comment_delimiter("f"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			/* Blank start keys are not allowed. */
+			code_edit->add_comment_delimiter("", "#", false);
+			CHECK_FALSE(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			ERR_PRINT_ON;
+
+			/* Blank end keys are allowed. */
+			code_edit->add_comment_delimiter("#", "", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 3);
+
+			/* Remove a delimiter. */
+			code_edit->remove_comment_delimiter("#");
+			CHECK_FALSE(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			/* Set should override existing, and test multiline. */
+			TypedArray<String> delimiters;
+			delimiters.push_back("^^ ^^");
+
+			code_edit->set_comment_delimiters(delimiters);
+			CHECK_FALSE(code_edit->has_comment_delimiter("\""));
+			CHECK(code_edit->has_comment_delimiter("^^"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* clear should remove all. */
+			code_edit->clear_comment_delimiters();
+			CHECK_FALSE(code_edit->has_comment_delimiter("^^"));
+			CHECK(code_edit->get_comment_delimiters().size() == 0);
+		}
+
+		SUBCASE("[CodeEdit] add and remove mixed delimiters") {
+			code_edit->add_comment_delimiter("#", "", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			ERR_PRINT_OFF;
+
+			/* Disallow adding a string with the same start key as comment. */
+			code_edit->add_string_delimiter("#", "", false);
+			CHECK_FALSE(code_edit->has_string_delimiter("#"));
+			CHECK(code_edit->get_string_delimiters().size() == 0);
+
+			code_edit->add_string_delimiter("\"", "\"", false);
+			CHECK(code_edit->has_string_delimiter("\""));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* Disallow adding a comment with the same start key as string. */
+			code_edit->add_comment_delimiter("\"", "", false);
+			CHECK_FALSE(code_edit->has_comment_delimiter("\""));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			ERR_PRINT_ON;
+
+			/* Cannot remove string with remove comment. */
+			code_edit->remove_comment_delimiter("\"");
+			CHECK(code_edit->has_string_delimiter("\""));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* Cannot remove comment with remove string. */
+			code_edit->remove_string_delimiter("#");
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* Clear comments leave strings. */
+			code_edit->clear_comment_delimiters();
+			CHECK(code_edit->has_string_delimiter("\""));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* Clear string leave comments. */
+			code_edit->add_comment_delimiter("#", "", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			code_edit->clear_string_delimiters();
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+		}
+	}
+
+	SUBCASE("[CodeEdit] single line delimiters") {
+		SUBCASE("[CodeEdit] single line string delimiters") {
+			/* Blank end key should set lineonly to true. */
+			code_edit->add_string_delimiter("#", "", false);
+			CHECK(code_edit->has_string_delimiter("#"));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* Insert line above, line with string then line below. */
+			code_edit->insert_text_at_caret(" \n#\n ");
+
+			/* Check line above is not in string. */
+			CHECK(code_edit->is_in_string(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in string. */
+			CHECK(code_edit->is_in_string(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column after start key is in string and start / end positions are correct. */
+			CHECK(code_edit->is_in_string(1, 1) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1));
+
+			/* Check line after is not in string. */
+			CHECK(code_edit->is_in_string(2, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+			/* Check region metadata. */
+			int idx = code_edit->is_in_string(1, 1);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "");
+
+			/* Check nested strings are handeled correctly. */
+			code_edit->set_text(" \n#  # \n ");
+
+			/* Check line above is not in string. */
+			CHECK(code_edit->is_in_string(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before first start key is not in string. */
+			CHECK(code_edit->is_in_string(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column after the first start key is in string and start / end positions are correct. */
+			CHECK(code_edit->is_in_string(1, 1) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+			/* Check column after the second start key returns data for the first. */
+			CHECK(code_edit->is_in_string(1, 5) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+
+			/* Check line after is not in string. */
+			CHECK(code_edit->is_in_string(2, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+			/* Check is in string with no column retruns true if entire line is comment excluding whitespace. */
+			code_edit->set_text(" \n  #  # \n ");
+			CHECK(code_edit->is_in_string(1) != -1);
+
+			code_edit->set_text(" \n  text #  # \n ");
+			CHECK(code_edit->is_in_string(1) == -1);
+
+			/* Removing delimiter should update. */
+			code_edit->set_text(" \n  #  # \n ");
+
+			code_edit->remove_string_delimiter("#");
+			CHECK_FALSE(code_edit->has_string_delimiter("$"));
+			CHECK(code_edit->get_string_delimiters().size() == 0);
+
+			CHECK(code_edit->is_in_string(1) == -1);
+
+			/* Adding and clear should update. */
+			code_edit->add_string_delimiter("#", "", false);
+			CHECK(code_edit->has_string_delimiter("#"));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+			CHECK(code_edit->is_in_string(1) != -1);
+
+			code_edit->clear_string_delimiters();
+			CHECK_FALSE(code_edit->has_string_delimiter("$"));
+			CHECK(code_edit->get_string_delimiters().size() == 0);
+
+			CHECK(code_edit->is_in_string(1) == -1);
+		}
+
+		SUBCASE("[CodeEdit] single line comment delimiters") {
+			/* Blank end key should set lineonly to true. */
+			code_edit->add_comment_delimiter("#", "", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* Insert line above, line with comment then line below. */
+			code_edit->insert_text_at_caret(" \n#\n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column after start key is in comment and start / end positions are correct. */
+			CHECK(code_edit->is_in_comment(1, 1) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1));
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(2, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+			/* Check region metadata. */
+			int idx = code_edit->is_in_comment(1, 1);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "");
+
+			/* Check nested comments are handeled correctly. */
+			code_edit->set_text(" \n#  # \n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before first start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column after the first start key is in comment and start / end positions are correct. */
+			CHECK(code_edit->is_in_comment(1, 1) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+			/* Check column after the second start key returns data for the first. */
+			CHECK(code_edit->is_in_comment(1, 5) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(2, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+			/* Check is in comment with no column retruns true if entire line is comment excluding whitespace. */
+			code_edit->set_text(" \n  #  # \n ");
+			CHECK(code_edit->is_in_comment(1) != -1);
+
+			code_edit->set_text(" \n  text #  # \n ");
+			CHECK(code_edit->is_in_comment(1) == -1);
+
+			/* Removing delimiter should update. */
+			code_edit->set_text(" \n  #  # \n ");
+
+			code_edit->remove_comment_delimiter("#");
+			CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+			CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+			CHECK(code_edit->is_in_comment(1) == -1);
+
+			/* Adding and clear should update. */
+			code_edit->add_comment_delimiter("#", "", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+			CHECK(code_edit->is_in_comment(1) != -1);
+
+			code_edit->clear_comment_delimiters();
+			CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+			CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+			CHECK(code_edit->is_in_comment(1) == -1);
+		}
+
+		SUBCASE("[CodeEdit] single line mixed delimiters") {
+			/* Blank end key should set lineonly to true. */
+			/* Add string delimiter. */
+			code_edit->add_string_delimiter("&", "", false);
+			CHECK(code_edit->has_string_delimiter("&"));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* Add comment delimiter. */
+			code_edit->add_comment_delimiter("#", "", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* Nest a string delimiter inside a comment. */
+			code_edit->set_text(" \n#  & \n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before first start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column after the first start key is in comment and start / end positions are correct. */
+			CHECK(code_edit->is_in_comment(1, 1) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1));
+
+			/* Check column after the second start key returns data for the first, and does not state string. */
+			CHECK(code_edit->is_in_comment(1, 5) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+			CHECK(code_edit->is_in_string(1, 5) == -1);
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(2, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+			/* Remove the comment delimiter. */
+			code_edit->remove_comment_delimiter("#");
+			CHECK_FALSE(code_edit->has_comment_delimiter("$"));
+			CHECK(code_edit->get_comment_delimiters().size() == 0);
+
+			/* The "first" comment region is no longer valid. */
+			CHECK(code_edit->is_in_comment(1, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 1) == OUTSIDE_DELIMETER);
+
+			/* The "second" region as string is now valid. */
+			CHECK(code_edit->is_in_string(1, 5) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(4, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1));
+		}
+	}
+
+	SUBCASE("[CodeEdit] multiline delimiters") {
+		SUBCASE("[CodeEdit] multiline string delimiters") {
+			code_edit->clear_string_delimiters();
+			code_edit->clear_comment_delimiters();
+
+			/* Add string delimiter. */
+			code_edit->add_string_delimiter("#", "#", false);
+			CHECK(code_edit->has_string_delimiter("#"));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* First test over a single line. */
+			code_edit->set_text(" \n #  # \n ");
+
+			/* Check line above is not in string. */
+			CHECK(code_edit->is_in_string(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in string. */
+			CHECK(code_edit->is_in_string(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column before closing delimiter is in string. */
+			CHECK(code_edit->is_in_string(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1));
+
+			/* Check column after end key is not in string. */
+			CHECK(code_edit->is_in_string(1, 6) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in string. */
+			CHECK(code_edit->is_in_string(2, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+			/* Check the region metadata. */
+			int idx = code_edit->is_in_string(1, 2);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Next test over a multiple blank lines. */
+			code_edit->set_text(" \n # \n\n # \n ");
+
+			/* Check line above is not in string. */
+			CHECK(code_edit->is_in_string(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in string. */
+			CHECK(code_edit->is_in_string(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column just after start key is in string. */
+			CHECK(code_edit->is_in_string(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+			/* Check blank middle line. */
+			CHECK(code_edit->is_in_string(2, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+			/* Check column just before end key is in string. */
+			CHECK(code_edit->is_in_string(3, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+			/* Check column after end key is not in string. */
+			CHECK(code_edit->is_in_string(3, 3) == -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in string. */
+			CHECK(code_edit->is_in_string(4, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+			/* Next test over a multiple non-blank lines. */
+			code_edit->set_text(" \n # \n \n # \n ");
+
+			/* Check line above is not in string. */
+			CHECK(code_edit->is_in_string(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in string. */
+			CHECK(code_edit->is_in_string(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column just after start key is in string. */
+			CHECK(code_edit->is_in_string(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+			/* Check middle line. */
+			CHECK(code_edit->is_in_string(2, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+			/* Check column just before end key is in string. */
+			CHECK(code_edit->is_in_string(3, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+			/* Check column after end key is not in string. */
+			CHECK(code_edit->is_in_string(3, 3) == -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in string. */
+			CHECK(code_edit->is_in_string(4, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+			/* check the region metadata. */
+			idx = code_edit->is_in_string(1, 2);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Next test nested strings. */
+			code_edit->add_string_delimiter("^", "^", false);
+			CHECK(code_edit->has_string_delimiter("^"));
+			CHECK(code_edit->get_string_delimiters().size() == 2);
+
+			code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+			/* Check line above is not in string. */
+			CHECK(code_edit->is_in_string(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in string. */
+			CHECK(code_edit->is_in_string(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column just after start key is in string. */
+			CHECK(code_edit->is_in_string(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+			/* Check middle line. */
+			CHECK(code_edit->is_in_string(2, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+			/* Check column just before end key is in string. */
+			CHECK(code_edit->is_in_string(3, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+			/* Check column after end key is not in string. */
+			CHECK(code_edit->is_in_string(3, 3) == -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in string. */
+			CHECK(code_edit->is_in_string(4, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+			/* check the region metadata. */
+			idx = code_edit->is_in_string(1, 2);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Next test no end key. */
+			code_edit->set_text(" \n # \n ");
+
+			/* check the region metadata. */
+			idx = code_edit->is_in_string(1, 2);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1));
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Check is in string with no column retruns true if entire line is string excluding whitespace. */
+			code_edit->set_text(" \n # \n\n #\n ");
+			CHECK(code_edit->is_in_string(1) != -1);
+			CHECK(code_edit->is_in_string(2) != -1);
+			CHECK(code_edit->is_in_string(3) != -1);
+
+			code_edit->set_text(" \n test # \n\n # test \n ");
+			CHECK(code_edit->is_in_string(1) == -1);
+			CHECK(code_edit->is_in_string(2) != -1);
+			CHECK(code_edit->is_in_string(3) == -1);
+		}
+
+		SUBCASE("[CodeEdit] multiline comment delimiters") {
+			/* Add comment delimiter. */
+			code_edit->add_comment_delimiter("#", "#", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* First test over a single line. */
+			code_edit->set_text(" \n #  # \n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column before closing delimiter is in comment. */
+			CHECK(code_edit->is_in_comment(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1));
+
+			/* Check column after end key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 6) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(2, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER);
+
+			/* Check the region metadata. */
+			int idx = code_edit->is_in_comment(1, 2);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Next test over a multiple blank lines. */
+			code_edit->set_text(" \n # \n\n # \n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column just after start key is in comment. */
+			CHECK(code_edit->is_in_comment(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+			/* Check blank middle line. */
+			CHECK(code_edit->is_in_comment(2, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+			/* Check column just before end key is in comment. */
+			CHECK(code_edit->is_in_comment(3, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+			/* Check column after end key is not in comment. */
+			CHECK(code_edit->is_in_comment(3, 3) == -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(4, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+			/* Next test over a multiple non-blank lines. */
+			code_edit->set_text(" \n # \n \n # \n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column just after start key is in comment. */
+			CHECK(code_edit->is_in_comment(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3));
+
+			/* Check middle line. */
+			CHECK(code_edit->is_in_comment(2, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3));
+
+			/* Check column just before end key is in comment. */
+			CHECK(code_edit->is_in_comment(3, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3));
+
+			/* Check column after end key is not in comment. */
+			CHECK(code_edit->is_in_comment(3, 3) == -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(4, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+			/* check the region metadata. */
+			idx = code_edit->is_in_comment(1, 2);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Next test nested comments. */
+			code_edit->add_comment_delimiter("^", "^", false);
+			CHECK(code_edit->has_comment_delimiter("^"));
+			CHECK(code_edit->get_comment_delimiters().size() == 2);
+
+			code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column just after start key is in comment. */
+			CHECK(code_edit->is_in_comment(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+			/* Check middle line. */
+			CHECK(code_edit->is_in_comment(2, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+			/* Check column just before end key is in comment. */
+			CHECK(code_edit->is_in_comment(3, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+			/* Check column after end key is not in comment. */
+			CHECK(code_edit->is_in_comment(3, 3) == -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(4, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+			/* check the region metadata. */
+			idx = code_edit->is_in_comment(1, 2);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Next test no end key. */
+			code_edit->set_text(" \n # \n ");
+
+			/* check the region metadata. */
+			idx = code_edit->is_in_comment(1, 2);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1));
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Check is in comment with no column retruns true if entire line is comment excluding whitespace. */
+			code_edit->set_text(" \n # \n\n #\n ");
+			CHECK(code_edit->is_in_comment(1) != -1);
+			CHECK(code_edit->is_in_comment(2) != -1);
+			CHECK(code_edit->is_in_comment(3) != -1);
+
+			code_edit->set_text(" \n test # \n\n # test \n ");
+			CHECK(code_edit->is_in_comment(1) == -1);
+			CHECK(code_edit->is_in_comment(2) != -1);
+			CHECK(code_edit->is_in_comment(3) == -1);
+		}
+
+		SUBCASE("[CodeEdit] multiline mixed delimiters") {
+			/* Add comment delimiter. */
+			code_edit->add_comment_delimiter("#", "#", false);
+			CHECK(code_edit->has_comment_delimiter("#"));
+			CHECK(code_edit->get_comment_delimiters().size() == 1);
+
+			/* Add string delimiter. */
+			code_edit->add_string_delimiter("^", "^", false);
+			CHECK(code_edit->has_string_delimiter("^"));
+			CHECK(code_edit->get_string_delimiters().size() == 1);
+
+			/* Nest a string inside a comment. */
+			code_edit->set_text(" \n # ^\n \n^ # \n ");
+
+			/* Check line above is not in comment. */
+			CHECK(code_edit->is_in_comment(0, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER);
+
+			/* Check column before start key is not in comment. */
+			CHECK(code_edit->is_in_comment(1, 0) == -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER);
+
+			/* Check column just after start key is in comment. */
+			CHECK(code_edit->is_in_comment(1, 2) != -1);
+			CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3));
+
+			/* Check middle line. */
+			CHECK(code_edit->is_in_comment(2, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3));
+
+			/* Check column just before end key is in comment. */
+			CHECK(code_edit->is_in_comment(3, 0) != -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1));
+			CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3));
+
+			/* Check column after end key is not in comment. */
+			CHECK(code_edit->is_in_comment(3, 3) == -1);
+			CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER);
+
+			/* Check line after is not in comment. */
+			CHECK(code_edit->is_in_comment(4, 1) == -1);
+			CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER);
+			CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER);
+
+			/* check the region metadata. */
+			int idx = code_edit->is_in_comment(1, 2);
+			CHECK(code_edit->get_delimiter_start_key(idx) == "#");
+			CHECK(code_edit->get_delimiter_end_key(idx) == "#");
+
+			/* Check is in comment with no column retruns true as inner delimiter should not be counted. */
+			CHECK(code_edit->is_in_comment(1) != -1);
+			CHECK(code_edit->is_in_comment(2) != -1);
+			CHECK(code_edit->is_in_comment(3) != -1);
+		}
+	}
+
+	memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] indent") {
+	CodeEdit *code_edit = memnew(CodeEdit);
+	SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+	SUBCASE("[CodeEdit] indent settings") {
+		code_edit->set_indent_size(10);
+		CHECK(code_edit->get_indent_size() == 10);
+		CHECK(code_edit->get_tab_size() == 10);
+
+		code_edit->set_auto_indent_enabled(false);
+		CHECK_FALSE(code_edit->is_auto_indent_enabled());
+
+		code_edit->set_auto_indent_enabled(true);
+		CHECK(code_edit->is_auto_indent_enabled());
+
+		code_edit->set_indent_using_spaces(false);
+		CHECK_FALSE(code_edit->is_indent_using_spaces());
+
+		code_edit->set_indent_using_spaces(true);
+		CHECK(code_edit->is_indent_using_spaces());
+
+		/* Only the first char is registered. */
+		TypedArray<String> auto_indent_prefixes;
+		auto_indent_prefixes.push_back("::");
+		auto_indent_prefixes.push_back("s");
+		auto_indent_prefixes.push_back("1");
+		code_edit->set_auto_indent_prefixes(auto_indent_prefixes);
+
+		auto_indent_prefixes = code_edit->get_auto_indent_prefixes();
+		CHECK(auto_indent_prefixes.has(":"));
+		CHECK(auto_indent_prefixes.has("s"));
+		CHECK(auto_indent_prefixes.has("1"));
+	}
+
+	SUBCASE("[CodeEdit] indent tabs") {
+		code_edit->set_indent_size(4);
+		code_edit->set_auto_indent_enabled(true);
+		code_edit->set_indent_using_spaces(false);
+
+		/* Do nothing if not editable. */
+		code_edit->set_editable(false);
+
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0).is_empty());
+
+		code_edit->indent_lines();
+		CHECK(code_edit->get_line(0).is_empty());
+
+		code_edit->set_editable(true);
+
+		/* Simple indent. */
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "\t");
+
+		/* Check input action. */
+		SEND_GUI_ACTION(code_edit, "ui_text_indent");
+		CHECK(code_edit->get_line(0) == "\t\t");
+
+		/* Insert in place. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("test");
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "test\t");
+
+		/* Indent lines does entire line and works without selection. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("test");
+		code_edit->indent_lines();
+		CHECK(code_edit->get_line(0) == "\ttest");
+
+		/* Selection does entire line. */
+		code_edit->set_text("test");
+		code_edit->select_all();
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "\ttest");
+
+		/* Handles multiple lines. */
+		code_edit->set_text("test\ntext");
+		code_edit->select_all();
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "\ttest");
+		CHECK(code_edit->get_line(1) == "\ttext");
+
+		/* Do not indent line if last col is zero. */
+		code_edit->set_text("test\ntext");
+		code_edit->select(0, 0, 1, 0);
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "\ttest");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Indent even if last column of first line. */
+		code_edit->set_text("test\ntext");
+		code_edit->select(0, 4, 1, 0);
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "\ttest");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Check selection is adjusted. */
+		code_edit->set_text("test");
+		code_edit->select(0, 1, 0, 2);
+		code_edit->do_indent();
+		CHECK(code_edit->get_selection_from_column() == 2);
+		CHECK(code_edit->get_selection_to_column() == 3);
+		CHECK(code_edit->get_line(0) == "\ttest");
+		code_edit->undo();
+	}
+
+	SUBCASE("[CodeEdit] indent spaces") {
+		code_edit->set_indent_size(4);
+		code_edit->set_auto_indent_enabled(true);
+		code_edit->set_indent_using_spaces(true);
+
+		/* Do nothing if not editable. */
+		code_edit->set_editable(false);
+
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0).is_empty());
+
+		code_edit->indent_lines();
+		CHECK(code_edit->get_line(0).is_empty());
+
+		code_edit->set_editable(true);
+
+		/* Simple indent. */
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "    ");
+
+		/* Check input action. */
+		SEND_GUI_ACTION(code_edit, "ui_text_indent");
+		CHECK(code_edit->get_line(0) == "        ");
+
+		/* Insert in place. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("test");
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "test    ");
+
+		/* Indent lines does entire line and works without selection. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("test");
+		code_edit->indent_lines();
+		CHECK(code_edit->get_line(0) == "    test");
+
+		/* Selection does entire line. */
+		code_edit->set_text("test");
+		code_edit->select_all();
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "    test");
+
+		/* single indent only add required spaces. */
+		code_edit->set_text(" test");
+		code_edit->select_all();
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "    test");
+
+		/* Handles multiple lines. */
+		code_edit->set_text("test\ntext");
+		code_edit->select_all();
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "    test");
+		CHECK(code_edit->get_line(1) == "    text");
+
+		/* Do not indent line if last col is zero. */
+		code_edit->set_text("test\ntext");
+		code_edit->select(0, 0, 1, 0);
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "    test");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Indent even if last column of first line. */
+		code_edit->set_text("test\ntext");
+		code_edit->select(0, 4, 1, 0);
+		code_edit->do_indent();
+		CHECK(code_edit->get_line(0) == "    test");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Check selection is adjusted. */
+		code_edit->set_text("test");
+		code_edit->select(0, 1, 0, 2);
+		code_edit->do_indent();
+		CHECK(code_edit->get_selection_from_column() == 5);
+		CHECK(code_edit->get_selection_to_column() == 6);
+		CHECK(code_edit->get_line(0) == "    test");
+	}
+
+	SUBCASE("[CodeEdit] unindent tabs") {
+		code_edit->set_indent_size(4);
+		code_edit->set_auto_indent_enabled(true);
+		code_edit->set_indent_using_spaces(false);
+
+		/* Do nothing if not editable. */
+		code_edit->set_text("\t");
+
+		code_edit->set_editable(false);
+
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "\t");
+
+		code_edit->unindent_lines();
+		CHECK(code_edit->get_line(0) == "\t");
+
+		code_edit->set_editable(true);
+
+		/* Simple unindent. */
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "");
+
+		/* Should inindent inplace. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("test\t");
+
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+
+		/* Backspace does a simple unindent. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("\t");
+		code_edit->backspace();
+		CHECK(code_edit->get_line(0) == "");
+
+		/* Unindent lines does entire line and works without selection. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("\ttest");
+		code_edit->unindent_lines();
+		CHECK(code_edit->get_line(0) == "test");
+
+		/* Caret on col zero unindent line. */
+		code_edit->set_text("\t\ttest");
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "\ttest");
+
+		/* Check input action. */
+		code_edit->set_text("\t\ttest");
+		SEND_GUI_ACTION(code_edit, "ui_text_dedent");
+		CHECK(code_edit->get_line(0) == "\ttest");
+
+		/* Selection does entire line. */
+		code_edit->set_text("\t\ttest");
+		code_edit->select_all();
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "\ttest");
+
+		/* Handles multiple lines. */
+		code_edit->set_text("\ttest\n\ttext");
+		code_edit->select_all();
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Do not unindent line if last col is zero. */
+		code_edit->set_text("\ttest\n\ttext");
+		code_edit->select(0, 0, 1, 0);
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+		CHECK(code_edit->get_line(1) == "\ttext");
+
+		/* Unindent even if last column of first line. */
+		code_edit->set_text("\ttest\n\ttext");
+		code_edit->select(0, 5, 1, 1);
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Check selection is adjusted. */
+		code_edit->set_text("\ttest");
+		code_edit->select(0, 1, 0, 2);
+		code_edit->do_unindent();
+		CHECK(code_edit->get_selection_from_column() == 0);
+		CHECK(code_edit->get_selection_to_column() == 1);
+		CHECK(code_edit->get_line(0) == "test");
+	}
+
+	SUBCASE("[CodeEdit] unindent spaces") {
+		code_edit->set_indent_size(4);
+		code_edit->set_auto_indent_enabled(true);
+		code_edit->set_indent_using_spaces(true);
+
+		/* Do nothing if not editable. */
+		code_edit->set_text("    ");
+
+		code_edit->set_editable(false);
+
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "    ");
+
+		code_edit->unindent_lines();
+		CHECK(code_edit->get_line(0) == "    ");
+
+		code_edit->set_editable(true);
+
+		/* Simple unindent. */
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "");
+
+		/* Should inindent inplace. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("test    ");
+
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+
+		/* Backspace does a simple unindent. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("    ");
+		code_edit->backspace();
+		CHECK(code_edit->get_line(0) == "");
+
+		/* Backspace with letter. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("    a");
+		code_edit->backspace();
+		CHECK(code_edit->get_line(0) == "    ");
+
+		/* Unindent lines does entire line and works without selection. */
+		code_edit->set_text("");
+		code_edit->insert_text_at_caret("    test");
+		code_edit->unindent_lines();
+		CHECK(code_edit->get_line(0) == "test");
+
+		/* Caret on col zero unindent line. */
+		code_edit->set_text("        test");
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "    test");
+
+		/* Only as far as needed */
+		code_edit->set_text("       test");
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "    test");
+
+		/* Check input action. */
+		code_edit->set_text("        test");
+		SEND_GUI_ACTION(code_edit, "ui_text_dedent");
+		CHECK(code_edit->get_line(0) == "    test");
+
+		/* Selection does entire line. */
+		code_edit->set_text("        test");
+		code_edit->select_all();
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "    test");
+
+		/* Handles multiple lines. */
+		code_edit->set_text("    test\n    text");
+		code_edit->select_all();
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Do not unindent line if last col is zero. */
+		code_edit->set_text("    test\n    text");
+		code_edit->select(0, 0, 1, 0);
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+		CHECK(code_edit->get_line(1) == "    text");
+
+		/* Unindent even if last column of first line. */
+		code_edit->set_text("    test\n    text");
+		code_edit->select(0, 5, 1, 1);
+		code_edit->do_unindent();
+		CHECK(code_edit->get_line(0) == "test");
+		CHECK(code_edit->get_line(1) == "text");
+
+		/* Check selection is adjusted. */
+		code_edit->set_text("    test");
+		code_edit->select(0, 4, 0, 5);
+		code_edit->do_unindent();
+		CHECK(code_edit->get_selection_from_column() == 0);
+		CHECK(code_edit->get_selection_to_column() == 1);
+		CHECK(code_edit->get_line(0) == "test");
+	}
+
+	SUBCASE("[CodeEdit] auto indent") {
+		SUBCASE("[CodeEdit] auto indent tabs") {
+			code_edit->set_indent_size(4);
+			code_edit->set_auto_indent_enabled(true);
+			code_edit->set_indent_using_spaces(false);
+
+			/* Simple indent on new line. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test:");
+			CHECK(code_edit->get_line(1) == "\t");
+
+			/* new blank line should still indent. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+			CHECK(code_edit->get_line(0) == "test:");
+			CHECK(code_edit->get_line(1) == "\t");
+
+			/* new line above should not indent. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+			CHECK(code_edit->get_line(0) == "");
+			CHECK(code_edit->get_line(1) == "test:");
+
+			/* Whitespace between symbol and caret is okay. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:  ");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test:  ");
+			CHECK(code_edit->get_line(1) == "\t");
+
+			/* Comment between symbol and caret is okay. */
+			code_edit->add_comment_delimiter("#", "");
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test: # comment");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test: # comment");
+			CHECK(code_edit->get_line(1) == "\t");
+			code_edit->remove_comment_delimiter("#");
+
+			/* Strings between symbol and caret are not okay. */
+			code_edit->add_string_delimiter("#", "");
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test: # string");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test: # string");
+			CHECK(code_edit->get_line(1) == "");
+			code_edit->remove_comment_delimiter("#");
+
+			/* If between brace pairs an extra line is added. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test{}");
+			code_edit->set_caret_column(5);
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test{");
+			CHECK(code_edit->get_line(1) == "\t");
+			CHECK(code_edit->get_line(2) == "}");
+
+			/* Except when we are going above. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test{}");
+			code_edit->set_caret_column(5);
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+			CHECK(code_edit->get_line(0) == "");
+			CHECK(code_edit->get_line(1) == "test{}");
+
+			/* or below. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test{}");
+			code_edit->set_caret_column(5);
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+			CHECK(code_edit->get_line(0) == "test{}");
+			CHECK(code_edit->get_line(1) == "");
+		}
+
+		SUBCASE("[CodeEdit] auto indent spaces") {
+			code_edit->set_indent_size(4);
+			code_edit->set_auto_indent_enabled(true);
+			code_edit->set_indent_using_spaces(true);
+
+			/* Simple indent on new line. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test:");
+			CHECK(code_edit->get_line(1) == "    ");
+
+			/* new blank line should still indent. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+			CHECK(code_edit->get_line(0) == "test:");
+			CHECK(code_edit->get_line(1) == "    ");
+
+			/* new line above should not indent. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+			CHECK(code_edit->get_line(0) == "");
+			CHECK(code_edit->get_line(1) == "test:");
+
+			/* Whitespace between symbol and caret is okay. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test:  ");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test:  ");
+			CHECK(code_edit->get_line(1) == "    ");
+
+			/* Comment between symbol and caret is okay. */
+			code_edit->add_comment_delimiter("#", "");
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test: # comment");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test: # comment");
+			CHECK(code_edit->get_line(1) == "    ");
+			code_edit->remove_comment_delimiter("#");
+
+			/* Strings between symbol and caret are not okay. */
+			code_edit->add_string_delimiter("#", "");
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test: # string");
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test: # string");
+			CHECK(code_edit->get_line(1) == "");
+			code_edit->remove_comment_delimiter("#");
+
+			/* If between brace pairs an extra line is added. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test{}");
+			code_edit->set_caret_column(5);
+			SEND_GUI_ACTION(code_edit, "ui_text_newline");
+			CHECK(code_edit->get_line(0) == "test{");
+			CHECK(code_edit->get_line(1) == "    ");
+			CHECK(code_edit->get_line(2) == "}");
+
+			/* Except when we are going above. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test{}");
+			code_edit->set_caret_column(5);
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_above");
+			CHECK(code_edit->get_line(0) == "");
+			CHECK(code_edit->get_line(1) == "test{}");
+
+			/* or below. */
+			code_edit->set_text("");
+			code_edit->insert_text_at_caret("test{}");
+			code_edit->set_caret_column(5);
+			SEND_GUI_ACTION(code_edit, "ui_text_newline_blank");
+			CHECK(code_edit->get_line(0) == "test{}");
+			CHECK(code_edit->get_line(1) == "");
+		}
+	}
+
+	memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] folding") {
+	CodeEdit *code_edit = memnew(CodeEdit);
+	SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+	SUBCASE("[CodeEdit] folding settings") {
+		code_edit->set_line_folding_enabled(true);
+		CHECK(code_edit->is_line_folding_enabled());
+
+		code_edit->set_line_folding_enabled(false);
+		CHECK_FALSE(code_edit->is_line_folding_enabled());
+	}
+
+	SUBCASE("[CodeEdit] folding") {
+		code_edit->set_line_folding_enabled(true);
+
+		// No indent.
+		code_edit->set_text("line1\nline2\nline3");
+		for (int i = 0; i < 2; i++) {
+			CHECK_FALSE(code_edit->can_fold_line(i));
+			code_edit->fold_line(i);
+			CHECK_FALSE(code_edit->is_line_folded(i));
+		}
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Indented lines.
+		code_edit->set_text("\tline1\n\tline2\n\tline3");
+		for (int i = 0; i < 2; i++) {
+			CHECK_FALSE(code_edit->can_fold_line(i));
+			code_edit->fold_line(i);
+			CHECK_FALSE(code_edit->is_line_folded(i));
+		}
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Indent.
+		code_edit->set_text("line1\n\tline2\nline3");
+		CHECK(code_edit->can_fold_line(0));
+		for (int i = 1; i < 2; i++) {
+			CHECK_FALSE(code_edit->can_fold_line(i));
+			code_edit->fold_line(i);
+			CHECK_FALSE(code_edit->is_line_folded(i));
+		}
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK_FALSE(code_edit->is_line_folded(2));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2);
+
+		// Nested indents.
+		code_edit->set_text("line1\n\tline2\n\t\tline3\nline4");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK(code_edit->can_fold_line(1));
+		for (int i = 2; i < 3; i++) {
+			CHECK_FALSE(code_edit->can_fold_line(i));
+			code_edit->fold_line(i);
+			CHECK_FALSE(code_edit->is_line_folded(i));
+		}
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK(code_edit->is_line_folded(1));
+		CHECK_FALSE(code_edit->is_line_folded(2));
+		CHECK_FALSE(code_edit->is_line_folded(3));
+		CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 2);
+
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK_FALSE(code_edit->is_line_folded(2));
+		CHECK_FALSE(code_edit->is_line_folded(3));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+		// Check metadata.
+		CHECK(code_edit->get_folded_lines().size() == 1);
+		CHECK((int)code_edit->get_folded_lines()[0] == 0);
+
+		// Cannot unfold nested.
+		code_edit->unfold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// (un)Fold all / toggle.
+		code_edit->unfold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Check metadata.
+		CHECK(code_edit->get_folded_lines().size() == 0);
+
+		code_edit->fold_all_lines();
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+		code_edit->unfold_all_lines();
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		code_edit->toggle_foldable_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3);
+
+		// Can also unfold from hidden line.
+		code_edit->unfold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Blank lines.
+		code_edit->set_text("line1\n\tline2\n\n\n\ttest\n\nline3");
+		CHECK(code_edit->can_fold_line(0));
+		for (int i = 1; i < code_edit->get_line_count(); i++) {
+			CHECK_FALSE(code_edit->can_fold_line(i));
+			code_edit->fold_line(i);
+			CHECK_FALSE(code_edit->is_line_folded(i));
+		}
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		for (int i = 1; i < code_edit->get_line_count(); i++) {
+			CHECK_FALSE(code_edit->is_line_folded(i));
+		}
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 6);
+
+		// End of file.
+		code_edit->set_text("line1\n\tline2");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Comment & string blocks.
+		// Single line block
+		code_edit->add_comment_delimiter("#", "", true);
+		code_edit->set_text("#line1\n#\tline2");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Has to be full line.
+		code_edit->set_text("test #line1\n#\tline2");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		code_edit->set_text("#line1\ntest #\tline2");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// String.
+		code_edit->add_string_delimiter("^", "", true);
+		code_edit->set_text("^line1\n^\tline2");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Has to be full line.
+		code_edit->set_text("test ^line1\n^\tline2");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		code_edit->set_text("^line1\ntest ^\tline2");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Multiline blocks.
+		code_edit->add_comment_delimiter("&", "&", false);
+		code_edit->set_text("&line1\n\tline2&");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Has to be full line.
+		code_edit->set_text("test &line1\n\tline2&");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		code_edit->set_text("&line1\n\tline2& test");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Strings.
+		code_edit->add_string_delimiter("$", "$", false);
+		code_edit->set_text("$line1\n\tline2$");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Has to be full line.
+		code_edit->set_text("test $line1\n\tline2$");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		code_edit->set_text("$line1\n\tline2$ test");
+		CHECK_FALSE(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK_FALSE(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1);
+
+		// Non-indented comments/ strings.
+		// Single line
+		code_edit->set_text("test\n\tline1\n#line1\n#line2\n\ttest");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+		code_edit->set_text("test\n\tline1\n^line1\n^line2\n\ttest");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+		// Multiline
+		code_edit->set_text("test\n\tline1\n&line1\nline2&\n\ttest");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+
+		code_edit->set_text("test\n\tline1\n$line1\nline2$\n\ttest");
+		CHECK(code_edit->can_fold_line(0));
+		CHECK_FALSE(code_edit->can_fold_line(1));
+		code_edit->fold_line(1);
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		code_edit->fold_line(0);
+		CHECK(code_edit->is_line_folded(0));
+		CHECK_FALSE(code_edit->is_line_folded(1));
+		CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4);
+	}
+
+	memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] completion") {
+	CodeEdit *code_edit = memnew(CodeEdit);
+	SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+	SUBCASE("[CodeEdit] auto brace completion") {
+		code_edit->set_auto_brace_completion_enabled(true);
+		CHECK(code_edit->is_auto_brace_completion_enabled());
+
+		code_edit->set_highlight_matching_braces_enabled(true);
+		CHECK(code_edit->is_highlight_matching_braces_enabled());
+
+		/* Try setters, any length. */
+		Dictionary auto_brace_completion_pairs;
+		auto_brace_completion_pairs["["] = "]";
+		auto_brace_completion_pairs["'"] = "'";
+		auto_brace_completion_pairs[";"] = "'";
+		auto_brace_completion_pairs["'''"] = "'''";
+		code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs);
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+		CHECK(code_edit->get_auto_brace_completion_pairs()["["] == "]");
+		CHECK(code_edit->get_auto_brace_completion_pairs()["'"] == "'");
+		CHECK(code_edit->get_auto_brace_completion_pairs()[";"] == "'");
+		CHECK(code_edit->get_auto_brace_completion_pairs()["'''"] == "'''");
+
+		ERR_PRINT_OFF;
+
+		/* No duplicate start keys. */
+		code_edit->add_auto_brace_completion_pair("[", "]");
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+		/* No empty keys. */
+		code_edit->add_auto_brace_completion_pair("[", "");
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+		code_edit->add_auto_brace_completion_pair("", "]");
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+		code_edit->add_auto_brace_completion_pair("", "");
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+		/* Must be a symbol. */
+		code_edit->add_auto_brace_completion_pair("a", "]");
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+		code_edit->add_auto_brace_completion_pair("[", "a");
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+		code_edit->add_auto_brace_completion_pair("a", "a");
+		CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4);
+
+		ERR_PRINT_ON;
+
+		/* Check metadata. */
+		CHECK(code_edit->has_auto_brace_completion_open_key("["));
+		CHECK(code_edit->has_auto_brace_completion_open_key("'"));
+		CHECK(code_edit->has_auto_brace_completion_open_key(";"));
+		CHECK(code_edit->has_auto_brace_completion_open_key("'''"));
+		CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("("));
+
+		CHECK(code_edit->has_auto_brace_completion_close_key("]"));
+		CHECK(code_edit->has_auto_brace_completion_close_key("'"));
+		CHECK(code_edit->has_auto_brace_completion_close_key("'''"));
+		CHECK_FALSE(code_edit->has_auto_brace_completion_close_key(")"));
+
+		CHECK(code_edit->get_auto_brace_completion_close_key("[") == "]");
+		CHECK(code_edit->get_auto_brace_completion_close_key("'") == "'");
+		CHECK(code_edit->get_auto_brace_completion_close_key(";") == "'");
+		CHECK(code_edit->get_auto_brace_completion_close_key("'''") == "'''");
+		CHECK(code_edit->get_auto_brace_completion_close_key("(").is_empty());
+
+		/* Check typing inserts closing pair. */
+		code_edit->clear();
+		SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETLEFT);
+		CHECK(code_edit->get_line(0) == "[]");
+
+		/* Should first match and insert smaller key. */
+		code_edit->clear();
+		SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE);
+		CHECK(code_edit->get_line(0) == "''");
+		CHECK(code_edit->get_caret_column() == 1);
+
+		/* Move out from centre, Should match and insert larger key. */
+		SEND_GUI_ACTION(code_edit, "ui_text_caret_right");
+		SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE);
+		CHECK(code_edit->get_line(0) == "''''''");
+		CHECK(code_edit->get_caret_column() == 3);
+
+		/* Backspace should remove all. */
+		SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+		CHECK(code_edit->get_line(0).is_empty());
+
+		/* If in between and typing close key should "skip". */
+		SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETLEFT);
+		CHECK(code_edit->get_line(0) == "[]");
+		CHECK(code_edit->get_caret_column() == 1);
+		SEND_GUI_KEY_EVENT(code_edit, KEY_BRACKETRIGHT);
+		CHECK(code_edit->get_line(0) == "[]");
+		CHECK(code_edit->get_caret_column() == 2);
+
+		/* If current is char and inserting a string, do not autocomplete. */
+		code_edit->clear();
+		SEND_GUI_KEY_EVENT(code_edit, KEY_A);
+		SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE);
+		CHECK(code_edit->get_line(0) == "A'");
+
+		/* If in comment, do not complete. */
+		code_edit->add_comment_delimiter("#", "");
+		code_edit->clear();
+		SEND_GUI_KEY_EVENT(code_edit, KEY_NUMBERSIGN);
+		SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE);
+		CHECK(code_edit->get_line(0) == "#'");
+
+		/* If in string, and inserting string do not complete. */
+		code_edit->clear();
+		SEND_GUI_KEY_EVENT(code_edit, KEY_APOSTROPHE);
+		SEND_GUI_KEY_EVENT(code_edit, KEY_QUOTEDBL);
+		CHECK(code_edit->get_line(0) == "'\"'");
+	}
+
+	SUBCASE("[CodeEdit] autocomplete") {
+		code_edit->set_code_completion_enabled(true);
+		CHECK(code_edit->is_code_completion_enabled());
+
+		/* Set prefixes, single char only, disallow empty. */
+		TypedArray<String> completion_prefixes;
+		completion_prefixes.push_back("");
+		completion_prefixes.push_back(".");
+		completion_prefixes.push_back(".");
+		completion_prefixes.push_back(",,");
+
+		ERR_PRINT_OFF;
+		code_edit->set_code_completion_prefixes(completion_prefixes);
+		ERR_PRINT_ON;
+		completion_prefixes = code_edit->get_code_completion_prefixes();
+		CHECK(completion_prefixes.size() == 2);
+		CHECK(completion_prefixes.has("."));
+		CHECK(completion_prefixes.has(","));
+
+		code_edit->set_text("test\ntest");
+		CHECK(code_edit->get_text_for_code_completion() == String::chr(0xFFFF) + "test\ntest");
+	}
+
+	SUBCASE("[CodeEdit] autocomplete request") {
+		SIGNAL_WATCH(code_edit, "request_code_completion");
+		code_edit->set_code_completion_enabled(true);
+
+		Array signal_args;
+		signal_args.push_back(Array());
+
+		/* Force request. */
+		code_edit->request_code_completion();
+		SIGNAL_CHECK_FALSE("request_code_completion");
+		code_edit->request_code_completion(true);
+		SIGNAL_CHECK("request_code_completion", signal_args);
+
+		/* Manual request should force. */
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_query");
+		SIGNAL_CHECK("request_code_completion", signal_args);
+
+		/* Insert prefix. */
+		TypedArray<String> completion_prefixes;
+		completion_prefixes.push_back(".");
+		code_edit->set_code_completion_prefixes(completion_prefixes);
+
+		code_edit->insert_text_at_caret(".");
+		code_edit->request_code_completion();
+		SIGNAL_CHECK("request_code_completion", signal_args);
+
+		/* Should work with space too. */
+		code_edit->insert_text_at_caret(" ");
+		code_edit->request_code_completion();
+		SIGNAL_CHECK("request_code_completion", signal_args);
+
+		/* Should work when complete ends with prefix. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("t");
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "test.", "test.");
+		code_edit->update_code_completion_options();
+		code_edit->confirm_code_completion();
+		CHECK(code_edit->get_line(0) == "test.");
+		SIGNAL_CHECK("request_code_completion", signal_args);
+
+		SIGNAL_UNWATCH(code_edit, "request_code_completion");
+	}
+
+	SUBCASE("[CodeEdit] autocomplete completion") {
+		CHECK(code_edit->get_code_completion_selected_index() == -1);
+		code_edit->set_code_completion_enabled(true);
+		CHECK(code_edit->get_code_completion_selected_index() == -1);
+
+		code_edit->update_code_completion_options();
+		code_edit->set_code_completion_selected_index(1);
+		CHECK(code_edit->get_code_completion_selected_index() == -1);
+		CHECK(code_edit->get_code_completion_option(0).size() == 0);
+		CHECK(code_edit->get_code_completion_options().size() == 0);
+
+		/* Adding does not update the list. */
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_0.", "item_0");
+
+		code_edit->set_code_completion_selected_index(1);
+		CHECK(code_edit->get_code_completion_selected_index() == -1);
+		CHECK(code_edit->get_code_completion_option(0).size() == 0);
+		CHECK(code_edit->get_code_completion_options().size() == 0);
+
+		/* After update, pending add should not be counted, */
+		/* also does not work on col 0                      */
+		code_edit->insert_text_at_caret("i");
+		code_edit->update_code_completion_options();
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0", Color(1, 0, 0), RES(), Color(1, 0, 0));
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_1.", "item_1");
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_2.", "item_2");
+
+		ERR_PRINT_OFF;
+		code_edit->set_code_completion_selected_index(1);
+		ERR_PRINT_ON;
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+		CHECK(code_edit->get_code_completion_option(0).size() == 6);
+		CHECK(code_edit->get_code_completion_options().size() == 1);
+
+		/* Check cancel closes completion. */
+		SEND_GUI_ACTION(code_edit, "ui_cancel");
+		CHECK(code_edit->get_code_completion_selected_index() == -1);
+
+		code_edit->update_code_completion_options();
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+		code_edit->set_code_completion_selected_index(1);
+		CHECK(code_edit->get_code_completion_selected_index() == 1);
+		CHECK(code_edit->get_code_completion_option(0).size() == 6);
+		CHECK(code_edit->get_code_completion_options().size() == 3);
+
+		/* Check data. */
+		Dictionary option = code_edit->get_code_completion_option(0);
+		CHECK((int)option["kind"] == (int)CodeEdit::CodeCompletionKind::KIND_CLASS);
+		CHECK(option["display_text"] == "item_0.");
+		CHECK(option["insert_text"] == "item_0");
+		CHECK(option["font_color"] == Color(1, 0, 0));
+		CHECK(option["icon"] == RES());
+		CHECK(option["default_value"] == Color(1, 0, 0));
+
+		/* Set size for mouse input. */
+		code_edit->set_size(Size2(100, 100));
+
+		/* Check input. */
+		SEND_GUI_ACTION(code_edit, "ui_end");
+		CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+		SEND_GUI_ACTION(code_edit, "ui_home");
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		SEND_GUI_ACTION(code_edit, "ui_page_down");
+		CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+		SEND_GUI_ACTION(code_edit, "ui_page_up");
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		SEND_GUI_ACTION(code_edit, "ui_up");
+		CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+		SEND_GUI_ACTION(code_edit, "ui_down");
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		SEND_GUI_KEY_EVENT(code_edit, KEY_T);
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		SEND_GUI_ACTION(code_edit, "ui_left");
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		SEND_GUI_ACTION(code_edit, "ui_right");
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		SEND_GUI_ACTION(code_edit, "ui_text_backspace");
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		Point2 caret_pos = code_edit->get_caret_draw_pos();
+		caret_pos.y -= code_edit->get_line_height();
+		SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_NONE);
+		CHECK(code_edit->get_code_completion_selected_index() == 1);
+
+		SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_NONE);
+		CHECK(code_edit->get_code_completion_selected_index() == 0);
+
+		/* Single click selects. */
+		SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MASK_LEFT);
+		CHECK(code_edit->get_code_completion_selected_index() == 2);
+
+		/* Double click inserts. */
+		SEND_GUI_DOUBLE_CLICK(code_edit, caret_pos);
+		CHECK(code_edit->get_code_completion_selected_index() == -1);
+		CHECK(code_edit->get_line(0) == "item_2");
+
+		code_edit->set_auto_brace_completion_enabled(false);
+
+		/* Does nothing in readonly. */
+		code_edit->undo();
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+		code_edit->update_code_completion_options();
+		code_edit->set_editable(false);
+		code_edit->confirm_code_completion();
+		code_edit->set_editable(true);
+		CHECK(code_edit->get_line(0) == "i");
+
+		/* Replace */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1 test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0 test");
+
+		/* Replace string. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("\"item_1 test\"");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "\"item_0\"");
+
+		/* Normal replace if no end is given. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("\"item_1 test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "\"item_0\" test");
+
+		/* Insert at completion. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1 test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_accept");
+		CHECK(code_edit->get_line(0) == "item_01 test");
+
+		/* Insert at completion with string should have same output. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("\"item_1 test\"");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_accept");
+		CHECK(code_edit->get_line(0) == "\"item_0\"1 test\"");
+
+		/* Merge symbol at end on insert text. */
+		/* End on completion entry. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1 test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0( test");
+		CHECK(code_edit->get_caret_column() == 7);
+
+		/* End of text*/
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1( test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0( test");
+		CHECK(code_edit->get_caret_column() == 6);
+
+		/* End of both. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1( test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0( test");
+		CHECK(code_edit->get_caret_column() == 7);
+
+		/* Full set. */
+		/* End on completion entry. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1 test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0() test");
+		CHECK(code_edit->get_caret_column() == 8);
+
+		/* End of text*/
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1() test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0() test");
+		CHECK(code_edit->get_caret_column() == 6);
+
+		/* End of both. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1() test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0() test");
+		CHECK(code_edit->get_caret_column() == 8);
+
+		/* Autobrace completion. */
+		code_edit->set_auto_brace_completion_enabled(true);
+
+		/* End on completion entry. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1 test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0() test");
+		CHECK(code_edit->get_caret_column() == 7);
+
+		/* End of text*/
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1( test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0( test");
+		CHECK(code_edit->get_caret_column() == 6);
+
+		/* End of both. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1( test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0(");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0( test");
+		CHECK(code_edit->get_caret_column() == 7);
+
+		/* Full set. */
+		/* End on completion entry. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1 test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0() test");
+		CHECK(code_edit->get_caret_column() == 8);
+
+		/* End of text*/
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1() test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0() test");
+		CHECK(code_edit->get_caret_column() == 6);
+
+		/* End of both. */
+		code_edit->clear();
+		code_edit->insert_text_at_caret("item_1() test");
+		code_edit->set_caret_column(2);
+		code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()");
+		code_edit->update_code_completion_options();
+		SEND_GUI_ACTION(code_edit, "ui_text_completion_replace");
+		CHECK(code_edit->get_line(0) == "item_0() test");
+		CHECK(code_edit->get_caret_column() == 8);
+	}
+
+	memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] symbol lookup") {
+	CodeEdit *code_edit = memnew(CodeEdit);
+	SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+	code_edit->set_symbol_lookup_on_click_enabled(true);
+	CHECK(code_edit->is_symbol_lookup_on_click_enabled());
+
+	/* Set size for mouse input. */
+	code_edit->set_size(Size2(100, 100));
+
+	code_edit->set_text("this is some text");
+
+	Point2 caret_pos = code_edit->get_caret_draw_pos();
+	caret_pos.x += 55;
+	SEND_GUI_MOUSE_EVENT(code_edit, caret_pos, MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE);
+	CHECK(code_edit->get_text_for_symbol_lookup() == "this is s" + String::chr(0xFFFF) + "ome text");
+
+	SIGNAL_WATCH(code_edit, "symbol_validate");
+
+#ifdef OSX_ENABLED
+	SEND_GUI_KEY_EVENT(code_edit, KEY_META);
+#else
+	SEND_GUI_KEY_EVENT(code_edit, KEY_CTRL);
+#endif
+
+	Array signal_args;
+	Array arg;
+	arg.push_back("some");
+	signal_args.push_back(arg);
+	SIGNAL_CHECK("symbol_validate", signal_args);
+
+	SIGNAL_UNWATCH(code_edit, "symbol_validate");
+
+	memdelete(code_edit);
+}
+
+TEST_CASE("[SceneTree][CodeEdit] line length guidelines") {
+	CodeEdit *code_edit = memnew(CodeEdit);
+	SceneTree::get_singleton()->get_root()->add_child(code_edit);
+
+	TypedArray<int> guide_lines;
+
+	code_edit->set_line_length_guidelines(guide_lines);
+	CHECK(code_edit->get_line_length_guidelines().size() == 0);
+
+	guide_lines.push_back(80);
+	guide_lines.push_back(120);
+
+	/* Order should be preserved. */
+	code_edit->set_line_length_guidelines(guide_lines);
+	CHECK((int)code_edit->get_line_length_guidelines()[0] == 80);
+	CHECK((int)code_edit->get_line_length_guidelines()[1] == 120);
+
+	memdelete(code_edit);
+}
+
 } // namespace TestCodeEdit
 
 #endif // TEST_CODE_EDIT_H

+ 36 - 1
tests/test_macros.h

@@ -131,8 +131,12 @@ int register_test_command(String p_command, TestFunc p_function);
 			register_test_command(m_command, m_function);               \
 	DOCTEST_GLOBAL_NO_WARNINGS_END()
 
-// Utility macro to send an action event to a given object
+// Utility macros to send an event actions to a given object
 // Requires Message Queue and InputMap to be setup.
+// SEND_GUI_ACTION    - takes an object and a input map key. e.g SEND_GUI_ACTION(code_edit, "ui_text_newline").
+// SEND_GUI_KEY_EVENT - takes an object and a keycode set.   e.g SEND_GUI_KEY_EVENT(code_edit, KEY_A | KEY_MASK_CMD).
+// SEND_GUI_MOUSE_EVENT - takes an object, position, mouse button and mouse mask e.g SEND_GUI_MOUSE_EVENT(code_edit, Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE);
+// SEND_GUI_DOUBLE_CLICK - takes an object and a postion. e.g SEND_GUI_DOUBLE_CLICK(code_edit, Vector2(50, 50));
 
 #define SEND_GUI_ACTION(m_object, m_action)                                                           \
 	{                                                                                                 \
@@ -144,6 +148,37 @@ int register_test_command(String p_command, TestFunc p_function);
 		MessageQueue::get_singleton()->flush();                                                       \
 	}
 
+#define SEND_GUI_KEY_EVENT(m_object, m_input)                                \
+	{                                                                        \
+		Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \
+		event->set_pressed(true);                                            \
+		m_object->gui_input(event);                                          \
+		MessageQueue::get_singleton()->flush();                              \
+	}
+
+#define _CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask) \
+	Ref<InputEventMouseButton> event;                                   \
+	event.instantiate();                                                \
+	event->set_position(m_local_pos);                                   \
+	event->set_button_index(m_input);                                   \
+	event->set_button_mask(m_mask);                                     \
+	event->set_pressed(true);
+
+#define SEND_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask)     \
+	{                                                                    \
+		_CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, m_input, m_mask); \
+		m_object->get_viewport()->push_input(event);                     \
+		MessageQueue::get_singleton()->flush();                          \
+	}
+
+#define SEND_GUI_DOUBLE_CLICK(m_object, m_local_pos)                                          \
+	{                                                                                         \
+		_CREATE_GUI_MOUSE_EVENT(m_object, m_local_pos, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_LEFT); \
+		event->set_double_click(true);                                                        \
+		m_object->get_viewport()->push_input(event);                                          \
+		MessageQueue::get_singleton()->flush();                                               \
+	}
+
 // Utility class / macros for testing signals
 //
 // Use SIGNAL_WATCH(*object, "signal_name") to start watching