diff --git a/.gitignore b/.gitignore index 56a07d2..ed7a57b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ !include/**/ !source/ !source/**/ +!test/ +!test/**/ !*.* *~/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 59c7f48..205ddcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required (VERSION 3.5) option(TURBO_BUILD_APP "Build main editor application" ON) +option(TURBO_BUILD_TESTS "Build and run tests" OFF) option(TURBO_BUILD_EXAMPLES "Build example apps" OFF) option(TURBO_USE_SYSTEM_DEPS "Use system-wide dependencies instead of submodules (except Turbo Vision)" OFF) option(TURBO_USE_SYSTEM_TVISION "Use system-wide Turbo Vision instead of the submodule" OFF) @@ -150,6 +151,24 @@ if (TURBO_BUILD_APP) install(TARGETS ${TURBO} RUNTIME DESTINATION bin) endif() +# Target 'tests' + +if (TURBO_BUILD_TESTS) + file(GLOB_RECURSE TEST_SRC "${CMAKE_CURRENT_LIST_DIR}/test/*.cc") + message(WARNING ${TEST_SRC}) + add_executable(${TURBO}-test ${TEST_SRC}) + target_compile_features(${TURBO}-test PRIVATE cxx_std_17) + turbo_set_warnings(${TURBO}-test) + find_library(GTEST gtest REQUIRED) + find_library(GTEST_MAIN gtest_main REQUIRED) + target_link_libraries(${TURBO}-test PRIVATE + ${TURBO}-core + ${GTEST} + ${GTEST_MAIN} + ) + add_custom_command(TARGET ${TURBO}-test POST_BUILD COMMAND ${TURBO}-test WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +endif() + # Examples add_subdirectory(source/examples) @@ -164,5 +183,7 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0" AND TURBO_OPTIMIZE_BUILD) if (TURBO_BUILD_APP) set_target_properties(${TURBO} PROPERTIES UNITY_BUILD ON) endif() - + if (TURBO_BUILD_TESTS) + set_target_properties(${TURBO}-test PROPERTIES UNITY_BUILD ON) + endif() endif() diff --git a/README.md b/README.md index ae6eac0..a225d0d 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Some of the default keybindings are: * `Ctrl+X`/`Shift+Del`: cut. * `Ctrl+Z`, `Ctrl+Y`: undo/redo. * `Tab`, `Shift+Tab`: indent/unindent. +* `Ctrl+Q`: toggle comment. * `Ctrl+A`: select all. * `Shift+Arrow`: extend selection. * `Ctrl+F`: find. @@ -89,7 +90,7 @@ Some of the default keybindings are: * `Ctrl+S`: save document. * `Ctrl+W`: close focused document. * `F6`, `Shift+F6`: next/previous document (in MRU order). -* `Alt+X`: exit the application (I prefer this over `Ctrl+Q`, which is too close to `Ctrl+A` and `Ctrl+W`). +* `Alt+X`: exit the application. In environments with extended keyboard support (e.g. the Linux console, Windows or Kitty ≥ 0.20.0), the following key shortcuts may also work: @@ -128,6 +129,7 @@ Below is my TO-DO list of features I would like to implement in Turbo: - [ ] Color scheme customization. - [x] Syntax highlighting for some languages (C/C++, Rust, Python, JavaScript, Make, Bash, Ruby, JSON, YAML). - [ ] Syntax highlighting for the rest of [languages supported by Scintilla](https://github.com/RaiKoHoff/scintilla/blob/master/include/SciLexer.h). +- [x] Comment toggling. - [x] Brace match highlighting. - [ ] VIM input mode. - [ ] Localization. diff --git a/include/test/test.h b/include/test/test.h new file mode 100644 index 0000000..5bd8c17 --- /dev/null +++ b/include/test/test.h @@ -0,0 +1,45 @@ +#ifndef TURBO_TEST_H +#define TURBO_TEST_H + +#include + +#include +#include + +namespace turbo +{ + +template +struct TestCase +{ + T input; + T result; +}; + +template +inline void expectMatchingResult(const T1 &actual, const TestCase &testCase) +{ + EXPECT_EQ(actual, testCase.result) << "With test input:\n" << testCase.input; +} + +struct TextState +{ + enum : char { + chCaret = '|', + chAnchor = '^', + }; + + std::string text; + Sci::Position caret {-1}; + Sci::Position anchor {-1}; + + static TextState decode(std::string_view); + static std::string encode(TextState); +}; + +TScintilla &createScintilla(TextState state); +TextState getTextState(TScintilla &scintilla); + +} // namespace turbo + +#endif // TURBO_TEST_H diff --git a/include/turbo/editor.h b/include/turbo/editor.h index 923b221..8c5b78a 100644 --- a/include/turbo/editor.h +++ b/include/turbo/editor.h @@ -107,6 +107,7 @@ class Editor : protected TScintillaParent inline void uppercase(); inline void lowercase(); inline void capitalize(); + inline void toggleComment(); }; template @@ -138,6 +139,11 @@ inline void Editor::capitalize() turbo::changeCaseOfSelection(scintilla, caseConvCapitalize); } +inline void Editor::toggleComment() +{ + turbo::toggleComment(scintilla, language); +} + class EditorView : public TSurfaceView { // 'EditorView' is used to display an 'Editor's contents and feed input diff --git a/include/turbo/editstates.h b/include/turbo/editstates.h index 57cbe58..3289696 100644 --- a/include/turbo/editstates.h +++ b/include/turbo/editstates.h @@ -89,6 +89,9 @@ void applyTheming(const LexerSettings *lexer, const ColorScheme *scheme, TScinti // Highlights matching braces if there are any. void updateBraces(const ColorScheme *scheme, TScintilla &scintilla); +// Toggles comment in selected text. +void toggleComment(TScintilla &scintilla, const Language *language); + void stripTrailingSpaces(TScintilla &scintilla); void ensureNewlineAtEnd(TScintilla &scintilla); diff --git a/include/turbo/styles.h b/include/turbo/styles.h index 4b7ef3a..3d7dc37 100644 --- a/include/turbo/styles.h +++ b/include/turbo/styles.h @@ -7,9 +7,21 @@ namespace turbo { -class Language +struct Language { -public: + TStringView lineComment {}; + TStringView blockCommentOpen {}; + TStringView blockCommentClose {}; + + constexpr bool hasLineComments() const + { + return !lineComment.empty(); + } + + constexpr bool hasBlockComments() const + { + return !blockCommentOpen.empty() && !blockCommentClose.empty(); + } static const Language CPP, @@ -39,10 +51,6 @@ class Language Erlang, Smalltalk, Markdown; - -private: - - Language() = default; }; enum TextStyle : uchar diff --git a/source/turbo-core/editstates.cc b/source/turbo-core/editstates.cc index aa2c231..7866443 100644 --- a/source/turbo-core/editstates.cc +++ b/source/turbo-core/editstates.cc @@ -86,6 +86,299 @@ void AutoIndent::applyToCurrentLine(TScintilla &scintilla) } } +///////////////////////////////////////////////////////////////////////// +// Comment toggling + +static bool removeComment(TScintilla &, const Language &); +static bool removeBlockComment(TScintilla &, const Language &); +static Sci::Position getSelectionEndSkippingEmptyLastLine(TScintilla &, Sci::Position); +static void getLineStartAndEnd(TScintilla &, Sci::Position &, Sci::Position &); +static TStringView getViewIntoText(TScintilla &, Sci::Position, Sci::Position); +static size_t findCommentAtStart(TStringView, TStringView); +static size_t findCommentAtEnd(TStringView, TStringView); +static bool removeLineComments(TScintilla &, const Language &); +static bool noLinesBeginWithoutLineComment(TScintilla &, const Language &, Sci::Line, Sci::Line); +static void removeLineCommentFromLine(TScintilla &, const Language &, Sci::Line); +static void insertComment(TScintilla &, const Language &); +static bool thereIsTextBeforeOrAfterSelection(TScintilla &); +static void insertBlockComment(TScintilla &, const Language &); +static void restoreSelectionAfterInsert(TScintilla &, Sci::Position, Sci::Position, Sci::Position, size_t); +static void insertLineComments(TScintilla &, const Language &); +static size_t minIndentationInLines(TScintilla &, Sci::Line, Sci::Line); +static size_t insertLineCommentIntoLine(TScintilla &, const Language &, Sci::Line, size_t); + +void toggleComment(TScintilla &scintilla, const Language *language) +{ + if (language && (language->hasLineComments() || language->hasBlockComments())) + { + if (!removeComment(scintilla, *language)) + insertComment(scintilla, *language); + call(scintilla, SCI_SCROLLCARET, 0U, 0U); + } +} + +static bool removeComment(TScintilla &scintilla, const Language &language) +{ + return removeBlockComment(scintilla, language) + || removeLineComments(scintilla, language); +} + +static bool removeBlockComment(TScintilla &scintilla, const Language &language) +{ + if (language.hasBlockComments()) + { + Sci::Position posStart = call(scintilla, SCI_GETSELECTIONSTART, 0U, 0U); + Sci::Position posEnd = getSelectionEndSkippingEmptyLastLine(scintilla, posStart); + if (posStart == posEnd) + getLineStartAndEnd(scintilla, posStart, posEnd); + TStringView text = getViewIntoText(scintilla, posStart, posEnd); + + size_t openStart = findCommentAtStart(text, language.blockCommentOpen); + if (openStart < text.size()) + { + size_t closeStart = findCommentAtEnd(text, language.blockCommentClose); + if (closeStart < text.size()) + { + size_t openSize = language.blockCommentOpen.size(); + size_t closeSize = language.blockCommentClose.size(); + call(scintilla, SCI_BEGINUNDOACTION, 0U, 0U); + call(scintilla, SCI_DELETERANGE, posStart + openStart, openSize); + call(scintilla, SCI_DELETERANGE, posStart + closeStart - openSize, closeSize); + call(scintilla, SCI_ENDUNDOACTION, 0U, 0U); + return true; + } + } + } + return false; +} + +static Sci::Position getSelectionEndSkippingEmptyLastLine(TScintilla &scintilla, Sci::Position selStart) +{ + Sci::Position selEnd = call(scintilla, SCI_GETSELECTIONEND, 0U, 0U); + if (selStart < selEnd) + { + Sci::Line line = call(scintilla, SCI_LINEFROMPOSITION, selEnd, 0U); + Sci::Line prevPosLine = call(scintilla, SCI_LINEFROMPOSITION, selEnd - 1, 0U); + if (prevPosLine < line) + return call(scintilla, SCI_GETLINEENDPOSITION, prevPosLine, 0U); + } + return selEnd; +} + +static void getLineStartAndEnd(TScintilla &scintilla, Sci::Position &posStart, Sci::Position &posEnd) +{ + Sci::Line line = call(scintilla, SCI_LINEFROMPOSITION, posStart, 0U); + posStart = call(scintilla, SCI_POSITIONFROMLINE, line, 0U); + posEnd = call(scintilla, SCI_GETLINEENDPOSITION, line, 0U); +} + +static TStringView getViewIntoText(TScintilla &scintilla, Sci::Position start, Sci::Position end) +{ + auto length = size_t(end - start); + return TStringView { + (const char *) call(scintilla, SCI_GETRANGEPOINTER, start, length), + size_t(length), + }; +} + +static size_t findCommentAtStart(TStringView text, TStringView comment) +{ + size_t i = 0; + while (i < text.size() && Scintilla::IsSpaceOrTab(text[i])) + ++i; + size_t j = 0; + while (j < comment.size()) + if (!(i < text.size() && text[i++] == comment[j++])) + return text.size(); + return i - comment.size(); +} + +static size_t findCommentAtEnd(TStringView text, TStringView comment) +{ + size_t i = text.size(); + while (i > 0 && Scintilla::IsSpaceOrTab(text[i - 1])) + --i; + size_t j = comment.size(); + while (j > 0) + if (!(i > 0 && text[--i] == comment[--j])) + return text.size(); + return i; +} + +static bool removeLineComments(TScintilla &scintilla, const Language &language) +{ + if (language.hasLineComments()) + { + Sci::Position selStart = call(scintilla, SCI_GETSELECTIONSTART, 0U, 0U); + Sci::Position selEnd = getSelectionEndSkippingEmptyLastLine(scintilla, selStart); + Sci::Line firstLine = call(scintilla, SCI_LINEFROMPOSITION, selStart, 0U); + Sci::Line lastLine = call(scintilla, SCI_LINEFROMPOSITION, selEnd, 0U); + + if (noLinesBeginWithoutLineComment(scintilla, language, firstLine, lastLine)) + { + call(scintilla, SCI_BEGINUNDOACTION, 0U, 0U); + for (Sci::Line line = firstLine; line <= lastLine; ++line) + removeLineCommentFromLine(scintilla, language, line); + call(scintilla, SCI_ENDUNDOACTION, 0U, 0U); + return true; + } + } + return false; +} + +static bool noLinesBeginWithoutLineComment(TScintilla &scintilla, const Language &language, Sci::Line firstLine, Sci::Line lastLine) +{ + bool atLeastOneIsNotEmpty = false; + for (Sci::Line line = firstLine; line <= lastLine; ++line) + { + Sci::Position lineStart = call(scintilla, SCI_POSITIONFROMLINE, line, 0U); + Sci::Position lineEnd = call(scintilla, SCI_GETLINEENDPOSITION, line, 0U); + TStringView text = getViewIntoText(scintilla, lineStart, lineEnd); + if (!text.empty() && text.size() == findCommentAtStart(text, language.lineComment)) + return false; + else if (!text.empty()) + atLeastOneIsNotEmpty = true; + } + return atLeastOneIsNotEmpty; +} + +static void removeLineCommentFromLine(TScintilla &scintilla, const Language &language, Sci::Line line) +// Pre: 'language.lineComment' is not empty. +{ + Sci::Position lineStart = call(scintilla, SCI_POSITIONFROMLINE, line, 0U); + Sci::Position lineEnd = call(scintilla, SCI_GETLINEENDPOSITION, line, 0U); + TStringView text = getViewIntoText(scintilla, lineStart, lineEnd); + TStringView comment = language.lineComment; + size_t commentStart = findCommentAtStart(text, comment); + if (commentStart < text.size()) + { + size_t commentEnd = commentStart + comment.size(); + if (comment.back() != ' ' && commentEnd < text.size() && text[commentEnd] == ' ') + commentEnd += 1; + call(scintilla, SCI_DELETERANGE, lineStart + commentStart, commentEnd - commentStart); + } +} + +static void insertComment(TScintilla &scintilla, const Language &language) +// Pre: language supports at least one kind of comment. +{ + if ( !language.hasLineComments() + || (language.hasBlockComments() && thereIsTextBeforeOrAfterSelection(scintilla)) ) + insertBlockComment(scintilla, language); + else + insertLineComments(scintilla, language); +} + +bool thereIsTextBeforeOrAfterSelection(TScintilla &scintilla) +{ + Sci::Position selStart = call(scintilla, SCI_GETSELECTIONSTART, 0U, 0U); + Sci::Position selEnd = getSelectionEndSkippingEmptyLastLine(scintilla, selStart); + if (selStart < selEnd) + { + Sci::Line firstLine = call(scintilla, SCI_LINEFROMPOSITION, selStart, 0U); + Sci::Position firstLineStart = call(scintilla, SCI_POSITIONFROMLINE, firstLine, 0U); + TStringView textBefore = getViewIntoText(scintilla, firstLineStart, selStart); + for (char c : textBefore) + if (!Scintilla::IsSpaceOrTab(c)) + return true; + Sci::Line lastLine = call(scintilla, SCI_LINEFROMPOSITION, selEnd, 0U); + Sci::Position lastLineEnd = call(scintilla, SCI_GETLINEENDPOSITION, lastLine, 0U); + TStringView textAfter = getViewIntoText(scintilla, selEnd, lastLineEnd); + for (char c : textAfter) + if (!Scintilla::IsSpaceOrTab(c)) + return true; + } + return false; +} + +static void insertBlockComment(TScintilla &scintilla, const Language &language) +// Pre: language.hasBlockComments() +{ + Sci::Position caret = call(scintilla, SCI_GETCURRENTPOS, 0U, 0U); + Sci::Position anchor = call(scintilla, SCI_GETANCHOR, 0U, 0U); + Sci::Position posStart = call(scintilla, SCI_GETSELECTIONSTART, 0U, 0U); + Sci::Position posEnd = getSelectionEndSkippingEmptyLastLine(scintilla, posStart); + if (posStart == posEnd) + getLineStartAndEnd(scintilla, posStart, posEnd); + + call(scintilla, SCI_BEGINUNDOACTION, 0U, 0U); + call(scintilla, SCI_INSERTTEXT, posEnd, (sptr_t) std::string(language.blockCommentClose).c_str()); + call(scintilla, SCI_INSERTTEXT, posStart, (sptr_t) std::string(language.blockCommentOpen).c_str()); + size_t insertLength = language.blockCommentOpen.size() + language.blockCommentClose.size(); + restoreSelectionAfterInsert(scintilla, caret, anchor, posStart, insertLength); + call(scintilla, SCI_ENDUNDOACTION, 0U, 0U); +} + +static void restoreSelectionAfterInsert(TScintilla &scintilla, Sci::Position caret, Sci::Position anchor, Sci::Position firstInsert, size_t insertLength) +{ + if (caret != anchor) + { + Sci::Position &selStart = caret < anchor ? caret : anchor; + Sci::Position &selEnd = caret > anchor ? caret : anchor; + if (firstInsert < selStart) + selStart += insertLength; + selEnd += insertLength; + call(scintilla, SCI_SETSEL, anchor, caret); + } +} + +static void insertLineComments(TScintilla &scintilla, const Language &language) +// Pre: language.hasLineComments() +{ + Sci::Position caret = call(scintilla, SCI_GETCURRENTPOS, 0U, 0U); + Sci::Position anchor = call(scintilla, SCI_GETANCHOR, 0U, 0U); + Sci::Position posStart = call(scintilla, SCI_GETSELECTIONSTART, 0U, 0U); + Sci::Position posEnd = getSelectionEndSkippingEmptyLastLine(scintilla, posStart); + Sci::Line firstLine = call(scintilla, SCI_LINEFROMPOSITION, posStart, 0U); + Sci::Line lastLine = call(scintilla, SCI_LINEFROMPOSITION, posEnd, 0U); + Sci::Position firstLineStart = call(scintilla, SCI_POSITIONFROMLINE, firstLine, 0U); + + size_t indentation = minIndentationInLines(scintilla, firstLine, lastLine); + call(scintilla, SCI_BEGINUNDOACTION, 0U, 0U); + size_t insertLength = 0; + for (Sci::Line line = firstLine; line <= lastLine; ++line) + insertLength += insertLineCommentIntoLine(scintilla, language, line, indentation); + restoreSelectionAfterInsert(scintilla, caret, anchor, firstLineStart + indentation, insertLength); + call(scintilla, SCI_ENDUNDOACTION, 0U, 0U); +} + +static size_t minIndentationInLines(TScintilla &scintilla, Sci::Line firstLine, Sci::Line lastLine) +{ + size_t result = (size_t) -1; + for (Sci::Line line = firstLine; line <= lastLine; ++line) + { + Sci::Position lineStart = call(scintilla, SCI_POSITIONFROMLINE, line, 0U); + Sci::Position lineEnd = call(scintilla, SCI_GETLINEENDPOSITION, line, 0U); + TStringView text = getViewIntoText(scintilla, lineStart, lineEnd); + if (!text.empty()) + { + size_t i = 0; + while (i < text.size() && Scintilla::IsSpaceOrTab(text[i])) + ++i; + result = min(i, result); + } + } + return result == (size_t) -1 ? 0 : result; +} + +static size_t insertLineCommentIntoLine(TScintilla &scintilla, const Language &language, Sci::Line line, size_t indentation) +{ + std::string comment {language.lineComment}; + if (comment.back() != ' ') + comment.push_back(' '); + size_t insertLength = 0; + Sci::Position lineStart = call(scintilla, SCI_POSITIONFROMLINE, line, 0U); + Sci::Position lineEnd = call(scintilla, SCI_GETLINEENDPOSITION, line, 0U); + if (lineStart == lineEnd && indentation > 0) + { + call(scintilla, SCI_INSERTTEXT, lineStart, (sptr_t) std::string(indentation, ' ').c_str()); + insertLength += indentation; + } + call(scintilla, SCI_INSERTTEXT, lineStart + indentation, (sptr_t) comment.c_str()); + insertLength += comment.size(); + return insertLength; +} + ///////////////////////////////////////////////////////////////////////// void applyTheming(const LexerSettings *lexer, const ColorScheme *aScheme, TScintilla &scintilla) diff --git a/source/turbo-core/styles.cc b/source/turbo-core/styles.cc index 500fcf6..0074ae5 100644 --- a/source/turbo-core/styles.cc +++ b/source/turbo-core/styles.cc @@ -12,32 +12,32 @@ namespace turbo { extern constexpr Language - Language::CPP, - Language::Makefile, - Language::Asm, - Language::JavaScript, - Language::Rust, - Language::Python, - Language::Bash, + Language::CPP {"//", "/*", "*/"}, + Language::Makefile {"#"}, + Language::Asm {";"}, + Language::JavaScript {"//", "/*", "*/"}, + Language::Rust {"//", "/*", "*/"}, + Language::Python {"#"}, + Language::Bash {"#"}, Language::Diff, - Language::JSON, - Language::HTML, - Language::XML, - Language::VB, - Language::Perl, - Language::Batch, - Language::LaTex, - Language::Lua, - Language::Ada, - Language::Lisp, - Language::Ruby, - Language::Tcl, - Language::VBScript, - Language::MATLAB, - Language::CSS, - Language::YAML, - Language::Erlang, - Language::Smalltalk, + Language::JSON {"//", "/*", "*/"}, + Language::HTML {{}, ""}, + Language::XML {{}, ""}, + Language::VB {"'"}, + Language::Perl {"#"}, + Language::Batch {"rem "}, + Language::LaTex {"%"}, + Language::Lua {"--", "--[[", "]]"}, + Language::Ada {"--"}, + Language::Lisp {";"}, + Language::Ruby {"#"}, + Language::Tcl {"#"}, + Language::VBScript {"#"}, + Language::MATLAB {"%"}, + Language::CSS {{}, "/*", "*/"}, + Language::YAML {"#"}, + Language::Erlang {"%"}, + Language::Smalltalk {{}, "\"", "\""}, Language::Markdown; static const const_unordered_map mime2lang = { diff --git a/source/turbo/app.cc b/source/turbo/app.cc index 187ca54..aa4849a 100644 --- a/source/turbo/app.cc +++ b/source/turbo/app.cc @@ -66,6 +66,7 @@ TurboApp::TurboApp(int argc, const char *argv[]) noexcept : ts += cmSelUppercase; ts += cmSelLowercase; ts += cmSelCapitalize; + ts += cmToggleComment; disableCommands(ts); // Actions that only make sense when there is at least one editor. @@ -131,6 +132,8 @@ TMenuBar *TurboApp::initMenuBar(TRect r) *new TMenuItem( "Find ~N~ext", cmSearchAgain, kbF3, hcNoContext, "F3" ) + *new TMenuItem( "Find ~P~revious", cmSearchPrev, kbShiftF3, hcNoContext, "Shift-F3" ) + *new TSubMenu( "Se~l~ection", kbAltL ) + + *new TMenuItem( "~T~oggle Comment", cmToggleComment, kbCtrlQ, hcNoContext ) + + newLine() + *new TMenuItem( "~U~ppercase", cmSelUppercase, kbNoKey, hcNoContext ) + *new TMenuItem( "~L~owercase", cmSelLowercase, kbNoKey, hcNoContext ) + *new TMenuItem( "~C~apitalize", cmSelCapitalize, kbNoKey, hcNoContext ) + diff --git a/source/turbo/app.h b/source/turbo/app.h index 85b4ca5..863d413 100644 --- a/source/turbo/app.h +++ b/source/turbo/app.h @@ -27,7 +27,8 @@ const ushort cmRename = 110, cmSelUppercase = 111, cmSelLowercase = 112, - cmSelCapitalize = 113; + cmSelCapitalize = 113, + cmToggleComment = 114; // Commands that cannot be deactivated. const ushort diff --git a/source/turbo/editwindow.cc b/source/turbo/editwindow.cc index a4210da..5d71fc2 100644 --- a/source/turbo/editwindow.cc +++ b/source/turbo/editwindow.cc @@ -37,6 +37,7 @@ EditorWindow::EditorWindow( const TRect &bounds, TurboEditor &aEditor, enabledCmds += cmSelUppercase; enabledCmds += cmSelLowercase; enabledCmds += cmSelCapitalize; + enabledCmds += cmToggleComment; // Commands that always get disabled when unfocusing the editor. disabledCmds += enabledCmds; @@ -90,6 +91,10 @@ void EditorWindow::handleEvent(TEvent &ev) { editor.capitalize(); editor.partialRedraw(); break; + case cmToggleComment: + editor.toggleComment(); + editor.partialRedraw(); + break; default: handled = false; } diff --git a/test/test.cc b/test/test.cc new file mode 100644 index 0000000..b083d22 --- /dev/null +++ b/test/test.cc @@ -0,0 +1,63 @@ +#include + +#include + +namespace turbo +{ + +TextState TextState::decode(std::string_view input) +{ + size_t caret = input.find(chCaret); + EXPECT_NE(caret, std::string_view::npos); + size_t anchor = input.find(chAnchor); + if (anchor == std::string_view::npos) + anchor = caret; + else if (anchor < caret) + caret -= 1; + else + anchor -= 1; + auto &&text = std::string(input); + text.erase( + std::remove_if(text.begin(), text.end(), [] (auto &c) { return c == chCaret || c == chAnchor; }), + text.end() + ); + return { + std::move(text), + Sci::Position(caret), + Sci::Position(anchor), + }; +} + +std::string TextState::encode(TextState self) +{ + self.text.insert(self.caret, 1, chCaret); + if (self.anchor != -1 && self.anchor != self.caret) + self.text.insert( + self.anchor + (self.caret < self.anchor), + 1, + chAnchor + ); + return std::move(self.text); +} + +TScintilla &createScintilla(TextState state) +{ + auto &scintilla = createScintilla((Clipboard *) nullptr); + call(scintilla, SCI_SETTEXT, 0U, (sptr_t) state.text.c_str()); + call(scintilla, SCI_SETSEL, state.anchor, state.caret); + return scintilla; +} + +TextState getTextState(TScintilla &scintilla) +{ + Sci::Position length = call(scintilla, SCI_GETLENGTH, 0U, 0U); + std::string text(length, char()); + call(scintilla, SCI_GETTEXT, length + 1, (sptr_t) text.data()); + return { + std::move(text), + (Sci::Position) call(scintilla, SCI_GETCURRENTPOS, 0U, 0U), + (Sci::Position) call(scintilla, SCI_GETANCHOR, 0U, 0U), + }; +} + +} // namespace turbo diff --git a/test/turbo-core/editstates.test.cc b/test/turbo-core/editstates.test.cc new file mode 100644 index 0000000..b87f4eb --- /dev/null +++ b/test/turbo-core/editstates.test.cc @@ -0,0 +1,330 @@ +#include +#include + +namespace turbo +{ + +static std::string toggleComment(const Language &language, std::string_view input) +{ + auto &&inputState = TextState::decode(input); + auto &scintilla = createScintilla(inputState); + toggleComment(scintilla, &language); + auto &&outputState = getTextState(scintilla); + destroyScintilla(scintilla); + return TextState::encode(std::move(outputState)); +} + +TEST(EditStates, ShouldRemoveHtmlBlockComments) +{ + static constexpr TestCase testCases[] = + { + { "\n" + "\n" + "\n", + + "\n" + "\n" + "\n", + }, + { "\n" + "\n" + "|ad>\n", + + "\n" + "\n" + "\n", + }, + { "\n" + "\n" + "^ad>\n", + + "\n" + "\n" + "\n", + }, + { "\n" + "^\n" + "|\n", + + "\n" + "^\n" + "|\n", + }, + { "^\n" + "|\n", + + "^\n" + "\n" + "\n" + "|\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::HTML, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldRemoveCppBlockComments) +{ + static constexpr TestCase testCases[] = + { + { "^/* */\n" + "/* */\n" + "/* */\n" + "|/**/\n", + + "^ */\n" + "/* */\n" + "/* \n" + "|/**/\n", + }, + { "void foo(int |/*unused*/^) { }\n", + + "void foo(int |unused^) { }\n", + }, + { "void foo(int |/* unused */^) { }\n", + + "void foo(int | unused ^) { }\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::CPP, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldRemoveBashLineComments) +{ + static constexpr TestCase testCases[] = + { + { "|\n" + "# echo a\n" + " #sleep 3\n" + "\n" + " # cat^\n", + + "|\n" + "echo a\n" + " sleep 3\n" + "\n" + " cat^\n", + }, + { "\n" + "# ech|o a\n" + " #sleep 3 # && echo b\n" + "\n" + " ^# cat\n", + + "\n" + "ech|o a\n" + " sleep 3 # && echo b\n" + "\n" + " ^ cat\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::Bash, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldRemoveBatchLineComments) +{ + static constexpr TestCase testCases[] = + { + { "^rem echo a\n" + "rem echo b\n" + "|\n", + + "^echo a\n" + " echo b\n" + "|\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::Batch, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldRemoveCppLineComments) +{ + static constexpr TestCase testCases[] = + { + { "// int i| = 0;\n", + + "int i| = 0;\n", + }, + { "|// int i = 0;\n" + "// int j = 0;\n" + "// int z = 0;^\n", + + "|int i = 0;\n" + "int j = 0;\n" + "int z = 0;^\n", + }, + { "// i^nt i = 0;\n" + "// in|t j = 0;\n", + + "i^nt i = 0;\n" + "in|t j = 0;\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::CPP, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldInsertHtmlBlockComments) +{ + static constexpr TestCase testCases[] = + { + { "^\n" + "\n" + "\n" + "|\n", + + "^\n" + "|\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::HTML, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldInsertCppBlockComments) +{ + static constexpr TestCase testCases[] = + { + { "void foo(int |unused^) { }\n", + + "void foo(int |/*unused*/^) { }\n", + }, + { "void foo(int | unused ^) { }\n", + + "void foo(int |/* unused */^) { }\n", + }, + { "^int i = 0;\n" + "int j = 0|;\n", + + "^/*int i = 0;\n" + "int j = 0*/|;\n", + }, + { "i^nt i = 0;\n" + "int j = 0;\n" + "|\n", + + "i^/*nt i = 0;\n" + "int j = 0;*/\n" + "|\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::CPP, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldInsertCppLineComments) +{ + static constexpr TestCase testCases[] = + { + { "int i| = 0;\n", + + "// int i| = 0;\n", + }, + { "|int i = 0;\n" + "int j = 0;\n" + "int z = 0;^\n", + + "|// int i = 0;\n" + "// int j = 0;\n" + "// int z = 0;^\n", + }, + { "^ int i = 0;\n" + "\n" + " int j = 0;\n" + " int z = 0;|\n", + + "^ // int i = 0;\n" + " // \n" + " // int j = 0;\n" + " // int z = 0;|\n", + }, + { "| int i = 0;\n" + "\t\tint j = 0;^\n", + + "| // int i = 0;\n" + "\t\t// int j = 0;^\n", + }, + { "int i = 0;|\n" + "^\n", + + "// int i = 0;|\n" + "^\n", + }, + { "int i = 0;^\n" + "|\n", + + "// int i = 0;^\n" + "|\n", + }, + { "|\n" + "\n" + "\n" + "^\n", + + "|// \n" + "// \n" + "// \n" + "^\n", + } + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::CPP, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +TEST(EditStates, ShouldInsertBatchLineComments) +{ + static constexpr TestCase testCases[] = + { + { "ech|o a\n", + + "rem ech|o a\n", + }, + }; + + for (auto &testCase : testCases) + { + auto &&actual = toggleComment(Language::Batch, testCase.input); + expectMatchingResult(actual, testCase); + } +} + +} // namespace turbo