Repository: akvelon/flutter-code-editor

Issue:
Hide service comments
Service comments like `//readonly` and `//[START section1]` should be hidden in the editor.



Fix patch:
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
new file mode 100644
index 00000000..22b4feb9
--- /dev/null
+++ b/example/analysis_options.yaml
@@ -0,0 +1,1 @@
+include: package:total_lints/app.yaml
diff --git a/example/lib/custom_code_box.dart b/example/lib/custom_code_box.dart
index 23bf1e83..5f97b507 100644
--- a/example/lib/custom_code_box.dart
+++ b/example/lib/custom_code_box.dart
@@ -1,12 +1,13 @@
+// ignore_for_file: implementation_imports
+
+import 'package:code_text_field/code_text_field.dart';
+import 'package:code_text_field/src/wip/autoRefactorService.dart';
 import 'package:flutter/material.dart';
 import 'package:highlight/languages/dart.dart';
 import 'package:highlight/languages/go.dart';
 import 'package:highlight/languages/java.dart';
 import 'package:highlight/languages/python.dart';
 import 'package:highlight/languages/scala.dart';
-import 'package:code_text_field/code_text_field.dart';
-
-import 'package:code_text_field/src/wip/autoRefactorService.dart';
 
 import 'themes.dart';
 
@@ -24,13 +25,13 @@ class CustomCodeBox extends StatefulWidget {
   final String theme;
 
   const CustomCodeBox({
-    Key? key,
+    super.key,
     required this.language,
     required this.theme,
-  }) : super(key: key);
+  });
 
   @override
-  _CustomCodeBoxState createState() => _CustomCodeBoxState();
+  State<CustomCodeBox> createState() => _CustomCodeBoxState();
 }
 
 class _CustomCodeBoxState extends State<CustomCodeBox> {
@@ -55,16 +56,20 @@ class _CustomCodeBoxState extends State<CustomCodeBox> {
   ];
 
   List<String?> themeList = <String>[
-    "monokai-sublime",
-    "a11y-dark",
-    "an-old-hope",
-    "vs2015",
-    "vs",
-    "atom-one-dark",
+    'monokai-sublime',
+    'a11y-dark',
+    'an-old-hope',
+    'vs2015',
+    'vs',
+    'atom-one-dark',
   ];
 
-  Widget buildDropdown(Iterable<String?> choices, String value, IconData icon,
-      Function(String?) onChanged) {
+  Widget buildDropdown(
+    Iterable<String?> choices,
+    String value,
+    IconData icon,
+    Function(String?) onChanged,
+  ) {
     return DropdownButton<String>(
       value: value,
       items: choices.map((String? value) {
@@ -94,11 +99,11 @@ class _CustomCodeBoxState extends State<CustomCodeBox> {
       setState(() => theme = val);
     });
     final resetButton = TextButton.icon(
-      icon: Icon(Icons.delete, color: Colors.white),
-      label: Text('Reset', style: TextStyle(color: Colors.white)),
+      icon: const Icon(Icons.delete, color: Colors.white),
+      label: const Text('Reset', style: TextStyle(color: Colors.white)),
       onPressed: () {
         setState(() {
-          reset = (!reset!);
+          reset = !reset!;
         });
       },
     );
@@ -108,29 +113,33 @@ class _CustomCodeBoxState extends State<CustomCodeBox> {
       color: Colors.deepPurple[900],
       child: Row(
         children: [
-          Spacer(flex: 2),
-          Text(
+          const Spacer(flex: 2),
+          const Text(
             'Code editor',
             style: TextStyle(fontSize: 28, color: Colors.white),
           ),
-          Spacer(flex: 35),
+          const Spacer(flex: 35),
           codeDropdown,
-          Spacer(),
+          const Spacer(),
           themeDropdown,
-          Spacer(),
+          const Spacer(),
           resetButton,
         ],
       ),
     );
+
     final codeField = InnerField(
-      key: ValueKey("$language - $theme - $reset"),
+      key: ValueKey('$language - $theme - $reset'),
       language: language!,
       theme: theme!,
     );
-    return Column(children: [
-      buttons,
-      codeField,
-    ]);
+
+    return Column(
+      children: [
+        buttons,
+        codeField,
+      ],
+    );
   }
 }
 
@@ -139,13 +148,13 @@ class InnerField extends StatefulWidget {
   final String theme;
 
   const InnerField({
-    Key? key,
+    super.key,
     required this.language,
     required this.theme,
-  }) : super(key: key);
+  });
 
   @override
-  _InnerFieldState createState() => _InnerFieldState();
+  State<InnerField> createState() => _InnerFieldState();
 }
 
 class _InnerFieldState extends State<InnerField> {
@@ -156,7 +165,17 @@ class _InnerFieldState extends State<InnerField> {
     super.initState();
     _codeController = CodeController(
       language: allLanguages[widget.language],
-      theme: THEMES[widget.theme],
+      text: '''
+class MyClass {
+  void readOnlyMethod() {// [START section1]
+  }// [END section1]
+  // [START section2]
+  void method() {
+  }// [END section2]
+}
+''',
+      namedSectionParser: const BracketsStartEndNamedSectionParser(),
+      readOnlySectionNames: {'section1', 'nonexistent'},
     );
   }
 
@@ -168,31 +187,36 @@ class _InnerFieldState extends State<InnerField> {
 
   @override
   Widget build(BuildContext context) {
+    final theme = themes[widget.theme];
+
     return Container(
-      color: _codeController!.theme!['root']!.backgroundColor,
+      color: theme?['root']?.backgroundColor,
       height: MediaQuery.of(context).size.height / 13 * 12,
-      child: Stack(
-        children: [
-          SingleChildScrollView(
-            child: CodeField(
-              controller: _codeController!,
-              textStyle: const TextStyle(fontFamily: 'SourceCode'),
+      child: CodeTheme(
+        data: CodeThemeData(styles: theme),
+        child: Stack(
+          children: [
+            SingleChildScrollView(
+              child: CodeField(
+                controller: _codeController!,
+                textStyle: const TextStyle(fontFamily: 'SourceCode'),
+              ),
             ),
-          ),
-          Align(
-            alignment: Alignment.topRight,
-            child: FloatingActionButton(
-              child: const Icon(Icons.format_align_left_outlined),
-              backgroundColor: Colors.indigo[800],
-              onPressed: () {
-                setState(() {
-                  _codeController!.text =
-                      autoRefactor(_codeController!.text, widget.language);
-                });
-              },
+            Align(
+              alignment: Alignment.topRight,
+              child: FloatingActionButton(
+                backgroundColor: Colors.indigo[800],
+                onPressed: () {
+                  setState(() {
+                    _codeController!.text =
+                        autoRefactor(_codeController!.text, widget.language);
+                  });
+                },
+                child: const Icon(Icons.format_align_left_outlined),
+              ),
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 7b2b1f3c..b2bc5715 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,4 +1,3 @@
-//File only for testing. Later file will be deleted.
 import 'package:flutter/material.dart';
 
 import 'custom_code_box.dart';
@@ -8,7 +7,7 @@ void main() {
 }
 
 class CodeEditor extends StatelessWidget {
-  const CodeEditor({Key? key}) : super(key: key);
+  const CodeEditor({super.key});
 
   @override
   Widget build(BuildContext context) {
diff --git a/example/lib/themes.dart b/example/lib/themes.dart
index 1585b6d1..8b4380dc 100644
--- a/example/lib/themes.dart
+++ b/example/lib/themes.dart
@@ -89,7 +89,7 @@ import 'package:flutter_highlight/themes/xcode.dart';
 import 'package:flutter_highlight/themes/xt256.dart';
 import 'package:flutter_highlight/themes/zenburn.dart';
 
-const THEMES = {
+const themes = {
   'a11y-dark': a11yDarkTheme,
   'a11y-light': a11yLightTheme,
   'agate': agateTheme,
diff --git a/example/linux/.gitignore b/example/linux/.gitignore
new file mode 100644
index 00000000..d3896c98
--- /dev/null
+++ b/example/linux/.gitignore
@@ -0,0 +1,1 @@
+flutter/ephemeral
diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt
new file mode 100644
index 00000000..74c66dd4
--- /dev/null
+++ b/example/linux/CMakeLists.txt
@@ -0,0 +1,138 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.10)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "example")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.example.example")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+  set(CMAKE_BUILD_TYPE "Debug" CACHE
+    STRING "Flutter build mode" FORCE)
+  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+    "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+  target_compile_features(${TARGET} PUBLIC cxx_std_14)
+  target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+  target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
+  target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Define the application target. To change its name, change BINARY_NAME above,
+# not the value here, or `flutter run` will no longer work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+  "main.cc"
+  "my_application.cc"
+  "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+  PROPERTIES
+  RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+  set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+  file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+  " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+  COMPONENT Runtime)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+  install(FILES "${bundled_library}"
+    DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endforeach(bundled_library)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+  file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+  " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+  DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+  install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endif()
diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt
new file mode 100644
index 00000000..d5bd0164
--- /dev/null
+++ b/example/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+    set(NEW_LIST "")
+    foreach(element ${${LIST_NAME}})
+        list(APPEND NEW_LIST "${PREFIX}${element}")
+    endforeach(element)
+    set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+  "fl_basic_message_channel.h"
+  "fl_binary_codec.h"
+  "fl_binary_messenger.h"
+  "fl_dart_project.h"
+  "fl_engine.h"
+  "fl_json_message_codec.h"
+  "fl_json_method_codec.h"
+  "fl_message_codec.h"
+  "fl_method_call.h"
+  "fl_method_channel.h"
+  "fl_method_codec.h"
+  "fl_method_response.h"
+  "fl_plugin_registrar.h"
+  "fl_plugin_registry.h"
+  "fl_standard_message_codec.h"
+  "fl_standard_method_codec.h"
+  "fl_string_codec.h"
+  "fl_value.h"
+  "fl_view.h"
+  "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+  "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+  PkgConfig::GTK
+  PkgConfig::GLIB
+  PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+    ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+  COMMAND ${CMAKE_COMMAND} -E env
+    ${FLUTTER_TOOL_ENVIRONMENT}
+    "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+  VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+  "${FLUTTER_LIBRARY}"
+  ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 00000000..e71a16d2
--- /dev/null
+++ b/example/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+}
diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 00000000..e0f0a47b
--- /dev/null
+++ b/example/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+//  Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include <flutter_linux/flutter_linux.h>
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif  // GENERATED_PLUGIN_REGISTRANT_
diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake
new file mode 100644
index 00000000..2e1de87a
--- /dev/null
+++ b/example/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,23 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/example/linux/main.cc b/example/linux/main.cc
new file mode 100644
index 00000000..e7c5c543
--- /dev/null
+++ b/example/linux/main.cc
@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+  g_autoptr(MyApplication) app = my_application_new();
+  return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc
new file mode 100644
index 00000000..0ba8f430
--- /dev/null
+++ b/example/linux/my_application.cc
@@ -0,0 +1,104 @@
+#include "my_application.h"
+
+#include <flutter_linux/flutter_linux.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+  GtkApplication parent_instance;
+  char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+  MyApplication* self = MY_APPLICATION(application);
+  GtkWindow* window =
+      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+  // Use a header bar when running in GNOME as this is the common style used
+  // by applications and is the setup most users will be using (e.g. Ubuntu
+  // desktop).
+  // If running on X and not using GNOME then just use a traditional title bar
+  // in case the window manager does more exotic layout, e.g. tiling.
+  // If running on Wayland assume the header bar will work (may need changing
+  // if future cases occur).
+  gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+  GdkScreen* screen = gtk_window_get_screen(window);
+  if (GDK_IS_X11_SCREEN(screen)) {
+    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+    if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+      use_header_bar = FALSE;
+    }
+  }
+#endif
+  if (use_header_bar) {
+    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+    gtk_widget_show(GTK_WIDGET(header_bar));
+    gtk_header_bar_set_title(header_bar, "example");
+    gtk_header_bar_set_show_close_button(header_bar, TRUE);
+    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+  } else {
+    gtk_window_set_title(window, "example");
+  }
+
+  gtk_window_set_default_size(window, 1280, 720);
+  gtk_widget_show(GTK_WIDGET(window));
+
+  g_autoptr(FlDartProject) project = fl_dart_project_new();
+  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
+
+  FlView* view = fl_view_new(project);
+  gtk_widget_show(GTK_WIDGET(view));
+  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+  fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+  gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
+  MyApplication* self = MY_APPLICATION(application);
+  // Strip out the first argument as it is the binary name.
+  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+  g_autoptr(GError) error = nullptr;
+  if (!g_application_register(application, nullptr, &error)) {
+     g_warning("Failed to register: %s", error->message);
+     *exit_status = 1;
+     return TRUE;
+  }
+
+  g_application_activate(application);
+  *exit_status = 0;
+
+  return TRUE;
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+  MyApplication* self = MY_APPLICATION(object);
+  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+  G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
+  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+  return MY_APPLICATION(g_object_new(my_application_get_type(),
+                                     "application-id", APPLICATION_ID,
+                                     "flags", G_APPLICATION_NON_UNIQUE,
+                                     nullptr));
+}
diff --git a/example/linux/my_application.h b/example/linux/my_application.h
new file mode 100644
index 00000000..72271d5e
--- /dev/null
+++ b/example/linux/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include <gtk/gtk.h>
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+                     GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif  // FLUTTER_MY_APPLICATION_H_
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 2350849e..b14d5058 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,23 +1,21 @@
 name: example
 description: Example
-publish_to: "none" # Remove this line if you wish to publish to pub.dev
+publish_to: 'none'
 version: 1.0.0+1
 
 environment:
-  sdk: ">=2.12.0 <3.0.0"
+  sdk: '>=2.17.0 <3.0.0'
+  flutter: '>=3.0.0'
 
 dependencies:
-  flutter:
-    sdk: flutter
-  cupertino_icons: ^1.0.3
-  font_awesome_flutter: ^9.0.0
-  url_launcher: ^6.0.4
-  
+  code_text_field: { path: ../ }
+  flutter: { sdk: flutter }
+  flutter_highlight: ^0.7.0
+  highlight: ^0.7.0
+
 dev_dependencies:
-  flutter_test:
-    sdk: flutter
-  code_text_field:
-    path: ../
+  flutter_test: { sdk: flutter }
+  total_lints: ^2.17.4
 
 flutter:
   uses-material-design: true
diff --git a/lib/src/code/code.dart b/lib/src/code/code.dart
index 3fbb3915..3fe3ff4e 100644
--- a/lib/src/code/code.dart
+++ b/lib/src/code/code.dart
@@ -1,20 +1,30 @@
+import 'dart:math';
+
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:highlight/highlight_core.dart';
 
+import '../hidden_ranges/hidden_range.dart';
+import '../hidden_ranges/hidden_ranges.dart';
 import '../named_sections/named_section.dart';
 import '../named_sections/parsers/abstract.dart';
 import '../single_line_comments/parser/single_line_comment_parser.dart';
 import '../single_line_comments/parser/single_line_comments.dart';
 import '../single_line_comments/single_line_comment.dart';
+import 'code_edit_result.dart';
 import 'code_line.dart';
+import 'string.dart';
 import 'text_range.dart';
 import 'tokens.dart';
 
 class Code {
   final String text;
+  final HiddenRanges hiddenRanges;
+  final Result? highlighted;
   final List<CodeLine> lines;
   final Map<String, NamedSection> namedSections;
+  final Result? visibleHighlighted;
+  final String visibleText;
 
   factory Code({
     required String text,
@@ -31,6 +41,11 @@ class Code {
       singleLineCommentSequences: sequences,
     );
 
+    final serviceComments = _filterServiceComments(
+      commentParser.comments,
+      namedSectionParser: namedSectionParser,
+    );
+
     final lines = _textToCodeLines(
       text: text,
       highlighted: highlighted,
@@ -40,7 +55,6 @@ class Code {
 
     final sections = namedSectionParser?.parse(
           singleLineComments: commentParser.comments,
-          lineCount: lines.length,
         ) ??
         const [];
     final sectionsMap = {for (final s in sections) s.name: s};
@@ -51,25 +65,75 @@ class Code {
       readOnlySectionNames: readOnlySectionNames,
     );
 
+    final hiddenRanges = HiddenRanges(
+      ranges: _commentsToHiddenRanges(serviceComments),
+      textLength: text.length,
+    );
+
     return Code._(
       text: text,
+      hiddenRanges: hiddenRanges,
+      highlighted: highlighted,
       lines: lines,
       namedSections: sectionsMap,
+      visibleHighlighted:
+          highlighted == null ? null : hiddenRanges.cutHighlighted(highlighted),
+      visibleText: hiddenRanges.cutString(text, start: 0),
     );
   }
 
   const Code._({
     required this.text,
+    required this.hiddenRanges,
+    required this.highlighted,
     required this.lines,
     required this.namedSections,
+    required this.visibleHighlighted,
+    required this.visibleText,
   });
 
   static const empty = Code._(
     text: '',
+    hiddenRanges: HiddenRanges.empty,
+    highlighted: null,
     lines: [],
     namedSections: {},
+    visibleHighlighted: null,
+    visibleText: '',
   );
 
+  static Iterable<SingleLineComment> _filterServiceComments(
+    List<SingleLineComment> comments, {
+    required AbstractNamedSectionParser? namedSectionParser,
+  }) sync* {
+    for (final comment in comments) {
+      if (_isServiceComment(comment, namedSectionParser: namedSectionParser)) {
+        yield comment;
+      }
+    }
+  }
+
+  static bool _isServiceComment(
+    SingleLineComment comment, {
+    required AbstractNamedSectionParser? namedSectionParser,
+  }) {
+    final words = _getCommentWords(comment.innerContent);
+    if (words.contains(Tokens.readonly)) {
+      return true;
+    }
+
+    if (namedSectionParser != null) {
+      final namedSections = namedSectionParser.parseUnsorted(
+        singleLineComments: [comment],
+      );
+      if (namedSections.isNotEmpty) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
   static List<CodeLine> _textToCodeLines({
     required String text,
     required Result? highlighted,
@@ -135,12 +199,24 @@ class Code {
         continue;
       }
 
-      for (int i = section.startLine; i <= section.endLine; i++) {
+      final lastLineIndex = section.endLine ?? lines.length - 1;
+
+      for (int i = section.startLine; i <= lastLineIndex; i++) {
         lines[i] = lines[i].copyWith(isReadOnly: true);
       }
     }
   }
 
+  static List<HiddenRange> _commentsToHiddenRanges(
+    Iterable<SingleLineComment> comments,
+  ) {
+    return comments
+        .map(
+          (c) => HiddenRange.fromStartAndText(c.characterIndex, c.outerContent),
+        )
+        .toList(growable: false);
+  }
+
   /// Returns the 0-based line number of the character at [characterIndex].
   ///
   /// [characterIndex] can be from 0 to length inclusive.
@@ -193,13 +269,24 @@ class Code {
 
   /// Returns whether the current selection has any read-only part.
   bool isReadOnlySelected(TextRange range) {
+    if (range.start == -1 && range.end == -1) {
+      return false; // Empty selection.
+    }
+
     final startChar = range.normalized.start;
     final endChar = range.normalized.end;
 
-    final startLine = characterIndexToLineIndex(startChar);
-    final endLine = characterIndexToLineIndex(endChar);
+    return isReadOnlyInLineRange(
+      TextRange(
+        start: characterIndexToLineIndex(startChar),
+        end: characterIndexToLineIndex(endChar),
+      ),
+    );
+  }
 
-    for (int line = startLine; line <= endLine; line++) {
+  /// Returns whether any of the lines of this range is read-only.
+  bool isReadOnlyInLineRange(TextRange lineRange) {
+    for (int line = lineRange.start; line <= lineRange.end; line++) {
       if (lines[line].isReadOnly) {
         return true;
       }
@@ -207,4 +294,79 @@ class Code {
 
     return false;
   }
+
+  CodeEditResult getEditResult(TextEditingValue visibleAfter) {
+    final visibleRangeAfter = visibleAfter.text.getChangedRange(
+      visibleText,
+      attributeChangeTo: TextAffinity.upstream,
+    );
+
+    if (visibleRangeAfter.start == -1 && visibleRangeAfter.end == -1) {
+      return CodeEditResult(
+        fullTextAfter: text,
+        linesChanged: TextRange.empty,
+      );
+    }
+
+    // Recover what exactly was the full old text that was replaced
+    // with the new one. For this, inspect the start and end points
+    // of the changed part in the old visibleText.
+    // If any hidden ranges are collapsed at those points,
+    // the trick is to correctly replace them or to correctly preserve them.
+    final rangeBefore = TextRange(
+      // `end` is responsible for deciding between these two cases when the user
+      // entered some text at the point with a hidden range:
+      //     text we are adding at the beginning of this line// [START section1]
+      //     // [START section1]text we are adding at the beginning of this line
+      // We do not want to add that text to the comment and its hidden range.
+      // So we must place all hidden ranges collapsed at the end of the diff
+      // *after* that diff:
+      end: hiddenRanges.recoverPosition(
+        visibleText.length - visibleAfter.text.length + visibleRangeAfter.end,
+        placeHiddenRanges: TextAffinity.downstream,
+      ),
+
+      // If we are inserting (and the old changed part is empty),
+      // then using the same `placeHiddenRanges` is straightforward.
+      //
+      // However, this is also the only option for a non-empty old changed range
+      // with a hidden range collapsed at its start. Otherwise we would
+      // append to that comment, and the new text will not be visible.
+      // Using `TextAffinity.downstream` effectively deletes any such hidden
+      // range because it falls into the range of replacement.
+      // We may want to reconsider this if we support terminatable hidden range
+      // comments like /* ... */ that do not acquire all text to the
+      // end of the string.
+      start: hiddenRanges.recoverPosition(
+        visibleRangeAfter.start,
+        placeHiddenRanges: TextAffinity.downstream,
+      ),
+    );
+
+    final fullTextAfter = rangeBefore.textBefore(text) +
+        visibleRangeAfter.textInside(visibleAfter.text) +
+        rangeBefore.textAfter(text);
+
+    // The line at [start] has changed for sure.
+    // The line at [end - 1] has changed if [end > start].
+    // Additionally, the line at [end] has changed if two strings were glued:
+    //  - (1) The last old char was '\n' AND
+    //  - (2) The char before [start] is not '\n'.
+    // We don't need to check (1) because otherwise [end] and [end - 1]
+    // are on the same line.
+    final lastChar = rangeBefore.end -
+        ((rangeBefore.start == 0 || text[rangeBefore.start - 1] == '\n')
+            ? 1
+            : 0);
+
+    final linesChanged = TextRange(
+      start: characterIndexToLineIndex(rangeBefore.start),
+      end: characterIndexToLineIndex(max(lastChar, rangeBefore.start)),
+    );
+
+    return CodeEditResult(
+      fullTextAfter: fullTextAfter,
+      linesChanged: linesChanged,
+    );
+  }
 }
diff --git a/lib/src/code/code_edit_result.dart b/lib/src/code/code_edit_result.dart
new file mode 100644
index 00000000..9490156d
--- /dev/null
+++ b/lib/src/code/code_edit_result.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/widgets.dart';
+
+@immutable
+class CodeEditResult {
+  final String fullTextAfter;
+  final TextRange linesChanged;
+
+  const CodeEditResult({
+    required this.fullTextAfter,
+    required this.linesChanged,
+  });
+
+  @override
+  String toString() {
+    final buffer = StringBuffer();
+
+    buffer.write('fullTextAfter: $fullTextAfter, ');
+    buffer.write('linesChanged: $linesChanged');
+
+    return buffer.toString();
+  }
+
+  @override
+  int get hashCode => Object.hash(
+        fullTextAfter.hashCode,
+        linesChanged.hashCode,
+      );
+
+  @override
+  bool operator ==(Object other) {
+    return other is CodeEditResult &&
+        fullTextAfter == other.fullTextAfter &&
+        linesChanged == other.linesChanged;
+  }
+}
diff --git a/lib/src/code/string.dart b/lib/src/code/string.dart
new file mode 100644
index 00000000..713f7f2a
--- /dev/null
+++ b/lib/src/code/string.dart
@@ -0,0 +1,72 @@
+import 'dart:math';
+import 'dart:ui';
+
+extension MyString on String {
+  /// Returns the widest [TextRange] of this that is different from [old].
+  ///
+  /// `start` refers to the common prefix. It is the first character of this
+  /// that is different from the corresponding character of [old].
+  ///
+  /// `end` refers to the common suffix. It is the first character of this
+  /// after the diff that matches the corresponding character of [old].
+  ///
+  /// `null` is returned for equal strings.
+  ///
+  /// [attributeChangeTo] affects where the change is attributed in ambiguous
+  /// cases:
+  ///  - Inserting a duplicate: aBc -> aBBc
+  ///  - Deletion of a duplicate: aBBc -> aBc
+  TextRange getChangedRange(
+    String old, {
+    required TextAffinity attributeChangeTo,
+  }) {
+    if (this == old) {
+      return TextRange.empty;
+    }
+
+    switch (attributeChangeTo) {
+      case TextAffinity.upstream:
+        return _getChangedRangeAttributeChangeUpstream(old);
+      case TextAffinity.downstream:
+        return _getChangedRangeAttributeChangeDownstream(old);
+    }
+  }
+
+  TextRange _getChangedRangeAttributeChangeUpstream(String old) {
+    final minLast = max(0, length - old.length) - 1;
+    int start = 0;
+    int last = length - 1;
+    int oldLast = old.length - 1;
+
+    while (last > minLast && this[last] == old[oldLast]) {
+      last--;
+      oldLast--;
+    }
+
+    final maxStart = min(last, oldLast) + 1;
+    while (start < maxStart && this[start] == old[start]) {
+      start++;
+    }
+
+    return TextRange(start: start, end: last + 1);
+  }
+
+  TextRange _getChangedRangeAttributeChangeDownstream(String old) {
+    final minLength = min(length, old.length);
+    int start = 0;
+    int last = length - 1;
+    int oldLast = old.length - 1;
+
+    while (start < minLength && this[start] == old[start]) {
+      start++;
+    }
+
+    final minLast = start + max(0, length - old.length) - 1;
+    while (last > minLast && this[last] == old[oldLast]) {
+      last--;
+      oldLast--;
+    }
+
+    return TextRange(start: start, end: last + 1);
+  }
+}
diff --git a/lib/src/code/text_range.dart b/lib/src/code/text_range.dart
index e8330d71..69c769ea 100644
--- a/lib/src/code/text_range.dart
+++ b/lib/src/code/text_range.dart
@@ -1,7 +1,32 @@
 import 'dart:ui';
 
-extension Normalized on TextRange {
+extension MyTextRange on TextRange {
   TextRange get normalized {
     return isNormalized ? this : TextRange(start: end, end: start);
   }
+
+  bool isAfter(TextRange other) => start >= other.end;
+}
+
+class NormalizedTextRange extends TextRange {
+  const NormalizedTextRange({required super.start, required super.end})
+      : assert(
+          end >= start,
+          'End should be >= start, given end = $end, start = $start',
+        );
+
+  /// Returns 1 if the [position] is before the range,
+  /// -1 if the [position] is after the range,
+  /// and zero if the [position] is within the range.
+  int compareToPosition(int position) {
+    if (position < start) {
+      return 1;
+    }
+
+    if (position >= end) {
+      return -1;
+    }
+
+    return 0;
+  }
 }
diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart
index 9a4f77a7..bba1bc33 100644
--- a/lib/src/code_field/code_controller.dart
+++ b/lib/src/code_field/code_controller.dart
@@ -6,7 +6,8 @@ import 'package:flutter/services.dart';
 import 'package:highlight/highlight_core.dart';
 
 import '../code/code.dart';
-import '../code/text_range.dart';
+import '../code/code_edit_result.dart';
+import '../code_field/text_editing_value.dart';
 import '../code_modifiers/close_block_code_modifier.dart';
 import '../code_modifiers/code_modifier.dart';
 import '../code_modifiers/indent_code_modifier.dart';
@@ -18,6 +19,7 @@ import '../wip/autocomplete/popup_controller.dart';
 import '../wip/autocomplete/suggestion.dart';
 import '../wip/autocomplete/suggestion_generator.dart';
 import 'editor_params.dart';
+import 'span_builder.dart';
 
 const _middleDot = '·';
 
@@ -115,10 +117,10 @@ class CodeController extends TextEditingController {
     this.onChange,
   })  : _theme = theme,
         _readOnlySectionNames = readOnlySectionNames,
-        _lastCode = Code.empty,
-        super(text: text) {
+        _lastCode = Code.empty {
     this.language = language;
     _updateLastCode(text ?? '');
+    fullText = text ?? '';
 
     // Create modifier map
     for (final el in modifiers) {
@@ -191,10 +193,6 @@ class CodeController extends TextEditingController {
   }
 
   KeyEventResult _onKeyDownRepeat(KeyEvent event) {
-    if (!_shouldAllowKeyDownRepeat(event)) {
-      return KeyEventResult.handled; // Prevent the framework from handling.
-    }
-
     // TODO(alexeyinkin): Use a shortcut, https://github.com/akvelon/flutter-code-editor/issues/21
     if (event.logicalKey == LogicalKeyboardKey.tab) {
       text = text.replaceRange(selection.start, selection.end, '\t');
@@ -219,39 +217,6 @@ class CodeController extends TextEditingController {
     return KeyEventResult.ignored; // The framework will handle.
   }
 
-  bool _shouldAllowKeyDownRepeat(KeyEvent event) {
-    if (event.logicalKey == LogicalKeyboardKey.backspace) {
-      return _shouldAllowBackspace();
-    }
-
-    if (event.logicalKey == LogicalKeyboardKey.delete) {
-      return _shouldAllowDelete();
-    }
-
-    return true;
-  }
-
-  bool _shouldAllowBackspace() {
-    final start = super.value.selection.normalized.start - 1;
-    if (start < 0) {
-      return false;
-    }
-
-    final line = _lastCode.characterIndexToLineIndex(start);
-    return !_lastCode.lines[line].isReadOnly;
-  }
-
-  bool _shouldAllowDelete() {
-    // If we are at the end of the line, see after the \n.
-    final end = value.selection.normalized.end + 1;
-    if (end >= value.text.length) {
-      return false;
-    }
-
-    final line = _lastCode.characterIndexToLineIndex(end);
-    return !_lastCode.lines[line].isReadOnly;
-  }
-
   /// Inserts the word selected from the list of completions
   void insertSelectedWord() {
     final previousSelection = selection;
@@ -286,6 +251,13 @@ class CodeController extends TextEditingController {
     return _middleDotsToSpaces(super.text);
   }
 
+  String get fullText => _lastCode.text;
+
+  set fullText(String fullText) {
+    _updateLastCodeIfChanged(fullText);
+    super.value = TextEditingValue(text: _lastCode.visibleText);
+  }
+
   // Private methods
   bool get _webSpaceFix => kIsWeb && webSpaceFix;
 
@@ -314,11 +286,22 @@ class CodeController extends TextEditingController {
   @override
   set value(TextEditingValue newValue) {
     if (newValue.text != super.value.text) {
-      if (_lastCode.isReadOnlySelected(super.value.selection)) {
+      final editResult = _getEditResultNotBreakingReadOnly(newValue);
+
+      if (editResult == null) {
         return;
       }
 
-      _updateLastCode(newValue.text);
+      _updateLastCodeIfChanged(editResult.fullTextAfter);
+
+      if (newValue.text != _lastCode.text) {
+        // Manually typed in a text that has become a hidden range.
+        newValue = newValue.replacedText(_lastCode.visibleText);
+      }
+
+      // Uncomment this to see the hidden text in the console
+      // as you change the visible text.
+      //print('\n\n${_lastCode.text');
     }
 
     final loc = _insertedLoc(text, newValue.text);
@@ -358,11 +341,28 @@ class CodeController extends TextEditingController {
     }
   }
 
+  CodeEditResult? _getEditResultNotBreakingReadOnly(TextEditingValue newValue) {
+    final editResult = _lastCode.getEditResult(newValue);
+    if (!_lastCode.isReadOnlyInLineRange(editResult.linesChanged)) {
+      return editResult;
+    }
+
+    return null;
+  }
+
+  void _updateLastCodeIfChanged(String text) {
+    if (text != _lastCode.text) {
+      _updateLastCode(text);
+    }
+  }
+
   void _updateLastCode(String text) {
+    final rawText = _webSpaceFix ? _middleDotsToSpaces(text) : text;
+
     _lastCode = Code(
-      text: text,
+      text: rawText,
       language: language,
-      highlighted: highlight.parse(text, language: _languageId),
+      highlighted: highlight.parse(rawText, language: _languageId),
       namedSectionParser: namedSectionParser,
       readOnlySectionNames: _readOnlySectionNames,
     );
@@ -498,7 +498,12 @@ class CodeController extends TextEditingController {
 
     // Return parsing
     if (_language != null) {
-      return _processLanguage(text, CodeTheme.of(context), style);
+      return const SpanBuilder().build(
+        code: _lastCode,
+        theme: _getTheme(context),
+        textStyle: style,
+      );
+      //return _processLanguage(text, CodeTheme.of(context), style);
     }
 
     if (styleRegExp != null) {
@@ -507,4 +512,8 @@ class CodeController extends TextEditingController {
 
     return TextSpan(text: text, style: style);
   }
+
+  CodeThemeData _getTheme(BuildContext context) {
+    return CodeTheme.of(context) ?? CodeThemeData(styles: _theme ?? {});
+  }
 }
diff --git a/lib/src/code_field/span_builder.dart b/lib/src/code_field/span_builder.dart
new file mode 100644
index 00000000..a7b56da8
--- /dev/null
+++ b/lib/src/code_field/span_builder.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/widgets.dart';
+import 'package:highlight/highlight_core.dart';
+
+import '../code/code.dart';
+import '../code_theme/code_theme_data.dart';
+
+class SpanBuilder {
+  const SpanBuilder();
+
+  TextSpan build({
+    required Code code,
+    required CodeThemeData? theme,
+    TextStyle? textStyle,
+  }) {
+    return TextSpan(
+      style: textStyle,
+      children: _buildList(
+        nodes: code.visibleHighlighted?.nodes ?? [],
+        theme: theme,
+      ),
+    );
+  }
+
+  List<TextSpan>? _buildList({
+    required List<Node>? nodes,
+    required CodeThemeData? theme,
+  }) {
+    if (nodes == null) {
+      return null;
+    }
+
+    return nodes
+        .map((node) => _buildNode(node: node, theme: theme))
+        .toList(growable: false);
+  }
+
+  TextSpan _buildNode({
+    required Node node,
+    required CodeThemeData? theme,
+  }) {
+    return TextSpan(
+      text: node.value,
+      children: _buildList(nodes: node.children, theme: theme),
+      style: theme?.styles[node.className],
+    );
+  }
+}
diff --git a/lib/src/code_field/text_editing_value.dart b/lib/src/code_field/text_editing_value.dart
index 0c1377a9..78c43ffc 100644
--- a/lib/src/code_field/text_editing_value.dart
+++ b/lib/src/code_field/text_editing_value.dart
@@ -1,7 +1,25 @@
 import 'package:flutter/widgets.dart';
 
+import '../code/string.dart';
+
 extension ReplacedSelection on TextEditingValue {
   TextEditingValue replacedSelection(String value) {
     return replaced(selection, value);
   }
+
+  TextEditingValue replacedText(String newText) {
+    if (newText == text) {
+      return this;
+    }
+
+    final rangeAfter = newText.getChangedRange(
+      text,
+      attributeChangeTo: TextAffinity.upstream,
+    );
+
+    return TextEditingValue(
+      text: newText,
+      selection: TextSelection.collapsed(offset: rangeAfter.start),
+    );
+  }
 }
diff --git a/lib/src/code_theme/code_theme_data.dart b/lib/src/code_theme/code_theme_data.dart
index 9b518422..33e7cf77 100644
--- a/lib/src/code_theme/code_theme_data.dart
+++ b/lib/src/code_theme/code_theme_data.dart
@@ -4,9 +4,49 @@ import 'package:flutter/widgets.dart';
 class CodeThemeData {
   final Map<String, TextStyle> styles;
 
-  const CodeThemeData({
-    required this.styles,
-  });
+  CodeThemeData({
+    Map<String, TextStyle>? styles,
+    TextStyle? classStyle,
+    TextStyle? commentStyle,
+    TextStyle? functionStyle,
+    TextStyle? keywordStyle,
+    TextStyle? paramsStyle,
+    TextStyle? quoteStyle,
+    TextStyle? titleStyle,
+    TextStyle? variableStyle,
+  }) : styles = styles ?? {} {
+    if (classStyle != null) {
+      this.styles['class'] = classStyle;
+    }
+
+    if (commentStyle != null) {
+      this.styles['comment'] = commentStyle;
+    }
+
+    if (functionStyle != null) {
+      this.styles['function'] = functionStyle;
+    }
+
+    if (keywordStyle != null) {
+      this.styles['keyword'] = keywordStyle;
+    }
+
+    if (paramsStyle != null) {
+      this.styles['params'] = paramsStyle;
+    }
+
+    if (quoteStyle != null) {
+      this.styles['quote'] = quoteStyle;
+    }
+
+    if (titleStyle != null) {
+      this.styles['title'] = titleStyle;
+    }
+
+    if (variableStyle != null) {
+      this.styles['variable'] = variableStyle;
+    }
+  }
 
   @override
   int get hashCode => styles.hashCode;
diff --git a/lib/src/hidden_ranges/hidden_range.dart b/lib/src/hidden_ranges/hidden_range.dart
new file mode 100644
index 00000000..175d6551
--- /dev/null
+++ b/lib/src/hidden_ranges/hidden_range.dart
@@ -0,0 +1,35 @@
+import 'package:meta/meta.dart';
+
+import '../code/text_range.dart';
+
+@immutable
+class HiddenRange extends NormalizedTextRange {
+  final String text;
+
+  const HiddenRange({
+    required super.start,
+    required super.end,
+    required this.text,
+  })  : assert(start >= 0, 'Start should be >= 0, $start given'),
+        assert(
+          text.length == end - start,
+          'Length of $text is not equal to $end - $start',
+        );
+
+  const HiddenRange.fromStartAndText(int start, this.text)
+      : super(
+          start: start,
+          end: start + text.length,
+        );
+
+  @override
+  int get hashCode => Object.hash(
+        super.hashCode,
+        text,
+      );
+
+  @override
+  bool operator ==(Object other) {
+    return super == other && other is HiddenRange && text == other.text;
+  }
+}
diff --git a/lib/src/hidden_ranges/hidden_ranges.dart b/lib/src/hidden_ranges/hidden_ranges.dart
new file mode 100644
index 00000000..86430d9f
--- /dev/null
+++ b/lib/src/hidden_ranges/hidden_ranges.dart
@@ -0,0 +1,266 @@
+import 'dart:math';
+
+import 'package:collection/collection.dart';
+import 'package:flutter/widgets.dart';
+import 'package:highlight/highlight_core.dart';
+
+import '../code/text_range.dart';
+import 'hidden_range.dart';
+
+@immutable
+class HiddenRanges {
+  /// How many hidden characters are there before this range.
+  /// Has the length of [ranges] + 1. The last element shows how many
+  /// hidden characters are there in total.
+  final List<int> hiddenCharactersBeforeRanges;
+
+  final List<HiddenRange> ranges;
+  final int textLength;
+
+  factory HiddenRanges({
+    required List<HiddenRange> ranges,
+    required int textLength,
+  }) {
+    assert(
+      _areSortedAndNotOverlapping(ranges),
+      'Texts must be sorted and not overlap',
+    );
+
+    final nonEmptyRanges =
+        ranges.where((r) => r.text != '').toList(growable: false);
+
+    return HiddenRanges._(
+      hiddenCharactersBeforeRanges: _getHiddenCharactersBeforeRanges(
+        nonEmptyRanges,
+      ),
+      ranges: nonEmptyRanges,
+      textLength: textLength,
+    );
+  }
+
+  const HiddenRanges._({
+    required this.hiddenCharactersBeforeRanges,
+    required this.ranges,
+    required this.textLength,
+  });
+
+  static const empty = HiddenRanges._(
+    hiddenCharactersBeforeRanges: [0],
+    ranges: [],
+    textLength: 0,
+  );
+
+  static bool _areSortedAndNotOverlapping(List<HiddenRange> texts) {
+    for (int i = texts.length; --i >= 1;) {
+      if (!texts[i].isAfter(texts[i - 1])) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  static List<int> _getHiddenCharactersBeforeRanges(List<HiddenRange> ranges) {
+    int sum = 0;
+    final result = List.filled(ranges.length + 1, 0);
+
+    for (int i = 1; i <= ranges.length; i++) {
+      sum += ranges[i - 1].text.length;
+      result[i] = sum;
+    }
+
+    return result;
+  }
+
+  /// Cuts the hidden ranges from [str].
+  ///
+  /// [str] is considered a substring of a full text, starting at [start].
+  String cutString(String str, {required int start}) {
+    if (ranges.isEmpty || str == '') {
+      return str;
+    }
+
+    final end = start + str.length;
+    final buffer = StringBuffer();
+
+    // Skip the ranges that are too early.
+    int rangeIndex = _getFirstRangeIndexThatCanOverlapWith(start);
+
+    // The current position in a virtual full string we are cutting.
+    int position = start;
+
+    // In this loop, for each range add the content before its start.
+    // Break in 2 cases:
+    // 1. No more ranges.
+    // 2. `str` has ended, all further ranges are after it.
+    while (rangeIndex < ranges.length && position < end) {
+      final range = ranges[rangeIndex];
+
+      final substringEnd = min(range.start - start, str.length);
+      if (substringEnd > 0) {
+        buffer.write(str.substring(position - start, substringEnd));
+      }
+      position = range.end;
+      rangeIndex++;
+    }
+
+    // Add the remainder of `str` after all the ranges were processed.
+    if (position < end) {
+      buffer.write(str.substring(position - start));
+    }
+
+    return buffer.toString();
+  }
+
+  /// Returns the index of the first range that can potentially overlap
+  /// with a string that starts at [start]. It just skips ranges that end
+  /// before [start].
+  int _getFirstRangeIndexThatCanOverlapWith(int start) {
+    int i = 0;
+
+    for (; i < ranges.length; i++) {
+      if (ranges[i].end > start) {
+        return i;
+      }
+    }
+
+    return i;
+  }
+
+  /// Cuts hidden ranges from [highlighted].
+  Result cutHighlighted(Result highlighted) {
+    int fullChar = 0;
+
+    String? _cutString(String? nodeValue) {
+      if (nodeValue == null) {
+        return null;
+      }
+
+      final result = cutString(nodeValue, start: fullChar);
+      fullChar += nodeValue.length;
+      return result;
+    }
+
+    Node _cutHighlightedNode(Node node) {
+      final value = _cutString(node.value);
+      final children =
+          node.children?.map(_cutHighlightedNode).toList(growable: false);
+
+      return Node(
+        className: node.className,
+        value: value,
+        children: children,
+        noPrefix: node.noPrefix,
+      );
+    }
+
+    final nodes =
+        highlighted.nodes?.map(_cutHighlightedNode).toList(growable: false) ??
+            const <Node>[];
+
+    return Result(
+      relevance: highlighted.relevance,
+      nodes: nodes,
+      language: highlighted.language,
+      top: highlighted.top,
+    );
+  }
+
+  /// Translates the [position] in the visible text to the position in
+  /// the full text.
+  ///
+  /// If any hidden ranges are collapsed at this [position],
+  /// [placeHiddenRanges] determines whether to put them before or after
+  /// the cursor. Putting them after effectively ignores them.
+  int recoverPosition(int position, {required TextAffinity placeHiddenRanges}) {
+    if (ranges.isEmpty || position < ranges.first.start) {
+      return position;
+    }
+
+    if (position > ranges.last.end - hiddenCharactersBeforeRanges.last) {
+      return position + hiddenCharactersBeforeRanges.last;
+    }
+
+    int lowerRange = 0;
+    int upperRange = ranges.length - 1;
+
+    while (upperRange > lowerRange) {
+      // Visible characters if the range collapse positions.
+      final lowerChar =
+          ranges[lowerRange].end - hiddenCharactersBeforeRanges[lowerRange + 1];
+      final upperChar =
+          ranges[upperRange].end - hiddenCharactersBeforeRanges[upperRange + 1];
+
+      int rangeIndex = lowerRange +
+          ((position - lowerChar) /
+                  (upperChar - lowerChar) *
+                  (upperRange - lowerRange))
+              .floor();
+      final range = ranges[rangeIndex];
+
+      final collapsePosition =
+          range.end - hiddenCharactersBeforeRanges[rangeIndex + 1];
+
+      switch ((collapsePosition - position).sign) {
+        case 0: // position is at the range collapse.
+          switch (placeHiddenRanges) {
+            case TextAffinity.downstream:
+              // Find the first range collapsed at this point.
+              while (rangeIndex > 0 &&
+                  ranges[rangeIndex - 1].end == ranges[rangeIndex].start) {
+                rangeIndex--;
+              }
+              return ranges[rangeIndex].start;
+            case TextAffinity.upstream:
+              // Find the last range collapsed at this point.
+              while (rangeIndex < ranges.length - 1 &&
+                  ranges[rangeIndex].end == ranges[rangeIndex + 1].start) {
+                rangeIndex++;
+              }
+              return ranges[rangeIndex].end;
+          }
+
+        case -1: // position is after the range collapse
+          lowerRange = rangeIndex + 1;
+          final range = ranges[lowerRange];
+          final collapsePosition =
+              range.end - hiddenCharactersBeforeRanges[lowerRange + 1];
+          if (collapsePosition > position) {
+            return position + hiddenCharactersBeforeRanges[lowerRange];
+          }
+          break;
+
+        case 1: // position is before the range
+          upperRange = rangeIndex - 1;
+          final range = ranges[upperRange];
+          final collapsePosition =
+              range.end - hiddenCharactersBeforeRanges[upperRange + 1];
+          if (collapsePosition < position) {
+            return position + hiddenCharactersBeforeRanges[upperRange + 1];
+          }
+          break;
+      }
+    }
+
+    // There is only one range, and position is at its collapse.
+    switch (placeHiddenRanges) {
+      case TextAffinity.downstream:
+        return ranges[0].start;
+      case TextAffinity.upstream:
+        return ranges[0].end;
+    }
+  }
+
+  @override
+  int get hashCode => Object.hash(
+        Object.hashAll(ranges),
+        textLength,
+      );
+
+  @override
+  bool operator ==(Object other) {
+    return other is HiddenRanges &&
+        textLength == other.textLength &&
+        const ListEquality().equals(ranges, other.ranges);
+  }
+}
diff --git a/lib/src/named_sections/named_section.dart b/lib/src/named_sections/named_section.dart
index 6cf2beeb..35af363f 100644
--- a/lib/src/named_sections/named_section.dart
+++ b/lib/src/named_sections/named_section.dart
@@ -6,7 +6,8 @@ class NamedSection {
   final int startLine;
 
   /// Zero-based index of the line with the ending tag.
-  final int endLine;
+  /// `null` if the section spans till the end of the document.
+  final int? endLine;
 
   final String name;
 
diff --git a/lib/src/named_sections/parsers/abstract.dart b/lib/src/named_sections/parsers/abstract.dart
index 614d156a..58a55062 100644
--- a/lib/src/named_sections/parsers/abstract.dart
+++ b/lib/src/named_sections/parsers/abstract.dart
@@ -1,5 +1,3 @@
-import 'package:meta/meta.dart';
-
 import '../../single_line_comments/single_line_comment.dart';
 import '../named_section.dart';
 
@@ -19,28 +17,22 @@ abstract class AbstractNamedSectionParser {
 
   List<NamedSection> parse({
     required List<SingleLineComment> singleLineComments,
-    required int lineCount,
   }) {
     final unsorted = parseUnsorted(
       singleLineComments: singleLineComments,
-      lineCount: lineCount,
     );
 
-    return unsorted
-        .where((section) => _isValid(section, lineCount))
-        .toList(growable: false)
+    return unsorted.where(_isValid).toList(growable: false)
       ..sort((a, b) => a.startLine - b.startLine);
   }
 
-  @protected
   List<NamedSection> parseUnsorted({
     required List<SingleLineComment> singleLineComments,
-    required int lineCount,
   });
 
-  bool _isValid(NamedSection section, int lineCount) {
+  bool _isValid(NamedSection section) {
+    final endLine = section.endLine;
     return section.startLine >= 0 &&
-        section.endLine < lineCount &&
-        section.startLine <= section.endLine;
+        (endLine == null || section.startLine <= endLine);
   }
 }
diff --git a/lib/src/named_sections/parsers/brackets_start_end.dart b/lib/src/named_sections/parsers/brackets_start_end.dart
index ceba0919..5f4aa49c 100644
--- a/lib/src/named_sections/parsers/brackets_start_end.dart
+++ b/lib/src/named_sections/parsers/brackets_start_end.dart
@@ -16,7 +16,7 @@ import 'abstract.dart';
 /// different comments.
 ///
 /// If a section is never started, it starts at the line 0.
-/// If a section is never ended, it ends at the last line (count - 1).
+/// If a section is never ended, its `endLine` is `null`.
 ///
 /// If a section is started multiple times, the min line number takes effect.
 /// If a section is ended multiple times, the max line number takes effect.
@@ -31,7 +31,6 @@ class BracketsStartEndNamedSectionParser extends AbstractNamedSectionParser {
   @override
   List<NamedSection> parseUnsorted({
     required List<SingleLineComment> singleLineComments,
-    required int lineCount,
   }) {
     final starts = <String, int>{};
     final ends = <String, int>{};
@@ -39,7 +38,10 @@ class BracketsStartEndNamedSectionParser extends AbstractNamedSectionParser {
     for (final comment in singleLineComments) {
       for (final match in _startRe.allMatches(comment.innerContent)) {
         final name = match.group(3) ?? '';
-        starts[name] = min(comment.lineIndex, starts[name] ?? lineCount - 1);
+        final oldStart = starts[name];
+        starts[name] = oldStart == null
+            ? comment.lineIndex
+            : min(comment.lineIndex, oldStart);
       }
 
       for (final match in _endRe.allMatches(comment.innerContent)) {
@@ -48,13 +50,12 @@ class BracketsStartEndNamedSectionParser extends AbstractNamedSectionParser {
       }
     }
 
-    return _combineStartsAndEnds(starts, ends, lineCount);
+    return _combineStartsAndEnds(starts, ends);
   }
 
   List<NamedSection> _combineStartsAndEnds(
     Map<String, int> starts,
     Map<String, int> ends,
-    int lineCount,
   ) {
     final sections = <NamedSection>[];
 
@@ -80,7 +81,7 @@ class BracketsStartEndNamedSectionParser extends AbstractNamedSectionParser {
       sections.add(
         NamedSection(
           startLine: start,
-          endLine: ends[name] ?? lineCount - 1,
+          endLine: ends[name],
           name: name,
         ),
       );
diff --git a/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart b/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart
index fa8fc8d2..8db82864 100644
--- a/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart
+++ b/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart
@@ -21,6 +21,7 @@ class HighlightSingleLineCommentParser extends AbstractSingleLineCommentParser {
 
   void _parse() {
     int lineIndex = 0;
+    int characterIndex = 0;
 
     for (final node in highlighted.nodes ?? const <Node>[]) {
       if (node.className == 'comment') {
@@ -31,6 +32,7 @@ class HighlightSingleLineCommentParser extends AbstractSingleLineCommentParser {
             SingleLineComment.cut(
               value,
               lineIndex: lineIndex,
+              characterIndex: characterIndex,
               sequences: singleLineCommentSequences,
             ),
           );
@@ -38,6 +40,7 @@ class HighlightSingleLineCommentParser extends AbstractSingleLineCommentParser {
       }
 
       lineIndex += node.getNewLineCount();
+      characterIndex += node.getCharacterCount();
     }
   }
 
@@ -52,7 +55,7 @@ class HighlightSingleLineCommentParser extends AbstractSingleLineCommentParser {
   }
 }
 
-extension _NewLineCount on Node {
+extension on Node {
   int getNewLineCount() {
     int result = '\n'.allMatches(value ?? '').length;
 
@@ -62,4 +65,14 @@ extension _NewLineCount on Node {
 
     return result;
   }
+
+  int getCharacterCount() {
+    int result = value?.length ?? 0;
+
+    for (final child in children ?? const <Node>[]) {
+      result += child.getCharacterCount();
+    }
+
+    return result;
+  }
 }
diff --git a/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart b/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart
index 5934890f..7ed38595 100644
--- a/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart
+++ b/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart
@@ -20,6 +20,7 @@ class TextSingleLineCommentParser extends AbstractSingleLineCommentParser {
   void _parse() {
     final lines = text.split('\n');
     int lineIndex = 0;
+    int characterIndex = 0;
 
     for (final line in lines) {
       final column = _getCommentColumn(line);
@@ -29,12 +30,14 @@ class TextSingleLineCommentParser extends AbstractSingleLineCommentParser {
           SingleLineComment.cut(
             line.substring(column),
             lineIndex: lineIndex,
+            characterIndex: characterIndex + column,
             sequences: singleLineCommentSequences,
           ),
         );
       }
 
       lineIndex++;
+      characterIndex += line.length + 1;
     }
   }
 
diff --git a/lib/src/single_line_comments/single_line_comment.dart b/lib/src/single_line_comments/single_line_comment.dart
index b856a3e5..e0791879 100644
--- a/lib/src/single_line_comments/single_line_comment.dart
+++ b/lib/src/single_line_comments/single_line_comment.dart
@@ -2,46 +2,56 @@ import 'package:meta/meta.dart';
 
 @immutable
 class SingleLineComment {
-  /// Zero-based index of the line at which this comment is found.
-  final int lineIndex;
+  /// Zero-based index of the first character from the beginning of the text.
+  final int characterIndex;
 
   /// The comment text without the characters signifying the comment.
   final String innerContent;
 
+  /// Zero-based index of the line at which this comment is found.
+  final int lineIndex;
+
   /// The comment text with the characters signifying the comment.
   final String outerContent;
 
   const SingleLineComment({
-    required this.lineIndex,
     required this.innerContent,
+    required this.lineIndex,
+    this.characterIndex = 0,
     this.outerContent = '',
   });
 
+  /// Creates the object from [outerContent] by extracting [innerContent]
+  /// from it.
   SingleLineComment.cut(
     String outerContent, {
+    required int characterIndex,
     required int lineIndex,
     required List<String> sequences,
   }) : this(
-          lineIndex: lineIndex,
-          outerContent: outerContent,
+          characterIndex: characterIndex,
           innerContent: _cutSequence(
             outerContent: outerContent,
             sequences: sequences,
           ),
+          lineIndex: lineIndex,
+          outerContent: outerContent,
         );
 
   @override
   int get hashCode => Object.hash(
-        lineIndex,
+        characterIndex,
         innerContent,
+        lineIndex,
         outerContent,
       );
 
   @override
   bool operator ==(Object other) {
     return other is SingleLineComment &&
-        lineIndex == other.lineIndex &&
+        characterIndex == other.characterIndex &&
         innerContent == other.innerContent &&
+        lineIndex == other.lineIndex &&
         outerContent == other.outerContent;
   }
 
diff --git a/pubspec.yaml b/pubspec.yaml
index 2b4a4a3f..6bb18d62 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
 name: code_text_field
 description: A customizable code field supporting syntax highlighting, bi-directionnal scrolling and code modifiers
 version: 1.0.2
-homepage: https://github.com/BertrandBev/code_field
+homepage: https://github.com/akvelon/flutter-code-editor
 
 environment:
   sdk: ">=2.17.0 <3.0.0"
@@ -10,8 +10,7 @@ environment:
 dependencies:
   autotrie: ^2.0.0
   collection: ^1.16.0
-  flutter:
-    sdk: flutter
+  flutter: { sdk: flutter }
   flutter_highlight: ^0.7.0
   highlight: ^0.7.0
   linked_scroll_controller: ^0.2.0-nullsafety.0
@@ -20,9 +19,8 @@ dependencies:
   tuple: ^2.0.0
 
 dev_dependencies:
-  flutter_test:
-    sdk: flutter
-  total_lints: ^2.17.2
+  flutter_test: { sdk: flutter }
+  total_lints: ^2.17.4
 
 flutter:
   uses-material-design: true


Test patch:
diff --git a/test/src/code/code_get_edit_result_test.dart b/test/src/code/code_get_edit_result_test.dart
new file mode 100644
index 00000000..8fc3abf2
--- /dev/null
+++ b/test/src/code/code_get_edit_result_test.dart
@@ -0,0 +1,258 @@
+import 'dart:ui';
+
+import 'package:code_text_field/src/code/code.dart';
+import 'package:code_text_field/src/code/code_edit_result.dart';
+import 'package:code_text_field/src/named_sections/parsers/brackets_start_end.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/highlight.dart';
+import 'package:highlight/languages/java.dart';
+
+const _languageName = 'java';
+final _language = java;
+
+const _fullText1 = '''
+public class MyClass {
+  public void main() { // [START section1]
+  }
+  // [END section1]
+  // [START section2]
+  void method() {
+  }
+  // [END section2]
+}
+''';
+
+// Each blank line has two spaces here:
+const _visibleText1 = '''
+public class MyClass {
+  public void main() { 
+  }
+  
+  
+  void method() {
+  }
+  
+}
+''';
+
+void main() {
+  test('Code. getEditResult', () {
+    const examples = [
+      //
+      _Example(
+        'Empty -> Empty',
+        fullTextBefore: '',
+        visibleValueAfter: TextEditingValue.empty,
+        expected: CodeEditResult(
+          fullTextAfter: '',
+          linesChanged: TextRange.empty,
+        ),
+      ),
+
+      _Example(
+        'Empty -> Something',
+        fullTextBefore: '',
+        visibleValueAfter: TextEditingValue(text: _visibleText1),
+        expected: CodeEditResult(
+          fullTextAfter: _visibleText1,
+          linesChanged: TextRange(start: 0, end: 0),
+        ),
+      ),
+
+      _Example(
+        'Same',
+        fullTextBefore: _fullText1,
+        visibleValueAfter: TextEditingValue(text: _visibleText1),
+        expected: CodeEditResult(
+          fullTextAfter: _fullText1,
+          linesChanged: TextRange.empty,
+        ),
+      ),
+
+      _Example(
+        'Something -> Empty',
+        fullTextBefore: _fullText1,
+        visibleValueAfter: TextEditingValue.empty,
+        expected: CodeEditResult(
+          fullTextAfter: '',
+          linesChanged: TextRange(start: 0, end: 8), // Empty line 9 is intact.
+        ),
+      ),
+
+      _Example(
+        'Change not touching hidden range borders',
+        fullTextBefore: _fullText1,
+        visibleValueAfter: TextEditingValue(
+          // Each blank line has two spaces here:
+          text: '''
+public class MyClass {
+  public void main() { 
+  }
+  
+  
+  method(int a) {{
+  }}
+  
+}
+''',
+        ),
+        expected: CodeEditResult(
+          fullTextAfter: '''
+public class MyClass {
+  public void main() { // [START section1]
+  }
+  // [END section1]
+  // [START section2]
+  method(int a) {{
+  }}
+  // [END section2]
+}
+''',
+          linesChanged: TextRange(start: 5, end: 6),
+        ),
+      ),
+
+      _Example(
+        'Insertion at a range collapse - Inserts before the range',
+        fullTextBefore: _fullText1,
+        visibleValueAfter: TextEditingValue(
+          // Each blank line has two spaces here:
+          text: '''
+public class MyClass {
+  public void main() { 
+  }
+  
+  
+  void method() {
+  }
+  ;
+}
+''',
+        ),
+        expected: CodeEditResult(
+          fullTextAfter: '''
+public class MyClass {
+  public void main() { // [START section1]
+  }
+  // [END section1]
+  // [START section2]
+  void method() {
+  }
+  ;// [END section2]
+}
+''',
+          linesChanged: TextRange.collapsed(7),
+        ),
+      ),
+
+      _Example(
+        'Removing a block that is both before and after a hidden range - '
+        'Removes it before',
+        fullTextBefore: '''
+{
+//[START section1]
+}
+''',
+        visibleValueAfter: TextEditingValue(
+          text: '''
+{
+}
+''',
+        ),
+        expected: CodeEditResult(
+          fullTextAfter: '''
+{//[START section1]
+}
+''',
+          linesChanged: TextRange(start: 0, end: 1),
+        ),
+      ),
+
+      _Example(
+        'Replacing between ranges keeps the ranges - '
+        'Keeps the range after, Deletes the range before',
+        fullTextBefore: '''
+{//[START section1]
+;//[END section1]
+}
+''',
+        visibleValueAfter: TextEditingValue(
+          text: '''
+{()
+}
+''',
+        ),
+        expected: CodeEditResult(
+          fullTextAfter: '''
+{()//[END section1]
+}
+''',
+          linesChanged: TextRange(start: 0, end: 1),
+        ),
+      ),
+
+      _Example(
+        'All text is a single hidden range -> Empty -> No change',
+        fullTextBefore: '//[START section1]',
+        visibleValueAfter: TextEditingValue.empty,
+        expected: CodeEditResult(
+          fullTextAfter: '//[START section1]',
+          linesChanged: TextRange.empty,
+        ),
+      ),
+
+      _Example(
+        'If all text is a single hidden range, insert before it',
+        fullTextBefore: '//[START section1]',
+        visibleValueAfter: TextEditingValue(
+          text: ';',
+        ),
+        expected: CodeEditResult(
+          fullTextAfter: ';//[START section1]',
+          linesChanged: TextRange(start: 0, end: 0),
+        ),
+      ),
+    ];
+
+    for (final example in examples) {
+      final highlighted = highlight.parse(
+        example.fullTextBefore,
+        language: _languageName,
+      );
+
+      final code = Code(
+        text: example.fullTextBefore,
+        highlighted: highlighted,
+        language: _language,
+        namedSectionParser: const BracketsStartEndNamedSectionParser(),
+      );
+
+      expect(
+        () => code.getEditResult(example.visibleValueAfter),
+        returnsNormally,
+        reason: example.name,
+      );
+
+      final result = code.getEditResult(example.visibleValueAfter);
+      expect(
+        result,
+        example.expected,
+        reason: example.name,
+      );
+    }
+  });
+}
+
+class _Example {
+  final String name;
+  final String fullTextBefore;
+  final TextEditingValue visibleValueAfter;
+  final CodeEditResult? expected;
+
+  const _Example(
+    this.name, {
+    required this.fullTextBefore,
+    required this.visibleValueAfter,
+    this.expected,
+  });
+}
diff --git a/test/src/code/code_hidden_ranges_test.dart b/test/src/code/code_hidden_ranges_test.dart
new file mode 100644
index 00000000..e07825e1
--- /dev/null
+++ b/test/src/code/code_hidden_ranges_test.dart
@@ -0,0 +1,50 @@
+import 'package:code_text_field/src/code/code.dart';
+import 'package:code_text_field/src/hidden_ranges/hidden_range.dart';
+import 'package:code_text_field/src/hidden_ranges/hidden_ranges.dart';
+import 'package:code_text_field/src/named_sections/parsers/brackets_start_end.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/highlight.dart';
+import 'package:highlight/languages/java.dart';
+
+void main() {
+  group('Code. HiddenRanges.', () {
+    test('Section comments and readonly are parsed to hidden ranges', () {
+      const text = '''
+public class MyClass { // comment
+  public void main() { // [START section1]
+  } // comment readonly
+}
+''';
+
+      final highlighted = highlight.parse(text, language: 'java');
+      final code = Code(
+        text: text,
+        highlighted: highlighted,
+        namedSectionParser: const BracketsStartEndNamedSectionParser(),
+        readOnlySectionNames: {'section1', 'nonexistent'},
+        language: java,
+      );
+
+      expect(
+        code.hiddenRanges,
+        HiddenRanges(
+          ranges: const [
+            HiddenRange(start: 57, end: 76, text: '// [START section1]'),
+            HiddenRange(start: 81, end: 100, text: '// comment readonly'),
+          ],
+          textLength: text.length,
+        ),
+      );
+
+      expect(
+        code.visibleHighlighted!.toHtml(),
+        '''
+<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyClass</span> </span>{ <span class="hljs-comment">// comment</span>
+  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{ <span class="hljs-comment"></span>
+  } <span class="hljs-comment"></span>
+}
+''',
+      );
+    });
+  });
+}
diff --git a/test/src/code/code_test.dart b/test/src/code/code_lines_test.dart
similarity index 61%
rename from test/src/code/code_test.dart
rename to test/src/code/code_lines_test.dart
index 7b88caf1..4efc5178 100644
--- a/test/src/code/code_test.dart
+++ b/test/src/code/code_lines_test.dart
@@ -2,29 +2,13 @@ import 'dart:ui';
 
 import 'package:code_text_field/src/code/code.dart';
 import 'package:code_text_field/src/code/code_line.dart';
-import 'package:code_text_field/src/named_sections/parsers/brackets_start_end.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:highlight/languages/angelscript.dart';
 import 'package:highlight/languages/java.dart';
 
-final _language = java;
-const _text = '''
-1 Lorem ipsum dolor sit amet,
-2 consectetur adipiscing elit,
-3 sed do eiusmod tempor incididunt ut labore et dolore magna
-4 aliqua. Ut enim ad minim veniam,
-5 quis nostrud exercitation ullamco laboris nisi
-6 ut aliquip
-7 ex
+import '../common/lorem_ipsum.dart';
 
-9 ea commodo consequat.
-10 Duis aute irure dolor in
-11 reprehenderit in voluptate velit esse cillum dolore
-12 eu fugiat nulla pariatur.
-13 Excepteur sint occaecat cupidatat
-14 non proident,
-15 sunt in culpa qui officia deserunt mollit anim id est laborum.''';
+final _language = java;
 
 void main() {
   group('Code. Lines.', () {
@@ -51,7 +35,7 @@ void main() {
     });
 
     test('Parses lines', () {
-      final code = Code(text: _text, language: _language);
+      final code = Code(text: loremIpsum, language: _language);
 
       expect(
         code.lines,
@@ -153,7 +137,7 @@ void main() {
     test('characterIndexToLineIndex', () {
       const tails = ['', '\n'];
       for (final tail in tails) {
-        final textWithTail = _text + tail;
+        final textWithTail = loremIpsum + tail;
         final code = Code(
           text: textWithTail,
           language: _language,
@@ -222,95 +206,4 @@ void main() {
       }
     });
   });
-
-  group('Code. Read-only.', () {
-    test('Parse read-only lines by end comments', () {
-      const dataSets = [
-        {
-          'text': '''
-            readonly
-            editable// readonly
-''', //                                        Empty line inherits readonly
-          'readonly': [false, true, true],
-        },
-        {
-          'text': '''
-            readonly
-            readonly //readonly
-            readonly // a readonly b'''
-              '\n\n'
-              '''
-            The above line is empty but not last, so does not inherit readonly
-            ''',
-          'readonly': [false, true, true, false, false, false],
-        },
-        {
-          'text': '''
-            readonly
-            editable// readonly
-            ''', // Last after read only, but not empty, so editable.
-          'readonly': [false, true, false],
-        },
-      ];
-
-      for (final data in dataSets) {
-        final code = Code(
-          text: data['text']! as String,
-          language: _language,
-        );
-
-        final readonly = data['readonly']! as List<bool>;
-        for (int i = code.lines.length; --i >= 0;) {
-          expect(code.lines[i].isReadOnly, readonly[i], reason: 'Line #$i');
-        }
-      }
-    });
-
-    test(
-      'Does not parse an unsupported language',
-      () {
-        const textWithReadonly = 'end of line // readonly';
-        final code = Code(text: textWithReadonly, language: angelscript);
-        expect(code.lines.first.isReadOnly, false);
-      },
-    );
-
-    test('Lines in read-only sections are read-only', () {
-      const text = '''
-public class MyClass {
-  public void main() { // [START section1]
-  }
-  // [END section1]
-  // [START section2]
-  void method() {
-  }
-  // [END section2]
-}
-''';
-      const expected = [
-        false,
-        true,
-        true,
-        true,
-        false,
-        false,
-        false,
-        false,
-        false,
-        false,
-      ];
-
-      final code = Code(
-        text: text,
-        namedSectionParser: const BracketsStartEndNamedSectionParser(),
-        readOnlySectionNames: {'section1', 'nonexistent'},
-        language: java,
-      );
-
-      expect(
-        code.lines.map((line) => line.isReadOnly),
-        expected,
-      );
-    });
-  });
 }
diff --git a/test/src/code/code_read_only_test.dart b/test/src/code/code_read_only_test.dart
new file mode 100644
index 00000000..febec8f5
--- /dev/null
+++ b/test/src/code/code_read_only_test.dart
@@ -0,0 +1,102 @@
+import 'package:code_text_field/src/code/code.dart';
+import 'package:code_text_field/src/named_sections/parsers/brackets_start_end.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/languages/angelscript.dart';
+import 'package:highlight/languages/java.dart';
+
+final _language = java;
+
+void main() {
+  group('Code. Read-only.', () {
+    test('Parse read-only lines by end comments', () {
+      const dataSets = [
+        {
+          'text': '''
+            readonly
+            editable// readonly
+''', //                                        Empty line inherits readonly
+          'readonly': [false, true, true],
+        },
+        {
+          'text': '''
+            readonly
+            readonly //readonly
+            readonly // a readonly b'''
+              '\n\n'
+              '''
+            The above line is empty but not last, so does not inherit readonly
+            ''',
+          'readonly': [false, true, true, false, false, false],
+        },
+        {
+          'text': '''
+            readonly
+            editable// readonly
+            ''', // Last after read only, but not empty, so editable.
+          'readonly': [false, true, false],
+        },
+      ];
+
+      for (final data in dataSets) {
+        final code = Code(
+          text: data['text']! as String,
+          language: _language,
+        );
+
+        final readonly = data['readonly']! as List<bool>;
+        for (int i = code.lines.length; --i >= 0;) {
+          expect(code.lines[i].isReadOnly, readonly[i], reason: 'Line #$i');
+        }
+      }
+    });
+
+    test(
+      'Does not parse an unsupported language',
+      () {
+        const textWithReadonly = 'end of line // readonly';
+
+        final code = Code(text: textWithReadonly, language: angelscript);
+
+        expect(code.lines.first.isReadOnly, false);
+      },
+    );
+
+    test('Lines in read-only sections are read-only', () {
+      const text = '''
+public class MyClass {
+  public void main() { // [START section1]
+  }
+  // [END section1]
+  // [START section2]
+  void method() {
+  }
+  // [END section2]
+}
+''';
+      const expected = [
+        false,
+        true,
+        true,
+        true,
+        false,
+        false,
+        false,
+        false,
+        false,
+        false,
+      ];
+
+      final code = Code(
+        text: text,
+        namedSectionParser: const BracketsStartEndNamedSectionParser(),
+        readOnlySectionNames: {'section1', 'nonexistent'},
+        language: java,
+      );
+
+      expect(
+        code.lines.map((line) => line.isReadOnly),
+        expected,
+      );
+    });
+  });
+}
diff --git a/test/src/code/string_get_changed_range_test.dart b/test/src/code/string_get_changed_range_test.dart
new file mode 100644
index 00000000..bbf61283
--- /dev/null
+++ b/test/src/code/string_get_changed_range_test.dart
@@ -0,0 +1,132 @@
+import 'dart:ui';
+
+import 'package:code_text_field/src/code/string.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('String.getChangedRange', () {
+    const examples = [
+      //
+      _Example(
+        'Empty vs empty',
+        str1: '',
+        str2: '',
+        expectedSingle: TextRange.empty,
+      ),
+
+      _Example(
+        'Same',
+        str1: 'abc',
+        str2: 'abc',
+        expectedSingle: TextRange.empty,
+      ),
+
+      _Example(
+        'Adding: Empty vs non-empty',
+        str1: '',
+        str2: 'abc',
+        expectedSingle: TextRange(start: 0, end: 3),
+      ),
+
+      _Example(
+        'Adding: Common prefix',
+        str1: 'abc',
+        str2: 'abc123',
+        expectedSingle: TextRange(start: 3, end: 6),
+      ),
+
+      _Example(
+        'Adding: Common suffix',
+        str1: 'abc',
+        str2: '123abc',
+        expectedSingle: TextRange(start: 0, end: 3),
+      ),
+
+      _Example(
+        'Adding duplicate',
+        str1: 'abc',
+        str2: 'abc1abc',
+        expected: [TextRange(start: 0, end: 4), TextRange(start: 3, end: 7)],
+      ),
+
+      _Example(
+        'Removing: Non-empty vs empty',
+        str1: 'abc',
+        str2: '',
+        expectedSingle: TextRange(start: 0, end: 0),
+      ),
+
+      _Example(
+        'Removing: Common prefix',
+        str1: 'abc123',
+        str2: 'abc',
+        expectedSingle: TextRange(start: 3, end: 3),
+      ),
+
+      _Example(
+        'Removing a duplicate on a side',
+        str1: 'abc1abc',
+        str2: 'abc',
+        expected: [TextRange(start: 0, end: 0), TextRange(start: 3, end: 3)],
+      ),
+
+      _Example(
+        'Replacing',
+        str1: 'abc123def',
+        str2: 'abc3210def',
+        expectedSingle: TextRange(start: 3, end: 7),
+      ),
+
+      _Example(
+        'Removed a duplicate in the middle',
+        str1: 'abccde',
+        str2: 'abcde',
+        expected: [TextRange(start: 2, end: 2), TextRange(start: 3, end: 3)],
+      )
+    ];
+    const affinities = [TextAffinity.upstream, TextAffinity.downstream];
+
+    for (final example in examples) {
+      for (int affinityIndex = 0; affinityIndex < 2; affinityIndex++) {
+        final affinity = affinities[affinityIndex];
+        final reason = '${example.name}, $affinity';
+
+        expect(
+          () => example.str2.getChangedRange(
+            example.str1,
+            attributeChangeTo: affinity,
+          ),
+          returnsNormally,
+          reason: reason,
+        );
+
+        final range = example.str2.getChangedRange(
+          example.str1,
+          attributeChangeTo: affinity,
+        );
+
+        expect(
+          range,
+          example.expectedSingle ?? example.expected![affinityIndex],
+          reason: reason,
+        );
+      }
+    }
+  });
+}
+
+class _Example {
+  final String name;
+  final String str1;
+  final String str2;
+  final List<TextRange>? expected;
+  final TextRange? expectedSingle;
+
+  const _Example(
+    this.name, {
+    required this.str1,
+    required this.str2,
+    this.expected,
+    this.expectedSingle,
+  });
+}
diff --git a/test/src/code_field/code_controller_hidden_ranges_test.dart b/test/src/code_field/code_controller_hidden_ranges_test.dart
new file mode 100644
index 00000000..5cc5f27d
--- /dev/null
+++ b/test/src/code_field/code_controller_hidden_ranges_test.dart
@@ -0,0 +1,132 @@
+import 'package:code_text_field/code_text_field.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/languages/java.dart';
+
+import '../common/create_app.dart';
+
+const _text = '''
+// [START section2]
+void method() {
+}
+''';
+
+final _language = java;
+
+void main() {
+  group('CodeController. Value vs Code.', (){
+    test('With no service comments, value == code', () {
+      const text = '''
+public class MyClass {
+  public void main() {// comment
+  }
+}
+''';
+
+      final controller = CodeController(text: text, language: java);
+      final initialValue = controller.value;
+      controller.fullText = '$text//';
+      final changedValue = controller.value;
+
+      expect(initialValue, const TextEditingValue(text: text));
+      expect(changedValue, const TextEditingValue(text: '$text//'));
+    });
+
+    test('With service comments, value == code less service_comments', () {
+      const text = '''
+public class MyClass {// readonly
+  public void main() {// comment
+  }
+}
+''';
+      const textLessComments = '''
+public class MyClass {
+  public void main() {// comment
+  }
+}
+''';
+
+      final controller = CodeController(text: text, language: java);
+      final initialValue = controller.value;
+      controller.fullText = '$text//';
+      final changedValue = controller.value;
+
+      expect(initialValue, const TextEditingValue(text: textLessComments));
+      expect(changedValue, const TextEditingValue(text: '$textLessComments//'));
+    });
+  });
+
+  group('CodeController. Edits around hidden rages.', () {
+    testWidgets(
+      'Cannot get text into the hidden range comment, delete the range instead',
+      (WidgetTester wt) async {
+        final controller = CodeController(
+          text: _text,
+          language: _language,
+          namedSectionParser: const BracketsStartEndNamedSectionParser(),
+        );
+        final focusNode = FocusNode();
+
+        await wt.pumpWidget(createApp(controller, focusNode));
+        focusNode.requestFocus();
+
+        // Go to the beginning.
+        await wt.sendKeyDownEvent(LogicalKeyboardKey.alt);
+        await wt.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+        await wt.sendKeyUpEvent(LogicalKeyboardKey.alt);
+
+        await wt.sendKeyEvent(LogicalKeyboardKey.delete);
+
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: 'void method() {\n}\n',
+            //     \ cursor
+            selection: TextSelection.collapsed(offset: 0),
+          ),
+        );
+        expect(
+          controller.fullText,
+          'void method() {\n}\n',
+        );
+      },
+    );
+
+    testWidgets(
+      'A typed-in service comment becomes a hidden range',
+      (WidgetTester wt) async {
+        final controller = CodeController(
+          text: _text,
+          language: _language,
+          namedSectionParser: const BracketsStartEndNamedSectionParser(),
+        );
+        final focusNode = FocusNode();
+
+        await wt.pumpWidget(createApp(controller, focusNode));
+        focusNode.requestFocus();
+
+        // Go to the beginning.
+        await wt.sendKeyDownEvent(LogicalKeyboardKey.alt);
+        await wt.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+        await wt.sendKeyUpEvent(LogicalKeyboardKey.alt);
+
+        await wt.sendKeyEvent(LogicalKeyboardKey.arrowRight);
+        controller.value = controller.value.replacedSelection('//readonly ');
+
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: '\n\n}\n',
+            //       \ cursor
+            selection: TextSelection.collapsed(offset: 1),
+          ),
+        );
+        expect(
+          controller.fullText,
+          '// [START section2]\n//readonly void method() {\n}\n',
+        );
+      },
+    );
+  });
+}
diff --git a/test/src/code_field/code_controller_test.dart b/test/src/code_field/code_controller_readonly_test.dart
similarity index 61%
rename from test/src/code_field/code_controller_test.dart
rename to test/src/code_field/code_controller_readonly_test.dart
index 4e5157c6..9fb8ae6a 100644
--- a/test/src/code_field/code_controller_test.dart
+++ b/test/src/code_field/code_controller_readonly_test.dart
@@ -4,6 +4,8 @@ import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:highlight/languages/java.dart';
 
+import '../common/create_app.dart';
+
 const _editableBeginningEnd = '''
 abc
 ro//readonly
@@ -30,7 +32,7 @@ void main() {
         );
         final focusNode = FocusNode();
 
-        await wt.pumpWidget(_createApp(controller, focusNode));
+        await wt.pumpWidget(createApp(controller, focusNode));
         focusNode.requestFocus();
 
         // Go to the beginning.
@@ -43,24 +45,34 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\nabc\n',
+            text: 'abc\nro\nro\nabc\n',
             //     \ cursor
             selection: TextSelection.collapsed(offset: 0),
           ),
           reason: 'Backspace - No effect',
         );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Backspace - No effect',
+        );
 
         await wt.sendKeyEvent(LogicalKeyboardKey.delete);
 
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'bc\nro//readonly\nro//readonly\nabc\n',
+            text: 'bc\nro\nro\nabc\n',
             //     \ cursor
             selection: TextSelection.collapsed(offset: 0),
           ),
           reason: 'Delete - OK',
         );
+        expect(
+          controller.fullText,
+          'bc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Delete - OK',
+        );
 
         // TODO(alexeyinkin): Simulate keyboard entry, https://github.com/akvelon/flutter-code-editor/issues/30
         controller.value = controller.value.replacedSelection('a\n');
@@ -68,12 +80,17 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'a\nbc\nro//readonly\nro//readonly\nabc\n',
+            text: 'a\nbc\nro\nro\nabc\n',
             //        \ cursor
             selection: TextSelection.collapsed(offset: 2),
           ),
           reason: 'Type - OK',
         );
+        expect(
+          controller.fullText,
+          'a\nbc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Type - OK',
+        );
       },
     );
 
@@ -86,7 +103,7 @@ void main() {
         );
         final focusNode = FocusNode();
 
-        await wt.pumpWidget(_createApp(controller, focusNode));
+        await wt.pumpWidget(createApp(controller, focusNode));
         focusNode.requestFocus();
 
         // Go to the beginning.
@@ -99,27 +116,37 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\nabc\n',
-            //                                   cursor /
+            text: 'abc\nro\nro\nabc\n',
+            //               cursor /
             selection: TextSelection.collapsed(
-              offset: 34,
+              offset: 14,
               affinity: TextAffinity.upstream,
             ),
           ),
           reason: 'Delete - no effect',
         );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Delete - no effect',
+        );
 
         await wt.sendKeyEvent(LogicalKeyboardKey.backspace);
 
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\nabc',
-            //                                 cursor /
-            selection: TextSelection.collapsed(offset: 33),
+            text: 'abc\nro\nro\nabc',
+            //             cursor /
+            selection: TextSelection.collapsed(offset: 13),
           ),
           reason: 'Backspace - OK',
         );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabc',
+          reason: 'Backspace - OK',
+        );
 
         // TODO(alexeyinkin): Simulate keyboard entry, https://github.com/akvelon/flutter-code-editor/issues/30
         controller.value = controller.value.replacedSelection('d\n');
@@ -127,12 +154,17 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\nabcd\n',
-            //                                    cursor /
-            selection: TextSelection.collapsed(offset: 35),
+            text: 'abc\nro\nro\nabcd\n',
+            //                cursor /
+            selection: TextSelection.collapsed(offset: 15),
           ),
           reason: 'Type - OK',
         );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabcd\n',
+          reason: 'Type - OK',
+        );
       },
     );
 
@@ -145,7 +177,7 @@ void main() {
         );
         final focusNode = FocusNode();
 
-        await wt.pumpWidget(_createApp(controller, focusNode));
+        await wt.pumpWidget(createApp(controller, focusNode));
         focusNode.requestFocus();
 
         // Go to the end of the editable line before a read-only line.
@@ -162,24 +194,34 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\nabc\n',
+            text: 'abc\nro\nro\nabc\n',
             //        \ cursor
             selection: TextSelection.collapsed(offset: 3),
           ),
           reason: 'Delete EOL before readonly - No effect',
         );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Delete EOL before readonly - No effect',
+        );
 
         await wt.sendKeyEvent(LogicalKeyboardKey.backspace);
 
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'ab\nro//readonly\nro//readonly\nabc\n',
+            text: 'ab\nro\nro\nabc\n',
             //       \ cursor
             selection: TextSelection.collapsed(offset: 2),
           ),
           reason: 'Backspace before EOL before readonly - OK',
         );
+        expect(
+          controller.fullText,
+          'ab\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Backspace before EOL before readonly - OK',
+        );
 
         // TODO(alexeyinkin): Simulate keyboard entry, https://github.com/akvelon/flutter-code-editor/issues/30
         controller.value = controller.value.replacedSelection('c\n');
@@ -187,12 +229,17 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\n\nro//readonly\nro//readonly\nabc\n',
+            text: 'abc\n\nro\nro\nabc\n',
             //          \ cursor
             selection: TextSelection.collapsed(offset: 4),
           ),
           reason: 'Type before EOL before readonly - OK',
         );
+        expect(
+          controller.fullText,
+          'abc\n\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Type before EOL before readonly - OK',
+        );
 
         await wt.sendKeyEvent(LogicalKeyboardKey.arrowRight);
 
@@ -200,16 +247,39 @@ void main() {
         await wt.sendKeyEvent(LogicalKeyboardKey.backspace);
 
         // TODO(alexeyinkin): Simulate keyboard entry, https://github.com/akvelon/flutter-code-editor/issues/30
-        controller.value = controller.value.replacedSelection('y\n');
+        controller.value = controller.value.replacedSelection('y');
 
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\n\nro//readonly\nro//readonly\nabc\n',
+            text: 'abc\nro\nro\nabc\n',
             //          \ cursor
+            selection: TextSelection.collapsed(offset: 4),
+          ),
+          reason: 'Del/Type at start of readonly - No effect, Backspace - OK',
+        );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Del/Type at start of readonly - No effect, Backspace - OK',
+        );
+
+        // TODO(alexeyinkin): Simulate keyboard entry, https://github.com/akvelon/flutter-code-editor/issues/30
+        controller.value = controller.value.replacedSelection('\n');
+
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: 'abc\n\nro\nro\nabc\n',
+            //            \ cursor
             selection: TextSelection.collapsed(offset: 5),
           ),
-          reason: 'Delete Backspace Type at beginning of readonly - No effect',
+          reason: 'Newline at start of readonly - OK',
+        );
+        expect(
+          controller.fullText,
+          'abc\n\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Newline at start of readonly - OK',
         );
       },
     );
@@ -223,7 +293,7 @@ void main() {
         );
         final focusNode = FocusNode();
 
-        await wt.pumpWidget(_createApp(controller, focusNode));
+        await wt.pumpWidget(createApp(controller, focusNode));
         focusNode.requestFocus();
 
         // Go to the beginning of the editable line after a read-only line.
@@ -241,36 +311,76 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\nbc\n',
-            //                              cursor /
-            selection: TextSelection.collapsed(offset: 30),
+            text: 'abc\nro\nro\nbc\n',
+            //          cursor /
+            selection: TextSelection.collapsed(offset: 10),
           ),
           reason: 'Delete at beginning of first editable line - OK',
         );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nbc\n',
+          reason: 'Delete at beginning of first editable line - OK',
+        );
 
         await wt.sendKeyEvent(LogicalKeyboardKey.backspace);
 
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\nbc\n',
-            //                              cursor /
-            selection: TextSelection.collapsed(offset: 30),
+            text: 'abc\nro\nro\nbc\n',
+            //          cursor /
+            selection: TextSelection.collapsed(offset: 10),
           ),
           reason: 'Backspace at beginning of first editable line - No effect',
         );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nbc\n',
+          reason: 'Backspace at beginning of first editable line - No effect',
+        );
 
         // TODO(alexeyinkin): Simulate keyboard entry, https://github.com/akvelon/flutter-code-editor/issues/30
-        controller.value = controller.value.replacedSelection('a\n');
+        controller.value = controller.value.replacedSelection('a');
+        await wt.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
+
+        controller.value = controller.value.replacedSelection('\n');
+
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: 'abc\nro\nro\nabc\n',
+            //          cursor /
+            selection: TextSelection.collapsed(offset: 10),
+          ),
+          reason: 'Type at start of first editable line - OK, Newline - No eff',
+        );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Type at start of first editable line - OK, Newline - No eff',
+        );
+
+        // To the end of the last readonly line.
+        await wt.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
+
+        await wt.sendKeyEvent(LogicalKeyboardKey.delete);
+        await wt.sendKeyEvent(LogicalKeyboardKey.backspace);
+        controller.value = controller.value.replacedSelection('a');
 
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'abc\nro//readonly\nro//readonly\na\nbc\n',
-            //                                 cursor /
-            selection: TextSelection.collapsed(offset: 32),
+            text: 'abc\nro\nro\nabc\n',
+            //        cursor /
+            selection: TextSelection.collapsed(offset: 9),
           ),
-          reason: 'Type at beginning of first editable line - OK',
+          reason: 'Delete, Backspace, Type at last RO EOL - No effect',
+        );
+        expect(
+          controller.fullText,
+          'abc\nro//readonly\nro//readonly\nabc\n',
+          reason: 'Delete, Backspace, Type at last RO EOL - No effect',
         );
       },
     );
@@ -284,7 +394,7 @@ void main() {
         );
         final focusNode = FocusNode();
 
-        await wt.pumpWidget(_createApp(controller, focusNode));
+        await wt.pumpWidget(createApp(controller, focusNode));
         focusNode.requestFocus();
 
         // Go to the beginning.
@@ -296,17 +406,23 @@ void main() {
         await wt.sendKeyEvent(LogicalKeyboardKey.delete);
 
         // TODO(alexeyinkin): Simulate keyboard entry, https://github.com/akvelon/flutter-code-editor/issues/30
-        controller.value = controller.value.replacedSelection('a\n');
+        controller.value = controller.value.replacedSelection('a');
+        controller.value = controller.value.replacedSelection('\n');
 
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'ro//readonly\nabc\nro//readonly\n',
+            text: 'ro\nabc\nro\n',
             //     \ cursor
             selection: TextSelection.collapsed(offset: 0),
           ),
           reason: 'Backspace Delete Type - No effect',
         );
+        expect(
+          controller.fullText,
+          'ro//readonly\nabc\nro//readonly\n',
+          reason: 'Backspace Delete Type - No effect',
+        );
       },
     );
 
@@ -319,7 +435,7 @@ void main() {
         );
         final focusNode = FocusNode();
 
-        await wt.pumpWidget(_createApp(controller, focusNode));
+        await wt.pumpWidget(createApp(controller, focusNode));
         focusNode.requestFocus();
 
         // Go to the end.
@@ -336,12 +452,17 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'ro//readonly\nabc\nro//readonly\n',
-            //                              cursor /
-            selection: TextSelection.collapsed(offset: 30, affinity: TextAffinity.upstream),
+            text: 'ro\nabc\nro\n',
+            //          cursor /
+            selection: TextSelection.collapsed(offset: 10, affinity: TextAffinity.upstream),
           ),
           reason: 'Backspace Delete Type at last empty line - No effect',
         );
+        expect(
+          controller.fullText,
+          'ro//readonly\nabc\nro//readonly\n',
+          reason: 'Backspace Delete Type at last empty line - No effect',
+        );
 
         await wt.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
 
@@ -354,24 +475,18 @@ void main() {
         expect(
           controller.value,
           const TextEditingValue(
-            text: 'ro//readonly\nabc\nro//readonly\n',
-            //                            cursor /
-            selection: TextSelection.collapsed(offset: 29),
+            text: 'ro\nabc\nro\n',
+            //        cursor /
+            selection: TextSelection.collapsed(offset: 9),
           ),
           reason: 'Backspace Delete Type before last empty line - No effect',
         );
+        expect(
+          controller.fullText,
+          'ro//readonly\nabc\nro//readonly\n',
+          reason: 'Backspace Delete Type before last empty line - No effect',
+        );
       },
     );
   });
 }
-
-MaterialApp _createApp(CodeController controller, FocusNode focusNode) {
-  return MaterialApp(
-    home: Scaffold(
-      body: CodeField(
-        controller: controller,
-        focusNode: focusNode,
-      ),
-    ),
-  );
-}
diff --git a/test/src/code_field/span_builder_test.dart b/test/src/code_field/span_builder_test.dart
new file mode 100644
index 00000000..12471551
--- /dev/null
+++ b/test/src/code_field/span_builder_test.dart
@@ -0,0 +1,210 @@
+import 'package:code_text_field/code_text_field.dart';
+import 'package:code_text_field/src/code_field/span_builder.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/highlight_core.dart';
+import 'package:highlight/languages/java.dart';
+
+const _comment = TextStyle(color: Color(0x00000001));
+const _keyword = TextStyle(color: Color(0x00000002));
+const _class = TextStyle(color: Color(0x00000003));
+const _title = TextStyle(color: Color(0x00000004));
+const _function = TextStyle(color: Color(0x00000005));
+const _params = TextStyle(color: Color(0x00000006));
+
+final _themeData = CodeThemeData(
+  classStyle: _class,
+  commentStyle: _comment,
+  functionStyle: _function,
+  keywordStyle: _keyword,
+  titleStyle: _title,
+  paramsStyle: _params,
+);
+
+void main() {
+  group('SpanBuilder', () {
+    test('builds highlighted', () {
+      final examples = [
+        //
+        _Example(
+          'Ordinary comments are visible',
+          text: '''
+public class MyClass {
+  public void main() { // comment
+  }
+}
+''',
+          mode: java,
+          expected: const TextSpan(
+            children: [
+              TextSpan(text: ''),
+              TextSpan(
+                style: _keyword,
+                children: [TextSpan(text: 'public')],
+              ),
+              TextSpan(text: ' '),
+              TextSpan(
+                style: _class,
+                children: [
+                  TextSpan(text: ''),
+                  TextSpan(
+                    style: _keyword,
+                    children: [TextSpan(text: 'class')],
+                  ),
+                  TextSpan(text: ' '),
+                  TextSpan(
+                    style: _title,
+                    children: [TextSpan(text: 'MyClass')],
+                  ),
+                  TextSpan(text: ' '),
+                ],
+              ),
+              TextSpan(text: '{\n  '),
+              TextSpan(
+                style: _function,
+                children: [
+                  TextSpan(text: ''),
+                  TextSpan(
+                    style: _keyword,
+                    children: [TextSpan(text: 'public')],
+                  ),
+                  TextSpan(text: ' '),
+                  TextSpan(
+                    style: _keyword,
+                    children: [TextSpan(text: 'void')],
+                  ),
+                  TextSpan(text: ' '),
+                  TextSpan(
+                    style: _title,
+                    children: [TextSpan(text: 'main')],
+                  ),
+                  TextSpan(text: ''),
+                  TextSpan(
+                    style: _params,
+                    children: [TextSpan(text: '()')],
+                  ),
+                  TextSpan(text: ' '),
+                ],
+              ),
+              TextSpan(text: '{ '),
+              TextSpan(
+                style: _comment,
+                children: [TextSpan(text: '// comment')],
+              ),
+              TextSpan(text: '\n  }\n}\n'),
+            ],
+          ),
+        ),
+
+        _Example(
+          'Service comments are hidden',
+          text: '''
+public class MyClass {
+  public void main() { // readonly
+  }
+}
+''',
+          mode: java,
+          expected: const TextSpan(
+            children: [
+              TextSpan(text: ''),
+              TextSpan(
+                style: _keyword,
+                children: [TextSpan(text: 'public')],
+              ),
+              TextSpan(text: ' '),
+              TextSpan(
+                style: _class,
+                children: [
+                  TextSpan(text: ''),
+                  TextSpan(
+                    style: _keyword,
+                    children: [TextSpan(text: 'class')],
+                  ),
+                  TextSpan(text: ' '),
+                  TextSpan(
+                    style: _title,
+                    children: [TextSpan(text: 'MyClass')],
+                  ),
+                  TextSpan(text: ' '),
+                ],
+              ),
+              TextSpan(text: '{\n  '),
+              TextSpan(
+                style: _function,
+                children: [
+                  TextSpan(text: ''),
+                  TextSpan(
+                    style: _keyword,
+                    children: [TextSpan(text: 'public')],
+                  ),
+                  TextSpan(text: ' '),
+                  TextSpan(
+                    style: _keyword,
+                    children: [TextSpan(text: 'void')],
+                  ),
+                  TextSpan(text: ' '),
+                  TextSpan(
+                    style: _title,
+                    children: [TextSpan(text: 'main')],
+                  ),
+                  TextSpan(text: ''),
+                  TextSpan(
+                    style: _params,
+                    children: [TextSpan(text: '()')],
+                  ),
+                  TextSpan(text: ' '),
+                ],
+              ),
+              TextSpan(text: '{ '),
+              TextSpan(
+                style: _comment,
+                children: [TextSpan(text: '')],
+              ),
+              TextSpan(text: '\n  }\n}\n'),
+            ],
+          ),
+        ),
+      ];
+
+      for (final example in examples) {
+        Result? highlighted;
+
+        final mode = example.mode;
+        if (mode != null) {
+          highlight.registerLanguage('language', mode);
+          highlighted = highlight.parse(example.text, language: 'language');
+        }
+
+        final code = Code(
+          text: example.text,
+          highlighted: highlighted,
+          language: mode,
+        );
+
+        final builder = SpanBuilder();
+        final result = builder.build(code: code, theme: _themeData);
+
+        expect(
+          result,
+          example.expected,
+          reason: example.name,
+        );
+      }
+    });
+  });
+}
+
+class _Example {
+  final String name;
+  final String text;
+  final Mode? mode;
+  final TextSpan expected;
+
+  _Example(
+    this.name, {
+    required this.text,
+    required this.mode,
+    required this.expected,
+  });
+}
diff --git a/test/src/common/create_app.dart b/test/src/common/create_app.dart
new file mode 100644
index 00000000..2b1d981f
--- /dev/null
+++ b/test/src/common/create_app.dart
@@ -0,0 +1,13 @@
+import 'package:code_text_field/code_text_field.dart';
+import 'package:flutter/material.dart';
+
+MaterialApp createApp(CodeController controller, FocusNode focusNode) {
+  return MaterialApp(
+    home: Scaffold(
+      body: CodeField(
+        controller: controller,
+        focusNode: focusNode,
+      ),
+    ),
+  );
+}
diff --git a/test/src/common/lorem_ipsum.dart b/test/src/common/lorem_ipsum.dart
new file mode 100644
index 00000000..cdd884a7
--- /dev/null
+++ b/test/src/common/lorem_ipsum.dart
@@ -0,0 +1,16 @@
+const loremIpsum = '''
+1 Lorem ipsum dolor sit amet,
+2 consectetur adipiscing elit,
+3 sed do eiusmod tempor incididunt ut labore et dolore magna
+4 aliqua. Ut enim ad minim veniam,
+5 quis nostrud exercitation ullamco laboris nisi
+6 ut aliquip
+7 ex
+
+9 ea commodo consequat.
+10 Duis aute irure dolor in
+11 reprehenderit in voluptate velit esse cillum dolore
+12 eu fugiat nulla pariatur.
+13 Excepteur sint occaecat cupidatat
+14 non proident,
+15 sunt in culpa qui officia deserunt mollit anim id est laborum.''';
diff --git a/test/src/hidden_ranges/hidden_range_test.dart b/test/src/hidden_ranges/hidden_range_test.dart
new file mode 100644
index 00000000..f668e238
--- /dev/null
+++ b/test/src/hidden_ranges/hidden_range_test.dart
@@ -0,0 +1,26 @@
+import 'package:code_text_field/src/hidden_ranges/hidden_range.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+// ignore_for_file: prefer_const_constructors
+
+void main() {
+  group('HiddenText.', () {
+    test('text length = end - stat', () {
+      HiddenRange(start: 0, end: 3, text: '123');
+
+      expect(
+        () => HiddenRange(start: 0, end: 3, text: '12'),
+        throwsAssertionError,
+      );
+    });
+
+    test('start >= 0', () {
+      HiddenRange(start: 0, end: 3, text: '123');
+
+      expect(
+        () => HiddenRange(start: -1, end: 3, text: '1234'),
+        throwsAssertionError,
+      );
+    });
+  });
+}
diff --git a/test/src/hidden_ranges/hidden_ranges_position_test.dart b/test/src/hidden_ranges/hidden_ranges_position_test.dart
new file mode 100644
index 00000000..7beebd27
--- /dev/null
+++ b/test/src/hidden_ranges/hidden_ranges_position_test.dart
@@ -0,0 +1,136 @@
+import 'dart:ui';
+
+import 'package:code_text_field/src/hidden_ranges/hidden_range.dart';
+import 'package:code_text_field/src/hidden_ranges/hidden_ranges.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+final _hiddenRanges = HiddenRanges(
+  ranges: const [
+    //                How many chars hidden by the beginning of this range:
+    HiddenRange(start: 20, end: 23, text: '123'), //                      0
+    HiddenRange(start: 31, end: 38, text: '1234567'), //                  3
+    HiddenRange(start: 38, end: 42, text: '1234'), //                     10
+    HiddenRange(start: 67, end: 91, text: '123456789012345678901234'), // 14
+    HiddenRange(start: 100, end: 101, text: '1'), //                      38
+    HiddenRange(start: 102, end: 103, text: '1'), //                      39
+    HiddenRange(start: 104, end: 105, text: '1'), //                      40
+    HiddenRange(start: 106, end: 107, text: '1'), //                      41
+    HiddenRange(start: 108, end: 109, text: '1'), //                      42
+    HiddenRange(start: 110, end: 111, text: '1'), //                      43
+    HiddenRange(start: 113, end: 123, text: '1234567890'), //             44
+    //                                                                    54
+  ],
+  textLength: 140,
+);
+
+const affinities = [TextAffinity.upstream, TextAffinity.downstream];
+
+void main() {
+  group('HiddenRanges. recoverPosition.', () {
+    test('No ranges - No changes', () {
+      expect(
+        HiddenRanges.empty.recoverPosition(
+          7,
+          placeHiddenRanges: TextAffinity.upstream,
+        ),
+        7,
+      );
+      expect(
+        HiddenRanges.empty.recoverPosition(
+          7,
+          placeHiddenRanges: TextAffinity.downstream,
+        ),
+        7,
+      );
+    });
+
+    test('Recover with visible beginning and end', () {
+      final examples = <int, List<int>>{
+        -1: [-1, -1], //   No selection does not change
+        0: [0, 0], //      Zero before the first range does not change
+
+        19: [19, 19], //   Range 0 just before
+        20: [23, 20], //   Range 0 collapse
+        21: [24, 24], //   Range 0 just after
+
+        27: [30, 30], //   Ranges 1+2 just before
+        28: [42, 31], //   Ranges 1+2 collapse
+        29: [43, 43], //   Ranges 1+2 just after
+
+        52: [66, 66], //   Range 3 just before
+        53: [91, 67], //   Range 3 collapse
+        54: [92, 92], //   Range 3 just after
+
+        61: [99, 99], //   Range 4 just before
+        62: [101, 100], // Range 4 collapse / Range 5 just before
+        63: [103, 102], // Range 4 just after / Range 5 collapse
+        64: [105, 104], // Range 5 just after / Range 6 collapse
+        65: [107, 106], // Range 6 just after / Range 7 collapse
+        66: [109, 108], // Range 7 just after / Range 8 collapse
+        67: [111, 110], // Range 8 just after / Range 9 collapse
+        68: [112, 112], // Range 9 just after
+
+        69: [123, 113], // Range 10 collapse
+
+        70: [124, 124], // After all ranges
+      };
+      int i = 1;
+
+      for (final example in examples.entries) {
+        for (int affinityIndex = 0; affinityIndex < 2; affinityIndex++) {
+          final placeHiddenRanges = affinities[affinityIndex];
+          final expected = example.value[affinityIndex];
+
+          int cutPosition() {
+            return _hiddenRanges.recoverPosition(
+              example.key,
+              placeHiddenRanges: placeHiddenRanges,
+            );
+          }
+
+          final reason = '#$i. $placeHiddenRanges: ${example.key} -> $expected';
+          expect(cutPosition, returnsNormally, reason: reason);
+          expect(cutPosition(), expected, reason: reason);
+        }
+
+        i++;
+      }
+    });
+
+    test('Recover with a single range', () {
+      final hiddenRanges = HiddenRanges(
+        ranges: const [
+          HiddenRange(start: 5, end: 10, text: '12345'),
+        ],
+        textLength: 140,
+      );
+
+      final examples = <int, List<int>>{
+        4: [4, 4],
+        5: [10, 5],
+        6: [11, 11],
+      };
+      int i = 1;
+
+      for (final example in examples.entries) {
+        for (int affinityIndex = 0; affinityIndex < 2; affinityIndex++) {
+          final placeHiddenRanges = affinities[affinityIndex];
+          final expected = example.value[affinityIndex];
+
+          int cutPosition() {
+            return hiddenRanges.recoverPosition(
+              example.key,
+              placeHiddenRanges: placeHiddenRanges,
+            );
+          }
+
+          final reason = '#$i. $placeHiddenRanges: ${example.key} -> $expected';
+          expect(cutPosition, returnsNormally, reason: reason);
+          expect(cutPosition(), expected, reason: reason);
+        }
+
+        i++;
+      }
+    });
+  });
+}
diff --git a/test/src/hidden_ranges/hidden_ranges_test.dart b/test/src/hidden_ranges/hidden_ranges_test.dart
new file mode 100644
index 00000000..8a18d234
--- /dev/null
+++ b/test/src/hidden_ranges/hidden_ranges_test.dart
@@ -0,0 +1,178 @@
+import 'package:code_text_field/src/hidden_ranges/hidden_range.dart';
+import 'package:code_text_field/src/hidden_ranges/hidden_ranges.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/highlight.dart';
+
+const _text = '''
+public class MyClass {
+  public void main() { // comment
+  }
+}
+''';
+
+final _hiddenRanges = HiddenRanges(
+  ranges: const [
+    HiddenRange(start: 1, end: 1, text: ''),
+    HiddenRange(start: 9, end: 11, text: 'as'),
+    HiddenRange(start: 40, end: 61, text: 'n() { // comment\n  }\n'),
+  ],
+  textLength: _text.length,
+);
+
+const _cut = '''
+public cls MyClass {
+  public void mai}
+''';
+
+void main() {
+  group('HiddenRanges.', () {
+    test('Empty ranges are not stored', () {
+      expect(
+        HiddenRanges(
+          ranges: const [
+            HiddenRange(start: 1, end: 4, text: '123'),
+            HiddenRange(start: 7, end: 7, text: ''),
+          ],
+          textLength: _text.length,
+        ),
+        HiddenRanges(
+          ranges: const [
+            HiddenRange(start: 1, end: 4, text: '123'),
+          ],
+          textLength: _text.length,
+        ),
+      );
+    });
+
+    test('Ranges cannot overlap', () {
+      expect(
+        () => HiddenRanges(
+          ranges: const [
+            HiddenRange(start: 0, end: 3, text: '012'),
+            HiddenRange(start: 1, end: 4, text: '123'),
+          ],
+          textLength: _text.length,
+        ),
+        throwsAssertionError,
+      );
+    });
+
+    group('hiddenCharactersBeforeRanges', () {
+      test('0-array if no ranges', () {
+        expect(
+          HiddenRanges(
+            ranges: const [],
+            textLength: 5,
+          ).hiddenCharactersBeforeRanges,
+          const [0],
+        );
+        expect(
+          HiddenRanges.empty.hiddenCharactersBeforeRanges,
+          const [0],
+        );
+      });
+
+      test('Valid array if has ranges', () {
+        final ranges = HiddenRanges(
+          ranges: const [
+            HiddenRange(start: 2, end: 4, text: '23'),
+            HiddenRange(start: 7, end: 9, text: '78'),
+            HiddenRange(start: 10, end: 13, text: 'ABC'),
+          ],
+          textLength: 777,
+        );
+
+        expect(ranges.hiddenCharactersBeforeRanges, [0, 2, 4, 7]);
+      });
+    });
+
+    test('cutString', () {
+      final examples = [
+        _CutStringExample(
+          'Empty changes nothing (shortcut return)',
+          hiddenTexts: HiddenRanges.empty,
+          text: _text,
+          start: 123,
+          result: _text,
+        ),
+        _CutStringExample(
+          'Cuts full string (break with "rangeIndex < ranges.length" == false)',
+          hiddenTexts: _hiddenRanges,
+          text: _text,
+          start: 0,
+          result: _cut,
+        ),
+        _CutStringExample(
+          'Cuts a substring starting before and ending in a hidden range',
+          hiddenTexts: _hiddenRanges,
+          text: 'la',
+          start: 8,
+          result: 'l',
+        ),
+        _CutStringExample(
+          'Cuts a substring starting in and ending after a hidden range',
+          hiddenTexts: _hiddenRanges,
+          text: 'ss',
+          start: 10,
+          result: 's',
+        ),
+        _CutStringExample(
+          'Cuts a substring not starting or ending in hidden range '
+          '(break with "substringStart < end" == false',
+          hiddenTexts: _hiddenRanges,
+          text: 'class MyClass {',
+          start: 7,
+          result: 'cls MyClass {',
+        ),
+        _CutStringExample(
+          'Cuts a substring starting and ending in 2 hidden ranges '
+          '(break with "substringStart < end" == false',
+          hiddenTexts: _hiddenRanges,
+          text: 'ss MyClass {\n  public void main() { // com',
+          start: 10,
+          result: 's MyClass {\n  public void mai',
+        ),
+      ];
+
+      for (final example in examples) {
+        final result = example.hiddenTexts.cutString(
+          example.text,
+          start: example.start,
+        );
+
+        expect(result, example.result, reason: example.name);
+      }
+    });
+
+    group('cutHighlighted.', () {
+      test('Cuts inner text for hidden parts, keeps the tags', () {
+        final highlighted = highlight.parse(_text, language: 'java');
+
+        final result = _hiddenRanges.cutHighlighted(highlighted);
+        final html = result.toHtml();
+
+        expect(
+          html,
+          '<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">cls</span> <span class="hljs-title">MyClass</span> </span>{\n'
+          '  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">mai</span><span class="hljs-params"></span></span><span class="hljs-comment"></span>}\n',
+        );
+      });
+    });
+  });
+}
+
+class _CutStringExample {
+  final String name;
+  final HiddenRanges hiddenTexts;
+  final String text;
+  final int start;
+  final String result;
+
+  _CutStringExample(
+    this.name, {
+    required this.hiddenTexts,
+    required this.text,
+    required this.start,
+    required this.result,
+  });
+}
diff --git a/test/src/named_sections/parsers/brackets_start_end_test.dart b/test/src/named_sections/parsers/brackets_start_end_test.dart
index 9e870c75..7f07e49a 100644
--- a/test/src/named_sections/parsers/brackets_start_end_test.dart
+++ b/test/src/named_sections/parsers/brackets_start_end_test.dart
@@ -4,7 +4,7 @@ import 'package:code_text_field/src/single_line_comments/single_line_comment.dar
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
-  test('BracketsStartEndNamedSectionParser', () {
+  test('BracketsStartEndNamedSectionParser. Parses comments', () {
     const comments = [
       //
       SingleLineComment(lineIndex: 13, innerContent: ' no named section'),
@@ -65,16 +65,13 @@ void main() {
 
       SingleLineComment(lineIndex: -3, innerContent: '[START before0]'),
       SingleLineComment(lineIndex: -2, innerContent: '[END before0]'),
-      SingleLineComment(lineIndex: 98, innerContent: '[START after_eof]'),
-      SingleLineComment(lineIndex: 99, innerContent: '[END after_eof]'),
     ];
 
     const lineCount = 95;
-    const last = lineCount - 1;
 
     const expected = [
       NamedSection(startLine: 0, endLine: 14, name: 'never_started'),
-      NamedSection(startLine: 11, endLine: last, name: 'never_ending'),
+      NamedSection(startLine: 11, endLine: null, name: 'never_ending'),
       NamedSection(startLine: 16, endLine: 21, name: 'outer'),
       NamedSection(startLine: 18, endLine: 21, name: 'inner'),
       NamedSection(startLine: 24, endLine: 35, name: 'overlapping1'),
@@ -88,14 +85,13 @@ void main() {
       NamedSection(startLine: 80, endLine: 80, name: 'line'),
       NamedSection(startLine: 81, endLine: 81, name: 'swap'),
       NamedSection(startLine: 82, endLine: 82, name: 'split_line'),
-      NamedSection(startLine: 83, endLine: last, name: '2'),
-      NamedSection(startLine: 84, endLine: last, name: '_'),
+      NamedSection(startLine: 83, endLine: null, name: '2'),
+      NamedSection(startLine: 84, endLine: null, name: '_'),
     ];
 
     const parser = BracketsStartEndNamedSectionParser();
     final parsed = parser.parse(
       singleLineComments: comments,
-      lineCount: lineCount,
     );
 
     expect(parsed, expected);
diff --git a/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart b/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart
index 4058a21e..4464bf2f 100644
--- a/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart
+++ b/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: avoid_redundant_argument_values
+
 import 'package:code_text_field/src/single_line_comments/parser/highlight_single_line_comment_parser.dart';
 import 'package:code_text_field/src/single_line_comments/parser/single_line_comments.dart';
 import 'package:code_text_field/src/single_line_comments/single_line_comment.dart';
@@ -16,8 +18,8 @@ void main() {
       // ==================================
 
       _Example(
+        'Java. Parses unquoted sequences',
         language: java,
-        name: 'Java. Parses unquoted sequences',
         text: '''
 // slashed comment1 
 /* multi
@@ -29,11 +31,13 @@ public class MyClass {
         comments: const [
           SingleLineComment(
             lineIndex: 0,
+            characterIndex: 0,
             innerContent: ' slashed comment1 ',
             outerContent: '// slashed comment1 ', // note the trailing space.
           ),
           SingleLineComment(
             lineIndex: 4,
+            characterIndex: 68,
             innerContent: ' slashed comment2 // still comment2',
             outerContent: '// slashed comment2 // still comment2',
           ),
@@ -41,8 +45,8 @@ public class MyClass {
       ),
 
       _Example(
+        'Java. Ignores quoted sequences',
         language: java,
-        name: 'Java. Ignores quoted sequences',
         text: '''
 /*
  // not a comment
@@ -55,6 +59,7 @@ public class MyClass { // comment
         comments: const [
           SingleLineComment(
             lineIndex: 3,
+            characterIndex: 49,
             innerContent: ' comment',
             outerContent: '// comment',
           ),
@@ -62,8 +67,8 @@ public class MyClass { // comment
       ),
 
       _Example(
+        'Java. Breaks on single-quote multiline strings',
         language: java,
-        name: 'Java. Breaks on single-quote multiline strings',
         text: '''
 public class MyClass { // comment
   private string str2 = \'''
@@ -75,8 +80,8 @@ public class MyClass { // comment
       ),
 
       _Example(
+        'Java. Breaks on double-quote multiline strings',
         language: java,
-        name: 'Java. Breaks on double-quote multiline strings',
         text: '''
 public class MyClass { // comment
   private string str4 = """
@@ -88,8 +93,8 @@ public class MyClass { // comment
       ),
 
       _Example(
+        'Java. Breaks on number sign',
         language: java,
-        name: 'Java. Breaks on number sign',
         // https://github.com/git-touch/highlight.dart/issues/36
         text: '''
 // slashed comment1
@@ -105,8 +110,8 @@ text # not a comment
       // ==================================
 
       _Example(
+        'Go. Parses unquoted sequences',
         language: go,
-        name: 'Go. Parses unquoted sequences',
         text: '''
 // slashed comment1 
 /* multi
@@ -118,11 +123,13 @@ public class MyClass {
         comments: const [
           SingleLineComment(
             lineIndex: 0,
+            characterIndex: 0,
             innerContent: ' slashed comment1 ',
             outerContent: '// slashed comment1 ', // note the trailing space.
           ),
           SingleLineComment(
             lineIndex: 4,
+            characterIndex: 68,
             innerContent: ' slashed comment2 // still comment2',
             outerContent: '// slashed comment2 // still comment2',
           ),
@@ -130,8 +137,8 @@ public class MyClass {
       ),
 
       _Example(
+        'Go. Ignores quoted sequences',
         language: go,
-        name: 'Go. Ignores quoted sequences',
         text: '''
 /*
  // not a comment
@@ -148,6 +155,7 @@ public class MyClass { // comment
         comments: const [
           SingleLineComment(
             lineIndex: 3,
+            characterIndex: 49,
             innerContent: ' comment',
             outerContent: '// comment',
           ),
@@ -159,8 +167,8 @@ public class MyClass { // comment
       // ==================================
 
       _Example(
+        'Python. Parses unquoted sequences',
         language: python,
-        name: 'Python. Parses unquoted sequences',
         text: '''
 # hash comment1 
 def fn():
@@ -169,11 +177,13 @@ def fn():
         comments: const [
           SingleLineComment(
             lineIndex: 0,
+            characterIndex: 0,
             innerContent: ' hash comment1 ',
             outerContent: '# hash comment1 ', // note the trailing space.
           ),
           SingleLineComment(
             lineIndex: 2,
+            characterIndex: 34,
             innerContent: ' hash comment2 # still comment2',
             outerContent: '# hash comment2 # still comment2',
           ),
@@ -181,8 +191,8 @@ def fn():
       ),
 
       _Example(
+        'Python. Ignores quoted sequences',
         language: python,
-        name: 'Python. Ignores quoted sequences',
         text: '''
 def fn(): # comment
   str1 = '# not a comment';
@@ -197,6 +207,7 @@ def fn(): # comment
         comments: const [
           SingleLineComment(
             lineIndex: 0,
+            characterIndex: 10,
             innerContent: ' comment',
             outerContent: '# comment',
           ),
@@ -204,8 +215,8 @@ def fn(): # comment
       ),
 
       _Example(
+        'Python. Breaks on missing colon',
         language: python,
-        name: 'Python. Breaks on missing colon',
         text: '''
 def fn() # comment
 ''',
@@ -217,8 +228,8 @@ def fn() # comment
       // ==================================
 
       _Example(
+        'Scala. Parses unquoted sequences',
         language: scala,
-        name: 'Scala. Parses unquoted sequences',
         text: '''
 // slashed comment1 
 /* multi
@@ -230,11 +241,13 @@ public class MyClass {
         comments: const [
           SingleLineComment(
             lineIndex: 0,
+            characterIndex: 0,
             innerContent: ' slashed comment1 ',
             outerContent: '// slashed comment1 ', // note the trailing space.
           ),
           SingleLineComment(
             lineIndex: 4,
+            characterIndex: 68,
             innerContent: ' slashed comment2 // still comment2',
             outerContent: '// slashed comment2 // still comment2',
           ),
@@ -242,8 +255,8 @@ public class MyClass {
       ),
 
       _Example(
+        'Scala. Ignores quoted sequences',
         language: scala,
-        name: 'Scala. Ignores quoted sequences',
         text: '''
 /*
  // not a comment
@@ -255,6 +268,7 @@ public class MyClass { // comment
         comments: const [
           SingleLineComment(
             lineIndex: 3,
+            characterIndex: 49,
             innerContent: ' comment',
             outerContent: '// comment',
           ),
@@ -262,8 +276,8 @@ public class MyClass { // comment
       ),
 
       _Example(
+        'Scala. Breaks on multiline strings',
         language: scala,
-        name: 'Scala. Breaks on multiline strings',
         text: '''
 val str4 = """
            // not a comment
@@ -301,8 +315,8 @@ class _Example {
   final List<SingleLineComment> comments;
   final bool isLanguageLost;
 
-  _Example({
-    required this.name,
+  _Example(
+    this.name, {
     required this.text,
     required this.language,
     this.comments = const [],
diff --git a/test/src/single_line_comment/parser/single_line_comment_parser_test.dart b/test/src/single_line_comment/parser/single_line_comment_parser_test.dart
index bb502934..1c2dfbbc 100644
--- a/test/src/single_line_comment/parser/single_line_comment_parser_test.dart
+++ b/test/src/single_line_comment/parser/single_line_comment_parser_test.dart
@@ -27,8 +27,8 @@ public class MyClass {
     test('when type highlight fails, falls back to text', () {
       final examples = [
         _Example(
+          'Java. Highlight',
           language: java,
-          name: 'Java. Highlight',
           parserType: HighlightSingleLineCommentParser,
           text: '''
 // slashed comment1 
@@ -43,8 +43,8 @@ public class MyClass {
         // If the number sign bug gets fixed, find another example
         // that breaks highlighting.
         _Example(
+          'Java. Text with number sign',
           language: java,
-          name: 'Java. Text with number sign',
           parserType: TextSingleLineCommentParser,
           text: '''
 // slashed comment1
@@ -75,13 +75,13 @@ text # not a comment
 class _Example {
   final Mode language;
   final String name;
-  final String text;
   final Type parserType;
+  final String text;
 
-  _Example({
+  _Example(
+    this.name, {
     required this.language,
-    required this.name,
-    required this.text,
     required this.parserType,
+    required this.text,
   });
 }
diff --git a/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart b/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart
index 8ee461ff..386412d4 100644
--- a/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart
+++ b/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart
@@ -1,10 +1,12 @@
+// ignore_for_file: avoid_redundant_argument_values
+
 import 'package:code_text_field/src/single_line_comments/parser/text_single_line_comment_parser.dart';
 import 'package:code_text_field/src/single_line_comments/single_line_comment.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
   group('TextSingleLineCommentParser', () {
-    test('grabs everything at sequence and on', () {
+    test('grabs everything at the sequence and on', () {
       const sequences = ['//', '~'];
       const text = '''
 // slashed comment1 
@@ -18,31 +20,37 @@ text ~ tilde comment3
       const expected = [
         SingleLineComment(
           lineIndex: 0,
+          characterIndex: 0,
           innerContent: ' slashed comment1 ',
           outerContent: '// slashed comment1 ',
         ),
         SingleLineComment(
           lineIndex: 1,
+          characterIndex: 28,
           innerContent: ' slashed comment2 // still comment2',
           outerContent: '// slashed comment2 // still comment2',
         ),
         SingleLineComment(
           lineIndex: 4,
+          characterIndex: 97,
           innerContent: ' tilde comment3',
           outerContent: '~ tilde comment3',
         ),
         SingleLineComment(
           lineIndex: 5,
+          characterIndex: 122,
           innerContent: ' quotes ~ line\'',
           outerContent: '// quotes ~ line\'',
         ),
         SingleLineComment(
           lineIndex: 6,
+          characterIndex: 148,
           innerContent: ' quotes // line"',
           outerContent: '~ quotes // line"',
         ),
         SingleLineComment(
           lineIndex: 7,
+          characterIndex: 175,
           innerContent: 'line comment */',
           outerContent: '//line comment */',
         ),