tdf#140004 Toggle comment in the Basic IDE

This patch adds the "toggle comment" functionality to the Basic IDE.
The shortcut Ctrl + Alt + C is used to execute it.

It works similarly to other code editors such as Kate and VSCode.

Change-Id: Ifdae42b3729cc909baf87c729fe8c3cdf6428184
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/162005
Reviewed-by: Andreas Heinisch <andreas.heinisch@yahoo.de>
Tested-by: Andreas Heinisch <andreas.heinisch@yahoo.de>
diff --git a/basctl/sdi/baside.sdi b/basctl/sdi/baside.sdi
index 74b425c..34f34a6 100644
--- a/basctl/sdi/baside.sdi
+++ b/basctl/sdi/baside.sdi
@@ -688,6 +688,12 @@ shell basctl_Shell
        ExecMethod      = ExecuteDialog;
        StateMethod     = GetState;
    ]

    SID_TOGGLE_COMMENT
    [
        StateMethod = GetState;
        ExecMethod  = ExecuteGlobal;
    ]
}

interface BasicIDEDocument
diff --git a/basctl/source/basicide/baside2.cxx b/basctl/source/basicide/baside2.cxx
index db9b109..62bbaa7 100644
--- a/basctl/source/basicide/baside2.cxx
+++ b/basctl/source/basicide/baside2.cxx
@@ -1066,6 +1066,12 @@ void ModulWindow::ExecuteGlobal (SfxRequest& rReq)
            GetDispatcher()->Execute(SID_GOTOLINE);
        }
        break;

        case SID_TOGGLE_COMMENT:
        {
            GetEditView()->ToggleComment();
        }
        break;
    }
}

diff --git a/basctl/source/basicide/basides1.cxx b/basctl/source/basicide/basides1.cxx
index 8052845..6fe3b9a 100644
--- a/basctl/source/basicide/basides1.cxx
+++ b/basctl/source/basicide/basides1.cxx
@@ -1235,6 +1235,13 @@ void Shell::GetState(SfxItemSet &rSet)
                    rSet.DisableItem( nWh );
            }
            break;
            case SID_TOGGLE_COMMENT:
            {
                // Only available in a ModulWindow if the document can be edited
                if (pCurWin && (!dynamic_cast<ModulWindow*>(pCurWin.get()) || pCurWin->IsReadOnly()))
                    rSet.DisableItem(nWh);
            }
            break;
            case SID_GOTOLINE:
            {
                // if this is not a module window hide the
diff --git a/basctl/uiconfig/basicide/menubar/menubar.xml b/basctl/uiconfig/basicide/menubar/menubar.xml
index d649f96..bf41ce5 100644
--- a/basctl/uiconfig/basicide/menubar/menubar.xml
+++ b/basctl/uiconfig/basicide/menubar/menubar.xml
@@ -55,6 +55,7 @@
            <menu:menuitem menu:id=".uno:Paste"/>
            <menu:menuseparator/>
            <menu:menuitem menu:id=".uno:SelectAll"/>
            <menu:menuitem menu:id=".uno:ToggleComment"/>
            <menu:menuseparator/>
            <menu:menuitem menu:id="vnd.sun.star.findbar:FocusToFindbar"/>
            <menu:menuitem menu:id=".uno:SearchDialog"/>
@@ -177,4 +178,3 @@
        </menu:menupopup>
    </menu:menu>
</menu:menubar>

diff --git a/include/sfx2/sfxsids.hrc b/include/sfx2/sfxsids.hrc
index 4c8a080..176e500 100644
--- a/include/sfx2/sfxsids.hrc
+++ b/include/sfx2/sfxsids.hrc
@@ -670,6 +670,7 @@ class SvxZoomItem;
#define SID_BASICIDE_WATCH                  TypedWhichId<SfxBoolItem>( SID_BASICIDE_START + 55 )
#define SID_BASICIDE_STACK                  TypedWhichId<SfxBoolItem>( SID_BASICIDE_START + 56 )
#define SID_BASICIDE_COLOR_SCHEME_DLG       ( SID_BASICIDE_START + 57 )
#define SID_TOGGLE_COMMENT                  ( SID_BASICIDE_START + 58 )
#define SID_OPTIONS_TREEDIALOG              ( SID_BASICIDE_START + 862)
#define SID_OPTIONS_SECURITY                ( SID_BASICIDE_START + 863)

diff --git a/include/vcl/textview.hxx b/include/vcl/textview.hxx
index 84a89e8..0de1f7c 100644
--- a/include/vcl/textview.hxx
+++ b/include/vcl/textview.hxx
@@ -222,6 +222,9 @@ public:

    bool                IndentBlock();
    bool                UnindentBlock();

    // Used in the Basic IDE to toggle comment on a block of code
    void                ToggleComment();
};

#endif
diff --git a/officecfg/registry/data/org/openoffice/Office/Accelerators.xcu b/officecfg/registry/data/org/openoffice/Office/Accelerators.xcu
index 2265ec7..50f3cd1 100644
--- a/officecfg/registry/data/org/openoffice/Office/Accelerators.xcu
+++ b/officecfg/registry/data/org/openoffice/Office/Accelerators.xcu
@@ -371,6 +371,12 @@ Ctrl+Shift+e aka E_SHIFT_MOD1 under GTK/IBUS is for some emoji thing
    </node>
    <node oor:name="Modules">
      <node oor:name="com.sun.star.script.BasicIDE" oor:op="replace">
        <node oor:name="C_MOD1_MOD2" oor:op="replace">
          <prop oor:name="Command">
            <value xml:lang="x-no-translate">L10N SHORTCUTS - NO TRANSLATE</value>
            <value xml:lang="en-US">.uno:ToggleComment</value>
          </prop>
        </node>
        <node oor:name="F5" oor:op="replace">
          <prop oor:name="Command">
            <value xml:lang="x-no-translate">L10N SHORTCUTS - NO TRANSLATE</value>
diff --git a/officecfg/registry/data/org/openoffice/Office/UI/BasicIDECommands.xcu b/officecfg/registry/data/org/openoffice/Office/UI/BasicIDECommands.xcu
index 9783255..c63bbb2 100644
--- a/officecfg/registry/data/org/openoffice/Office/UI/BasicIDECommands.xcu
+++ b/officecfg/registry/data/org/openoffice/Office/UI/BasicIDECommands.xcu
@@ -18,6 +18,11 @@
          <value xml:lang="en-US">Line Numbers</value>
        </prop>
      </node>
      <node oor:name=".uno:ToggleComment" oor:op="replace">
        <prop oor:name="Label" oor:type="xs:string">
          <value xml:lang="en-US">Toggle Comment</value>
        </prop>
      </node>
      <node oor:name=".uno:InsertFormRadio" oor:op="replace">
        <prop oor:name="Label" oor:type="xs:string">
          <value xml:lang="en-US">Form Option Button</value>
diff --git a/sfx2/sdi/sfx.sdi b/sfx2/sdi/sfx.sdi
index 9c2b72d..5d26a16 100644
--- a/sfx2/sdi/sfx.sdi
+++ b/sfx2/sdi/sfx.sdi
@@ -2393,6 +2393,22 @@ SfxBoolItem ShowLines SID_SHOWLINES
    GroupId = SfxGroupId::Macro;
]

SfxVoidItem ToggleComment SID_TOGGLE_COMMENT
()
[
    AutoUpdate = TRUE,
    FastCall = FALSE,
    ReadOnlyDoc = TRUE,
    Toggle = FALSE,
    Container = FALSE,
    RecordAbsolute = FALSE,
    RecordPerSet;

    AccelConfig = TRUE,
    MenuConfig = TRUE,
    ToolBoxConfig = TRUE,
    GroupId = SfxGroupId::Macro;
]

SfxVoidItem RunMacro SID_RUNMACRO
()
diff --git a/vcl/source/edit/textview.cxx b/vcl/source/edit/textview.cxx
index ad1d28d19..331f69f 100644
--- a/vcl/source/edit/textview.cxx
+++ b/vcl/source/edit/textview.cxx
@@ -20,6 +20,7 @@
#include <memory>
#include <i18nutil/searchopt.hxx>
#include <o3tl/deleter.hxx>
#include <o3tl/string_view.hxx>
#include <utility>
#include <vcl/textview.hxx>
#include <vcl/texteng.hxx>
@@ -2234,5 +2235,181 @@ bool TextView::UnindentBlock()
    return ImpIndentBlock( false );
}

void TextView::ToggleComment()
{
    /* To determines whether to add or remove comment markers, the rule is:
     * - If any of the lines in the selection does not start with a comment character "'"
     *   or "REM" then the selection is commented
     * - Otherwise, the selection is uncommented (i.e. if all of the lines start with a
     *   comment marker "'" or "REM")
     * - Empty lines, or lines with only blank spaces or tabs are ignored
     */

    TextEngine* pEngine = GetTextEngine();
    TextSelection aSel = GetSelection();
    sal_uInt32 nStartPara = aSel.GetStart().GetPara();
    sal_uInt32 nEndPara = aSel.GetEnd().GetPara();

    // True = Comment character will be added; False = Comment marker will be removed
    bool bAddCommentChar = false;

    // Indicates whether any change has been made
    bool bChanged = false;

    // Indicates whether the selection is downwards (normal) or upwards (reversed)
    bool bSelReversed = false;

    if (nEndPara < nStartPara)
    {
        std::swap(nStartPara, nEndPara);
        bSelReversed = true;
    }

    for (sal_uInt32 n = nStartPara; n <= nEndPara; n++)
    {
        OUString sText = pEngine->GetText(n).trim();

        // Empty lines or lines with only blank spaces and tabs are ignored
        if (sText.isEmpty())
            continue;

        if (!sText.startsWith("'") && !sText.startsWithIgnoreAsciiCase("REM"))
        {
            bAddCommentChar = true;
            break;
        }

        // Notice that a REM comment is only actually a comment if:
        // a) There is no subsequent character or
        // b) The subsequent character is a blank space or a tab
        OUString sRest;
        if (sText.startsWithIgnoreAsciiCase("REM", &sRest))
        {
            if (sRest.getLength() > 0 && !sRest.startsWith(" ") && !sRest.startsWith("\t"))
            {
                bAddCommentChar = true;
                break;
            }
        }
    }

    if (bAddCommentChar)
    {
        // For each line, determine the first position where there is a character that is not
        // a blank space or a tab; the comment marker will be the smallest such position
        size_t nCommentPos = std::string::npos;

        for (sal_uInt32 n = nStartPara; n <= nEndPara; n++)
        {
            OUString sText = pEngine->GetText(n);
            std::u16string_view sLine(sText);
            sal_uInt32 nCharPos = sLine.find_first_not_of(u" \t");

            // Update the position where to place the comment marker
            if (nCharPos < nCommentPos)
                nCommentPos = nCharPos;

            // If the comment position is zero, then there's no more need to keep searching
            if (nCommentPos == 0)
                break;
        }

        // Insert the comment marker in all lines (except empty lines)
        for (sal_uInt32 n = nStartPara; n <= nEndPara; n++)
        {
            OUString sText = pEngine->GetText(n);
            std::u16string_view sLine(sText);
            if (o3tl::trim(sLine).length() > 0)
            {
                pEngine->ImpInsertText(TextPaM(n, nCommentPos), "' ");
                bChanged = true;
            }
        }
    }
    else
    {
        // For each line, find the first comment marker and remove it
        for (sal_uInt32 nPara = nStartPara; nPara <= nEndPara; nPara++)
        {
            OUString sText = pEngine->GetText(nPara);
            if (!sText.isEmpty())
            {
                // Determine the position of the comment marker and check whether it's
                // a single quote "'" or a "REM" comment
                sal_Int32 nQuotePos = sText.indexOf("'");
                sal_Int32 nRemPos = sText.toAsciiUpperCase().indexOf("REM");

                // An empty line or a line with only blank spaces or tabs needs to be skipped
                if (nQuotePos == -1 && nRemPos == -1)
                    continue;

                // nRemPos only refers to a comment if the subsequent character is a blank space or tab
                const sal_Int32 nRemSub = nRemPos + 3;
                if (nRemPos != -1 && nRemPos < sText.getLength() - 3 &&
                    sText.indexOf(" ", nRemSub) != nRemSub &&
                    sText.indexOf("\t", nRemSub) != nRemSub)
                {
                    nRemPos = -1;
                }

                // True = comment uses single quote; False = comment uses REM
                bool bQuoteComment = true;

                // Start and end positions to be removed
                sal_Int32 nStartPos = nQuotePos;
                sal_Int32 nEndPos = nStartPos + 1;

                if (nQuotePos == -1)
                    bQuoteComment = false;
                else if (nRemPos != -1 && nRemPos < nQuotePos)
                    bQuoteComment = false;

                if (!bQuoteComment)
                {
                    nStartPos = nRemPos;
                    nEndPos = nStartPos + 3;
                }

                // Check if the next character is a blank space or a tab
                if (sText.indexOf(" ", nEndPos) == nEndPos || sText.indexOf("\t", nEndPos) == nEndPos)
                    nEndPos++;

                // Remove the comment marker
                pEngine->ImpDeleteText(TextSelection(TextPaM(nPara, nStartPos), TextPaM(nPara, nEndPos)));
                bChanged = true;
            }
        }
    }

    // Update selection if there was a selection in the first place
    if (bChanged)
    {
        TextPaM aNewStart;
        if (!bSelReversed)
            aNewStart = TextPaM(nStartPara, std::min(aSel.GetStart().GetIndex(),
                                                     pEngine->GetText(nStartPara).getLength()));
        else
            aNewStart = TextPaM(nStartPara, std::min(aSel.GetEnd().GetIndex(),
                                                     pEngine->GetText(nEndPara).getLength()));

        if (HasSelection())
        {
            TextPaM aNewEnd;
            if (!bSelReversed)
                aNewEnd = TextPaM(nEndPara, pEngine->GetText(nEndPara).getLength());
            else
                aNewEnd = TextPaM(nEndPara, pEngine->GetText(nStartPara).getLength());

            TextSelection aNewSel(aNewStart, aNewEnd);
            ImpSetSelection(aNewSel);
        }
        else
        {
            TextSelection aNewSel(aNewStart, aNewStart);
            ImpSetSelection(aNewSel);
        }
    }
}


/* vim:set shiftwidth=4 softtabstop=4 expandtab: */