1414#include < nfd.h>
1515
1616#include < cstdio>
17+ #include < cmath>
1718#include < fstream>
1819#include < sstream>
1920#include < filesystem>
@@ -48,6 +49,7 @@ static void settings_save(const AppSettings& s, const std::string& path) {
4849 f << " \" show_status_bar\" : " << (s.show_status_bar ? " true" : " false" ) << " ,\n " ;
4950 f << " \" word_wrap\" : " << (s.word_wrap ? " true" : " false" ) << " ,\n " ;
5051 f << " \" show_line_numbers\" : " << (s.show_line_numbers ? " true" : " false" ) << " ,\n " ;
52+ f << " \" show_spaces\" : " << (s.show_spaces ? " true" : " false" ) << " ,\n " ;
5153 f << " \" tab_size\" : " << s.tab_size << " ,\n " ;
5254 f << " \" font_size\" : " << s.font_size << " ,\n " ;
5355 f << " \" font_name\" : \" " << json_escape (s.font_name ) << " \" ,\n " ;
@@ -98,6 +100,7 @@ static void settings_load(AppSettings& s, const std::string& path) {
98100 s.show_status_bar = get_bool (" show_status_bar" , true );
99101 s.word_wrap = get_bool (" word_wrap" , false );
100102 s.show_line_numbers = get_bool (" show_line_numbers" , true );
103+ s.show_spaces = get_bool (" show_spaces" , false );
101104 s.tab_size = get_int (" tab_size" , 4 );
102105 s.font_size = get_int (" font_size" , 16 );
103106 s.font_name = get_str (" font_name" );
@@ -221,7 +224,7 @@ void EditorApp::init() {
221224 }
222225
223226 ed->SetTabSize (settings_.tab_size );
224- ed->SetShowWhitespaces (false );
227+ ed->SetShowWhitespaces (settings_. show_spaces );
225228
226229 apply_zoom (tab_idx);
227230 }
@@ -600,13 +603,13 @@ void EditorApp::replace_all() {
600603// ============================================================================
601604void EditorApp::zoom_in () {
602605 if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
603- tabs_[active_tab_].zoom_pct = std::min (200 , tabs_[active_tab_].zoom_pct + 10 );
606+ tabs_[active_tab_].zoom_pct = ( std::min) (200 , tabs_[active_tab_].zoom_pct + 10 );
604607 apply_zoom (active_tab_);
605608}
606609
607610void EditorApp::zoom_out () {
608611 if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
609- tabs_[active_tab_].zoom_pct = std::max (50 , tabs_[active_tab_].zoom_pct - 10 );
612+ tabs_[active_tab_].zoom_pct = ( std::max) (50 , tabs_[active_tab_].zoom_pct - 10 );
610613 apply_zoom (active_tab_);
611614}
612615
@@ -636,8 +639,13 @@ void EditorApp::toggle_word_wrap() {
636639
637640void EditorApp::toggle_line_numbers () {
638641 settings_.show_line_numbers = !settings_.show_line_numbers ;
639- // Apply to all tabs - TextEditor always shows line numbers, but we can note the setting
640- // The actual rendering will be controlled by our wrapper in render_editor_area
642+ }
643+
644+ void EditorApp::toggle_spaces () {
645+ settings_.show_spaces = !settings_.show_spaces ;
646+ for (auto & tab : tabs_) {
647+ tab.editor ->SetShowWhitespaces (settings_.show_spaces );
648+ }
641649}
642650
643651void EditorApp::toggle_theme () {
@@ -664,12 +672,85 @@ void EditorApp::set_tab_size(int size) {
664672 }
665673}
666674
675+ // ============================================================================
676+ // Bookmarks
677+ // ============================================================================
678+ void EditorApp::toggle_bookmark (int line) {
679+ if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
680+ auto & bookmarks = tabs_[active_tab_].bookmarks ;
681+ auto it = std::find (bookmarks.begin (), bookmarks.end (), line);
682+ if (it != bookmarks.end ()) {
683+ bookmarks.erase (it);
684+ } else {
685+ bookmarks.push_back (line);
686+ std::sort (bookmarks.begin (), bookmarks.end ());
687+ }
688+ }
689+
690+ void EditorApp::next_bookmark () {
691+ if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
692+ auto & bookmarks = tabs_[active_tab_].bookmarks ;
693+ if (bookmarks.empty ()) return ;
694+ auto pos = tabs_[active_tab_].editor ->GetCursorPosition ().mLine ;
695+ auto it = std::upper_bound (bookmarks.begin (), bookmarks.end (), pos);
696+ if (it != bookmarks.end ()) {
697+ tabs_[active_tab_].editor ->SetCursorPosition (TextEditor::Coordinates (*it, 0 ));
698+ } else if (!bookmarks.empty ()) {
699+ tabs_[active_tab_].editor ->SetCursorPosition (TextEditor::Coordinates (bookmarks[0 ], 0 ));
700+ }
701+ }
702+
703+ void EditorApp::prev_bookmark () {
704+ if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
705+ auto & bookmarks = tabs_[active_tab_].bookmarks ;
706+ if (bookmarks.empty ()) return ;
707+ auto pos = tabs_[active_tab_].editor ->GetCursorPosition ().mLine ;
708+ auto it = std::lower_bound (bookmarks.begin (), bookmarks.end (), pos);
709+ if (it != bookmarks.begin ()) {
710+ --it;
711+ tabs_[active_tab_].editor ->SetCursorPosition (TextEditor::Coordinates (*it, 0 ));
712+ } else {
713+ tabs_[active_tab_].editor ->SetCursorPosition (TextEditor::Coordinates (bookmarks.back (), 0 ));
714+ }
715+ }
716+
717+ void EditorApp::clear_bookmarks () {
718+ if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
719+ tabs_[active_tab_].bookmarks .clear ();
720+ }
721+
722+ // ============================================================================
723+ // Change History
724+ // ============================================================================
725+ void EditorApp::update_change_history () {
726+ if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
727+ auto & tab = tabs_[active_tab_];
728+ int current_lines = tab.editor ->GetTotalLines ();
729+
730+ if (tab.last_saved_line_count == 0 ) {
731+ tab.last_saved_line_count = current_lines;
732+ return ;
733+ }
734+
735+ if (current_lines > tab.last_saved_line_count ) {
736+ for (int i = tab.last_saved_line_count ; i < current_lines; i++) {
737+ tab.changed_lines .push_back (i);
738+ }
739+ }
740+ tab.last_saved_line_count = current_lines;
741+ }
742+
743+ void EditorApp::clear_change_history () {
744+ if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
745+ tabs_[active_tab_].changed_lines .clear ();
746+ }
747+
667748void EditorApp::rebuild_fonts () {
668- // For now, we just update the global font scale
669- // In a full implementation, you would reload fonts from ImGui
670- ImGui::GetIO ().FontGlobalScale = settings_. font_size / 16 . 0f ;
749+ // Dynamic font change - apply immediately to all tabs
750+ float scale = settings_. font_size / 16 . 0f ;
751+ ImGui::GetIO ().FontGlobalScale = scale ;
671752
672- // Apply to all tabs via zoom
753+ // Mark all tabs as needing re-render
673754 for (int i = 0 ; i < (int )tabs_.size (); i++) {
674755 apply_zoom (i);
675756 }
@@ -713,7 +794,12 @@ void EditorApp::render() {
713794 if (ImGui::IsKeyPressed (ImGuiKey_F)) { show_find_ = true ; return ; }
714795 if (ImGui::IsKeyPressed (ImGuiKey_H)) { show_replace_ = true ; return ; }
715796 if (ImGui::IsKeyPressed (ImGuiKey_G)) { show_goto_ = true ; return ; }
716- if (ImGui::IsKeyPressed (ImGuiKey_A)) { /* Select All — handled by TextEditor */ }
797+ if (ImGui::IsKeyPressed (ImGuiKey_A)) {
798+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
799+ tabs_[active_tab_].editor ->SelectAll ();
800+ }
801+ return ;
802+ }
717803 }
718804 if (io.KeyCtrl && io.KeyShift && !io.KeyAlt ) {
719805 if (ImGui::IsKeyPressed (ImGuiKey_N)) { new_window (); return ; }
@@ -726,8 +812,14 @@ void EditorApp::render() {
726812 }
727813 if (!io.KeyCtrl && !io.KeyShift && !io.KeyAlt ) {
728814 if (ImGui::IsKeyPressed (ImGuiKey_F3)) { find_next (); return ; }
815+ if (ImGui::IsKeyPressed (ImGuiKey_F2)) {
816+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
817+ auto pos = tabs_[active_tab_].editor ->GetCursorPosition ();
818+ toggle_bookmark (pos.mLine );
819+ }
820+ return ;
821+ }
729822 if (ImGui::IsKeyPressed (ImGuiKey_F5)) {
730- // Insert time/date
731823 if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
732824 auto now = std::chrono::system_clock::now ();
733825 auto t = std::chrono::system_clock::to_time_t (now);
@@ -807,14 +899,10 @@ void EditorApp::render_menu_file() {
807899 open_file (settings_.recent_files [i]);
808900 }
809901 }
810- ImGui::EndMenu ();
902+ ImGui::EndMenu ();
811903 }
812904 }
813905
814- ImGui::Separator ();
815- if (ImGui::MenuItem (" Save" , " Ctrl+S" )) save_tab (active_tab_);
816- if (ImGui::MenuItem (" Save As..." , " Ctrl+Shift+S" )) save_tab_as (active_tab_);
817- if (ImGui::MenuItem (" Save All" , " Ctrl+Alt+S" )) save_all ();
818906 ImGui::Separator ();
819907 // Page Setup — placeholder (requires native print dialog)
820908 if (ImGui::MenuItem (" Page Setup..." )) { /* TODO: native dialog */ }
@@ -829,20 +917,44 @@ void EditorApp::render_menu_file() {
829917
830918void EditorApp::render_menu_edit () {
831919 if (ImGui::BeginMenu (" Edit" )) {
832- if (ImGui::MenuItem (" Undo" , " Ctrl+Z" )) { /* TextEditor handles */ }
920+ if (ImGui::MenuItem (" Undo" , " Ctrl+Z" )) {
921+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
922+ tabs_[active_tab_].editor ->Undo ();
923+ }
924+ }
833925 ImGui::Separator ();
834- if (ImGui::MenuItem (" Cut" , " Ctrl+X" )) { /* TextEditor handles */ }
835- if (ImGui::MenuItem (" Copy" , " Ctrl+C" )) { /* TextEditor handles */ }
836- if (ImGui::MenuItem (" Paste" , " Ctrl+V" )) { /* TextEditor handles */ }
837- if (ImGui::MenuItem (" Delete" , " Del" )) { /* TextEditor handles */ }
926+ if (ImGui::MenuItem (" Cut" , " Ctrl+X" )) {
927+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
928+ tabs_[active_tab_].editor ->Cut ();
929+ }
930+ }
931+ if (ImGui::MenuItem (" Copy" , " Ctrl+C" )) {
932+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
933+ tabs_[active_tab_].editor ->Copy ();
934+ }
935+ }
936+ if (ImGui::MenuItem (" Paste" , " Ctrl+V" )) {
937+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
938+ tabs_[active_tab_].editor ->Paste ();
939+ }
940+ }
941+ if (ImGui::MenuItem (" Delete" , " Del" )) {
942+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
943+ tabs_[active_tab_].editor ->Delete ();
944+ }
945+ }
838946 ImGui::Separator ();
839947 if (ImGui::MenuItem (" Find..." , " Ctrl+F" )) show_find_ = true ;
840948 if (ImGui::MenuItem (" Find Next" , " F3" )) find_next ();
841949 if (ImGui::MenuItem (" Find Previous" , " Shift+F3" )) find_prev ();
842950 if (ImGui::MenuItem (" Replace..." , " Ctrl+H" )) show_replace_ = true ;
843951 if (ImGui::MenuItem (" Go To..." , " Ctrl+G" )) show_goto_ = true ;
844952 ImGui::Separator ();
845- if (ImGui::MenuItem (" Select All" , " Ctrl+A" )) { /* TextEditor handles */ }
953+ if (ImGui::MenuItem (" Select All" , " Ctrl+A" )) {
954+ if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
955+ tabs_[active_tab_].editor ->SelectAll ();
956+ }
957+ }
846958 if (ImGui::MenuItem (" Time/Date" , " F5" )) {
847959 if (active_tab_ >= 0 && active_tab_ < (int )tabs_.size ()) {
848960 auto now = std::chrono::system_clock::now ();
@@ -874,12 +986,14 @@ void EditorApp::render_menu_view() {
874986 if (ImGui::MenuItem (" Word Wrap" , nullptr , &ww)) toggle_word_wrap ();
875987 bool ln = settings_.show_line_numbers ;
876988 if (ImGui::MenuItem (" Line Numbers" , nullptr , &ln)) toggle_line_numbers ();
989+ bool sp = settings_.show_spaces ;
990+ if (ImGui::MenuItem (" Show Spaces" , nullptr , &sp)) toggle_spaces ();
877991 ImGui::Separator ();
878992 if (ImGui::BeginMenu (" Spaces" )) {
879993 if (ImGui::MenuItem (" 2 Spaces" , nullptr , settings_.tab_size == 2 )) set_tab_size (2 );
880994 if (ImGui::MenuItem (" 4 Spaces" , nullptr , settings_.tab_size == 4 )) set_tab_size (4 );
881995 if (ImGui::MenuItem (" 8 Spaces" , nullptr , settings_.tab_size == 8 )) set_tab_size (8 );
882- if (ImGui::MenuItem (" Custom..." )) { show_spaces_dialog_ = true ; tab_size_temp_ = settings_.tab_size ; }
996+ if (ImGui::MenuItem (" Custom..." )) { show_spaces_ = true ; tab_size_temp_ = settings_.tab_size ; }
883997 ImGui::EndMenu ();
884998 }
885999 ImGui::Separator ();
@@ -912,8 +1026,10 @@ void EditorApp::render_editor_area() {
9121026 bool open = true ;
9131027 if (ImGui::BeginTabItem (label.c_str (), &open, tab_item_flags)) {
9141028 active_tab_ = i;
915- TextEditor* editor = tab.editor ;
916- editor->Render (" TextEditor" );
1029+
1030+ // Render gutter with bookmarks and change history
1031+ render_editor_with_margins ();
1032+
9171033 ImGui::EndTabItem ();
9181034 }
9191035 if (!open) {
@@ -927,6 +1043,74 @@ void EditorApp::render_editor_area() {
9271043 ImGui::End ();
9281044}
9291045
1046+ void EditorApp::render_editor_with_margins () {
1047+ if (active_tab_ < 0 || active_tab_ >= (int )tabs_.size ()) return ;
1048+ auto & tab = tabs_[active_tab_];
1049+ TextEditor* editor = tab.editor ;
1050+
1051+ // Calculate gutter width: bookmark margin + line number margin + change history margin
1052+ float gutter_width = 0 .0f ;
1053+ bool has_bookmarks = !tab.bookmarks .empty ();
1054+ bool has_changes = !tab.changed_lines .empty ();
1055+
1056+ if (has_bookmarks || has_changes || settings_.show_line_numbers ) {
1057+ gutter_width = 80 .0f ; // Combined gutter width
1058+ }
1059+
1060+ if (gutter_width > 0 .0f ) {
1061+ ImGui::BeginGroup ();
1062+ ImGui::PushStyleVar (ImGuiStyleVar_ItemSpacing, ImVec2 (0 , 0 ));
1063+
1064+ // Get cursor position to sync scroll
1065+ auto cursor = editor->GetCursorPosition ();
1066+ int total_lines = editor->GetTotalLines ();
1067+
1068+ // Render gutter
1069+ ImGui::PushStyleColor (ImGuiCol_ChildBg, ImVec4 (0 .2f , 0 .2f , 0 .24f , 1 .0f ));
1070+ if (ImGui::BeginChild (" ##Gutter" , ImVec2 (gutter_width, -1 ), false , ImGuiWindowFlags_NoScrollbar)) {
1071+ for (int line = 0 ; line < total_lines; line++) {
1072+ ImGui::PushID (line);
1073+
1074+ // Bookmark column
1075+ bool is_bookmarked = std::find (tab.bookmarks .begin (), tab.bookmarks .end (), line) != tab.bookmarks .end ();
1076+ if (is_bookmarked) {
1077+ ImGui::TextColored (ImVec4 (1 .0f , 0 .8f , 0 .0f , 1 .0f ), " \xE2\x9C\x93 " ); // Checkmark symbol
1078+ } else {
1079+ ImGui::Text (" " );
1080+ }
1081+ ImGui::SameLine ();
1082+
1083+ // Line number column
1084+ ImGui::TextColored (ImVec4 (0 .5f , 0 .5f , 0 .55f , 1 .0f ), " %d" , line + 1 );
1085+ ImGui::SameLine ();
1086+
1087+ // Change history column (highlight modified lines)
1088+ bool is_changed = std::find (tab.changed_lines .begin (), tab.changed_lines .end (), line) != tab.changed_lines .end ();
1089+ if (is_changed) {
1090+ ImGui::TextColored (ImVec4 (0 .3f , 0 .7f , 0 .3f , 1 .0f ), " \xE2\x80\xA2 " ); // Dot symbol
1091+ }
1092+
1093+ ImGui::PopID ();
1094+ }
1095+ }
1096+ ImGui::EndChild ();
1097+ ImGui::PopStyleColor ();
1098+
1099+ ImGui::SameLine ();
1100+ ImGui::PopStyleVar ();
1101+
1102+ // Render the text editor
1103+ ImGui::PushStyleVar (ImGuiStyleVar_ItemSpacing, ImVec2 (0 , 0 ));
1104+ editor->Render (" TextEditor" );
1105+ ImGui::PopStyleVar ();
1106+
1107+ ImGui::EndGroup ();
1108+ } else {
1109+ // No gutter needed, just render editor
1110+ editor->Render (" TextEditor" );
1111+ }
1112+ }
1113+
9301114// ============================================================================
9311115// Status Bar
9321116// ============================================================================
@@ -1097,7 +1281,7 @@ void EditorApp::render_font_dialog() {
10971281 ImGui::Text (" Font Size (8-48):" );
10981282 ImGui::SetNextItemWidth (100 );
10991283 ImGui::InputInt (" ##fontsize" , &font_size_temp_);
1100- font_size_temp_ = std::max (8 , std::min (48 , font_size_temp_));
1284+ font_size_temp_ = ( std::max) (8 , ( std::min) (48 , font_size_temp_));
11011285
11021286 ImGui::Separator ();
11031287 ImGui::TextUnformatted (" Note: Font size changes apply to all tabs." );
@@ -1197,7 +1381,7 @@ void EditorApp::render_spaces_dialog() {
11971381 ImGui::Text (" Tab size (1-16):" );
11981382 ImGui::SetNextItemWidth (100 );
11991383 ImGui::InputInt (" ##tabsize" , &tab_size_temp_);
1200- tab_size_temp_ = std::max (1 , std::min (16 , tab_size_temp_));
1384+ tab_size_temp_ = ( std::max) (1 , ( std::min) (16 , tab_size_temp_));
12011385
12021386 if (ImGui::Button (" Apply" )) {
12031387 set_tab_size (tab_size_temp_);
0 commit comments