Skip to content

Commit 75d0bc8

Browse files
committed
feat: implement bookmarks, change history gutter, File/Edit/View menus with full keyboard shortcuts and dynamic font reload
1 parent 7ded4e1 commit 75d0bc8

3 files changed

Lines changed: 246 additions & 29 deletions

File tree

CMakeLists.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ elseif(PLATFORM_BSD)
175175
endif()
176176

177177
target_compile_options(${PROJECT_NAME} PRIVATE
178-
-Wall -Wextra -Wno-unused-parameter
178+
$<$<CXX_COMPILER_ID:MSVC>:/W4>
179+
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>
180+
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wno-unused-parameter>
179181
)
180182

181183
# ============================================================================
@@ -189,7 +191,10 @@ install(TARGETS ${PROJECT_NAME} DESTINATION bin)
189191
enable_testing()
190192

191193
add_executable(test_view_features tests/test_view_features.cpp)
192-
target_compile_options(test_view_features PRIVATE -Wall -Wextra)
194+
target_compile_options(test_view_features PRIVATE
195+
$<$<CXX_COMPILER_ID:MSVC>:/W4>
196+
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>
197+
)
193198

194199
add_test(NAME ViewFeatures COMMAND test_view_features)
195200

src/editor_app.cpp

Lines changed: 211 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
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
// ============================================================================
601604
void 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

607610
void 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

637640
void 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

643651
void 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+
667748
void 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

830918
void 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

Comments
 (0)