diff --git a/PowerEditor/installer/nativeLang/english.xml b/PowerEditor/installer/nativeLang/english.xml index 619f03366..bf7c30168 100644 --- a/PowerEditor/installer/nativeLang/english.xml +++ b/PowerEditor/installer/nativeLang/english.xml @@ -137,6 +137,8 @@ Translation note: + + @@ -1468,6 +1470,9 @@ Your settings on cloud will be canceled. Please reset a coherent value via Prefe + + + + + @@ -1468,6 +1470,9 @@ Your settings on cloud will be canceled. Please reset a coherent value via Prefe + + + (sci->execute(SCI_GETCODEPAGE)); + + intptr_t lines, startPos, topLine, endPos, bottomLine; + bool rectangular = false; + bool noselection = false; + bool forward = true; + bool missingEOL = false; + + // Set: + // rectangular = true for rectangular and thin selections; false all other times + // noselection = true if nothing was selected; false all other times + // forward = false if caret is less than anchor; true all other times + // missingEOL = true if selection ends includes last line of document with no terminating line ending; otherwise false + // topLine and bottomLine = line indices to be sorted, bottomLine included + // startPos and endPos = text range of all lines to be sorted, endPos not included + // lines = number of lines to be sorted + // + // Return warnMultiple if there is a multiple stream selection or sortNothing if there is a selection covering fewer than two lines. + + switch (sci->execute(SCI_GETSELECTIONMODE)) + { + case SC_SEL_THIN : + case SC_SEL_RECTANGLE: + lines = sci->execute(SCI_GETSELECTIONS); + if (lines < 2) return sortNothing; + if (sci->execute(SCI_GETSELECTIONEMPTY)) noselection = true; + rectangular = true; + { + intptr_t rsa = sci->execute(SCI_GETRECTANGULARSELECTIONANCHOR); + intptr_t rsc = sci->execute(SCI_GETRECTANGULARSELECTIONCARET); + forward = rsc > rsa; + topLine = sci->execute(SCI_LINEFROMPOSITION, forward ? rsa : rsc); + bottomLine = sci->execute(SCI_LINEFROMPOSITION, forward ? rsc : rsa); + } + startPos = sci->execute(SCI_POSITIONFROMLINE, topLine); + if (bottomLine == sci->execute(SCI_GETLINECOUNT) - 1) // selection ends on last line of document + { + endPos = sci->execute(SCI_GETLENGTH); + missingEOL = true; + } + else + { + endPos = sci->execute(SCI_POSITIONFROMLINE, bottomLine + 1); + } + break; + + default: + if (sci->execute(SCI_GETSELECTIONS) != 1) return warnMultiple; + intptr_t anchor = sci->execute(SCI_GETANCHOR); + intptr_t caret = sci->execute(SCI_GETCURRENTPOS); + if (anchor == caret) + { + noselection = true; + topLine = startPos = 0; + bottomLine = sci->execute(SCI_GETLINECOUNT) - 1; + endPos = sci->execute(SCI_GETLENGTH); + if (sci->execute(SCI_POSITIONFROMLINE, bottomLine) == endPos) // last line is empty; don't sort it + bottomLine--; + else // last line of document is not empty (no line ending) + missingEOL = true; + } + else + { + forward = anchor < caret; + topLine = sci->execute(SCI_LINEFROMPOSITION, forward ? anchor : caret); + startPos = sci->execute(SCI_POSITIONFROMLINE, topLine); + endPos = forward ? caret : anchor; + bottomLine = sci->execute(SCI_LINEFROMPOSITION, endPos); + if (sci->execute(SCI_POSITIONFROMLINE, bottomLine) == endPos) // selection ends at beginning of a line + { + bottomLine--; + } + else if (bottomLine == sci->execute(SCI_GETLINECOUNT) - 1) // selection ends in last line of document, with no line ending + { + missingEOL = true; + endPos = sci->execute(SCI_GETLENGTH); + } + else // move end position to include line ending + { + endPos = sci->execute(SCI_POSITIONFROMLINE, bottomLine + 1); + } + } + lines = bottomLine - topLine + 1; + if (lines < 2) return sortNothing; + } + + // Extensive memory allocation which follows is enclosed in a try block, so failures can be intercepted. + // No changes are made to the Scintilla document within the try block; failure when changing the document + // should be caught by the ordinary Notepad++ error capture routines, since the document cannot be recovered. + // First declare some variables which will be required after the try block is finished. + + std::string sortedText; + intptr_t cpAnchor = 0; + intptr_t cpCaret = 0; + intptr_t vsAnchor = 0; + intptr_t vsCaret = 0; + intptr_t lnCaret = 0; + + try + { + + // Build a vector which will contain the sort keys and pointers to the lines to be sorted. + + struct SortLine { + std::string key; + std::string_view content; + intptr_t index = 0; + intptr_t lineStart = 0; + intptr_t lineLength = 0; + intptr_t keyStart = 0; + intptr_t keyLength = 0; + bool appendEOL = false; + }; + std::vector sortLines(lines); + if (missingEOL) sortLines.back().appendEOL = true; + + // Get the information we need from Scintilla. + + intptr_t cpNextLine = endPos; + for (intptr_t n = lines - 1; n >= 0; --n) + { + SortLine& sl = sortLines[n]; + if (rectangular) + { + sl.index = forward ? n : lines - 1 - n; + sl.lineStart = sci->execute(SCI_POSITIONFROMLINE, topLine + n); + sl.keyStart = sci->execute(SCI_GETSELECTIONNSTART, sl.index); + sl.keyLength = + (noselection ? sci->execute(SCI_GETLINEENDPOSITION, topLine + n) : sci->execute(SCI_GETSELECTIONNEND, sl.index)) + - sl.keyStart; + } + else + { + sl.index = n; + sl.lineStart = sl.keyStart = sci->execute(SCI_POSITIONFROMLINE, topLine + n); + sl.keyLength = sci->execute(SCI_GETLINEENDPOSITION, topLine + n) - sl.keyStart; + } + sl.lineLength = cpNextLine - sl.lineStart; + cpNextLine = sl.lineStart; + } + + std::string docEOL; + if (missingEOL) + { + auto eolMode = sci->execute(SCI_GETEOLMODE); + docEOL = eolMode == SC_EOL_CR ? "\r" : eolMode == SC_EOL_LF ? "\n" : "\r\n"; + } + + // Next, get a pointer into Scintilla's buffer for the range encompassing everything to be sorted. + // Note that this pointer becomes invalid as soon as we access Scintilla again. + // Then find the sort keys and content for each line, sort as requested, and build the replacement text. + + const char* const textPointer = reinterpret_cast(sci->execute(SCI_GETRANGEPOINTER, startPos, endPos - startPos)); + + for (SortLine& sl : sortLines) + { + sl.content = std::string_view(sl.lineStart - startPos + textPointer, sl.lineLength); + std::string_view keyText(sl.keyStart - startPos + textPointer, sl.keyLength); + if (!keyText.empty()) + { + constexpr unsigned int safeSize = std::numeric_limits::max() / 2; + size_t textLength = keyText.length(); + int sortableLength = textLength > safeSize ? safeSize : static_cast(textLength); + int wideLength = MultiByteToWideChar(codepage, 0, keyText.data(), sortableLength, 0, 0); + std::wstring wideText(wideLength, 0); + MultiByteToWideChar(codepage, 0, keyText.data(), sortableLength, wideText.data(), wideLength); + int m = LCMapStringEx(locale, options, wideText.data(), wideLength, 0, 0, 0, 0, 0); + sl.key.resize(m, 0); + LCMapStringEx(locale, options, wideText.data(), wideLength, reinterpret_cast(sl.key.data()), m, 0, 0, 0); + } + } + + if (descending) + std::stable_sort(sortLines.begin(), sortLines.end(), [](const SortLine& a, const SortLine& b) { return a.key > b.key; }); + else + std::stable_sort(sortLines.begin(), sortLines.end(), [](const SortLine& a, const SortLine& b) { return a.key < b.key; }); + + sortedText.reserve(endPos - startPos + docEOL.length()); + for (SortLine& sl : sortLines) + { + sortedText.append(sl.content); + if (sl.appendEOL) sortedText.append(docEOL); + } + + if (missingEOL) // if we added a line ending, remove line ending from last line of sorted text + { + if (sortedText.back() == '\n') sortedText.pop_back(); + if (sortedText.back() == '\r') sortedText.pop_back(); + } + + // Before updating Scintilla, get information we will need to restore the selection (as best we can) + + if (rectangular) + { + intptr_t ixTop = sortLines.front().index; + intptr_t ixBottom = sortLines.back().index; + intptr_t ixAnchor = forward ? ixTop : ixBottom; + intptr_t ixCaret = forward ? ixBottom : ixTop; + cpAnchor = sci->execute(SCI_GETSELECTIONNANCHOR, ixAnchor); + vsAnchor = sci->execute(SCI_GETSELECTIONNANCHORVIRTUALSPACE, ixAnchor); + cpCaret = sci->execute(SCI_GETSELECTIONNCARET, ixCaret); + vsCaret = sci->execute(SCI_GETSELECTIONNCARETVIRTUALSPACE, ixCaret); + cpAnchor -= sci->execute(SCI_POSITIONFROMLINE, sci->execute(SCI_LINEFROMPOSITION, cpAnchor)); + cpCaret -= sci->execute(SCI_POSITIONFROMLINE, sci->execute(SCI_LINEFROMPOSITION, cpCaret)); + } + else if (noselection) + { + cpCaret = sci->execute(SCI_GETCURRENTPOS); + lnCaret = sci->execute(SCI_LINEFROMPOSITION, cpCaret); + cpCaret -= sci->execute(SCI_POSITIONFROMLINE, lnCaret); + for (intptr_t n = 0; n < lines; ++n) if (sortLines[n].index == lnCaret) + { + lnCaret = n; + break; + } + } + + } + + catch (const std::exception& e) + { + try + { + int errlen = MultiByteToWideChar(CP_ACP, 0, e.what(), -1, 0, 0); + if (errlen < 2) return errorUnknown; + std::wstring errmsg(errlen - 1, 0); + MultiByteToWideChar(CP_ACP, 0, e.what(), -1, errmsg.data(), errlen); + return { MB_ICONERROR, "SortLocaleExcept", errmsg }; + } + catch (...) + { + return errorUnknown; + } + } + + catch (...) + { + return errorUnknown; + } + + // Update Scintilla and restore position or selection + + sci->execute(SCI_SETTARGETRANGE, startPos, endPos); + sci->execute(SCI_SETSTATUS, 0); + sci->execute(SCI_REPLACETARGET, sortedText.length(), reinterpret_cast(sortedText.data())); + int replaceStatus = static_cast(sci->execute(SCI_GETSTATUS)); + sci->execute(SCI_SETSTATUS, 0); + if (replaceStatus != SC_STATUS_OK && replaceStatus < SC_STATUS_WARN_START) + { + struct ScintillaMemory : std::exception + { + const char* what() const noexcept override { return "Scintilla ran out of memory while updating the document."; } + }; + struct ScintillaFail : std::exception + { + const char* what() const noexcept override { return "Scintilla was unable to update the document."; } + }; + SendMessage(sci->getHSelf(), WM_SETREDRAW, FALSE, 0); // Without this, Scintilla can hang before message is displayed + if (replaceStatus == SC_STATUS_BADALLOC) + throw ScintillaMemory(); + else + throw ScintillaFail(); + } + + if (rectangular) + { + cpAnchor += sci->execute(SCI_POSITIONFROMLINE, forward ? topLine : bottomLine); + cpCaret += sci->execute(SCI_POSITIONFROMLINE, forward ? bottomLine : topLine); + sci->execute(SCI_SETRECTANGULARSELECTIONANCHOR, cpAnchor); + sci->execute(SCI_SETRECTANGULARSELECTIONCARET, cpCaret); + sci->execute(SCI_SETRECTANGULARSELECTIONANCHORVIRTUALSPACE, vsAnchor); + sci->execute(SCI_SETRECTANGULARSELECTIONCARETVIRTUALSPACE, vsCaret); + } + else if (noselection) + sci->execute(SCI_GOTOPOS, sci->execute(SCI_POSITIONFROMLINE, lnCaret) + cpCaret); + else if (forward) + sci->execute(SCI_SETSEL, sci->execute(SCI_GETTARGETSTART), sci->execute(SCI_GETTARGETEND)); + else + sci->execute(SCI_SETSEL, sci->execute(SCI_GETTARGETEND), sci->execute(SCI_GETTARGETSTART)); + + return sortSuccess; + +} \ No newline at end of file diff --git a/PowerEditor/src/MISC/Common/SortLocale.h b/PowerEditor/src/MISC/Common/SortLocale.h new file mode 100644 index 000000000..18c50df59 --- /dev/null +++ b/PowerEditor/src/MISC/Common/SortLocale.h @@ -0,0 +1,39 @@ +// This file is part of Notepad++ project +// Copyright (C)2025 Randall Joseph Fellmy + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// at your option any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + + +#pragma once + +#include +#include +#include <../ScintillaComponent/ScintillaEditView.h> + +class SortLocale +{ +public: + struct Result + { + UINT status = 0; // Will be 0 (successful sort), MB_ICONWARNING or MB_ICONERROR + std::string tagName; // The tag name for translation + std::wstring message; // A message describing the status, if it isn't 0 + }; + std::wstring localeName; + bool caseSensitive = false; + bool digitsAsNumbers = true; + bool ignoreDiacritics = false; + bool ignoreSymbols = false; + Result sort(ScintillaEditView* sci, bool descending) const; +}; \ No newline at end of file diff --git a/PowerEditor/src/Notepad_plus.rc b/PowerEditor/src/Notepad_plus.rc index dd9e806be..6fd114eaa 100644 --- a/PowerEditor/src/Notepad_plus.rc +++ b/PowerEditor/src/Notepad_plus.rc @@ -541,12 +541,14 @@ BEGIN MENUITEM SEPARATOR MENUITEM "Sort Lines Lexicographically Ascending", IDM_EDIT_SORTLINES_LEXICOGRAPHIC_ASCENDING MENUITEM "Sort Lines Lex. Ascending Ignoring Case", IDM_EDIT_SORTLINES_LEXICO_CASE_INSENS_ASCENDING + MENUITEM "Sort Lines In Locale Order Ascending", IDM_EDIT_SORTLINES_LOCALE_ASCENDING MENUITEM "Sort Lines As Integers Ascending", IDM_EDIT_SORTLINES_INTEGER_ASCENDING MENUITEM "Sort Lines As Decimals (Comma) Ascending", IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING MENUITEM "Sort Lines As Decimals (Dot) Ascending", IDM_EDIT_SORTLINES_DECIMALDOT_ASCENDING MENUITEM SEPARATOR MENUITEM "Sort Lines Lexicographically Descending", IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING MENUITEM "Sort Lines Lex. Descending Ignoring Case", IDM_EDIT_SORTLINES_LEXICO_CASE_INSENS_DESCENDING + MENUITEM "Sort Lines In Locale Order Descending", IDM_EDIT_SORTLINES_LOCALE_DESCENDING MENUITEM "Sort Lines As Integers Descending", IDM_EDIT_SORTLINES_INTEGER_DESCENDING MENUITEM "Sort Lines As Decimals (Comma) Descending", IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING MENUITEM "Sort Lines As Decimals (Dot) Descending", IDM_EDIT_SORTLINES_DECIMALDOT_DESCENDING diff --git a/PowerEditor/src/NppCommands.cpp b/PowerEditor/src/NppCommands.cpp index 86b6de7d6..b8186e158 100644 --- a/PowerEditor/src/NppCommands.cpp +++ b/PowerEditor/src/NppCommands.cpp @@ -35,6 +35,7 @@ #include "sha-256.h" #include "calc_sha1.h" #include "sha512.h" +#include "SortLocale.h" using namespace std; @@ -852,6 +853,21 @@ void Notepad_plus::command(int id) } break; + case IDM_EDIT_SORTLINES_LOCALE_ASCENDING: + case IDM_EDIT_SORTLINES_LOCALE_DESCENDING: + { + std::lock_guard lock(command_mutex); + SortLocale sortLocale; + auto result = sortLocale.sort(_pEditView, id == IDM_EDIT_SORTLINES_LOCALE_DESCENDING); + if (result.status) + _nativeLangSpeaker.messageBox(result.tagName.data(), + _pPublicInterface->getHSelf(), + result.message.data(), + result.status == MB_ICONERROR ? L"Sort Failed" : L"Sort not performed", + result.status | MB_OK | MB_APPLMODAL, 0, result.message.data()); + } + break; + case IDM_EDIT_BLANKLINEABOVECURRENT: { _pEditView->insertNewLineAboveCurrentLine(); @@ -4273,6 +4289,8 @@ void Notepad_plus::command(int id) case IDM_EDIT_MULTISELECTNEXTMATCHCASEWHOLEWORD: case IDM_EDIT_MULTISELECTUNDO: case IDM_EDIT_MULTISELECTSSKIP: + case IDM_EDIT_SORTLINES_LOCALE_ASCENDING: + case IDM_EDIT_SORTLINES_LOCALE_DESCENDING: _macro.push_back(recordedMacroStep(id)); break; diff --git a/PowerEditor/src/Parameters.cpp b/PowerEditor/src/Parameters.cpp index d40bd3cdd..b820e1ebd 100644 --- a/PowerEditor/src/Parameters.cpp +++ b/PowerEditor/src/Parameters.cpp @@ -131,6 +131,8 @@ static const WinMenuKeyDefinition winKeyDefs[] = { VK_NULL, IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING, false, false, false, nullptr }, { VK_NULL, IDM_EDIT_SORTLINES_LEXICO_CASE_INSENS_ASCENDING, false, false, false, nullptr }, { VK_NULL, IDM_EDIT_SORTLINES_LEXICO_CASE_INSENS_DESCENDING, false, false, false, nullptr }, + { VK_NULL, IDM_EDIT_SORTLINES_LOCALE_ASCENDING, false, false, false, nullptr }, + { VK_NULL, IDM_EDIT_SORTLINES_LOCALE_DESCENDING, false, false, false, nullptr }, { VK_NULL, IDM_EDIT_SORTLINES_INTEGER_ASCENDING, false, false, false, nullptr }, { VK_NULL, IDM_EDIT_SORTLINES_INTEGER_DESCENDING, false, false, false, nullptr }, { VK_NULL, IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING, false, false, false, nullptr }, diff --git a/PowerEditor/src/menuCmdID.h b/PowerEditor/src/menuCmdID.h index c029d2008..ae0c9a1ed 100644 --- a/PowerEditor/src/menuCmdID.h +++ b/PowerEditor/src/menuCmdID.h @@ -182,6 +182,8 @@ #define IDM_EDIT_MULTISELECTNEXTMATCHCASEWHOLEWORD (IDM_EDIT + 97) #define IDM_EDIT_MULTISELECTUNDO (IDM_EDIT + 98) #define IDM_EDIT_MULTISELECTSSKIP (IDM_EDIT + 99) + #define IDM_EDIT_SORTLINES_LOCALE_ASCENDING (IDM_EDIT + 100) + #define IDM_EDIT_SORTLINES_LOCALE_DESCENDING (IDM_EDIT + 101) #define IDM_EDIT_AUTOCOMPLETE (50000 + 0) #define IDM_EDIT_AUTOCOMPLETE_CURRENTFILE (50000 + 1) diff --git a/PowerEditor/visual.net/notepadPlus.vcxproj b/PowerEditor/visual.net/notepadPlus.vcxproj index 6f74b83e4..667e4c129 100755 --- a/PowerEditor/visual.net/notepadPlus.vcxproj +++ b/PowerEditor/visual.net/notepadPlus.vcxproj @@ -106,6 +106,7 @@ + @@ -247,6 +248,7 @@ +