No need to double zero arrays
[TortoiseGit.git] / src / Utils / MiscUI / SciEdit.cpp
blob6a9a24e2e2356b90cc64224966ce21dfa6996a1d
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2009-2022 - TortoiseGit
4 // Copyright (C) 2003-2008, 2012-2020 - TortoiseSVN
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "stdafx.h"
21 #include "LoglistCommonResource.h"
22 #include "PathUtils.h"
23 #include "UnicodeUtils.h"
24 #include <string>
25 #include "registry.h"
26 #include "SciEdit.h"
27 #include "SmartHandle.h"
28 #include "../../TortoiseUDiff/UDiffColors.h"
29 #include "LoadIconEx.h"
30 #include "Theme.h"
31 #include "Lexilla.h"
32 #include "DarkModeHelper.h"
34 void CSciEditContextMenuInterface::InsertMenuItems(CMenu&, int&) {return;}
35 bool CSciEditContextMenuInterface::HandleMenuItemClick(int, CSciEdit *) {return false;}
36 void CSciEditContextMenuInterface::HandleSnippet(int, const CString &, CSciEdit *) { return; }
39 #define STYLE_ISSUEBOLD 11
40 #define STYLE_ISSUEBOLDITALIC 12
41 #define STYLE_BOLD 14
42 #define STYLE_ITALIC 15
43 #define STYLE_UNDERLINED 16
44 #define STYLE_URL 17
45 #define INDIC_MISSPELLED 18
47 #define STYLE_MASK 0x1f
49 #define SCI_ADDWORD 2000
51 struct loc_map {
52 const char * cp;
53 const char * def_enc;
56 struct loc_map enc2locale[] = {
57 {"28591","ISO8859-1"},
58 {"28592","ISO8859-2"},
59 {"28593","ISO8859-3"},
60 {"28594","ISO8859-4"},
61 {"28595","ISO8859-5"},
62 {"28596","ISO8859-6"},
63 {"28597","ISO8859-7"},
64 {"28598","ISO8859-8"},
65 {"28599","ISO8859-9"},
66 {"28605","ISO8859-15"},
67 {"20866","KOI8-R"},
68 {"21866","KOI8-U"},
69 {"1251","microsoft-cp1251"},
70 {"65001","UTF-8"},
74 IMPLEMENT_DYNAMIC(CSciEdit, CWnd)
76 CSciEdit::CSciEdit() : m_DirectFunction(NULL)
77 , m_DirectPointer(NULL)
78 , m_spellcodepage(0)
79 , m_spellCheckerFactory(nullptr)
80 , m_SpellChecker(nullptr)
81 , m_separator(0)
82 , m_typeSeparator(1)
83 , m_bDoStyle(false)
84 , m_nAutoCompleteMinChars(3)
85 , m_SpellingCache(2000)
86 , m_blockModifiedHandler(false)
87 , m_bReadOnly(false)
88 , m_themeCallbackId(0)
90 m_hModule = ::LoadLibrary(L"SciLexer_tgit.dll");
93 CSciEdit::~CSciEdit()
95 CTheme::Instance().RemoveRegisteredCallback(m_themeCallbackId);
96 m_personalDict.Save();
99 static std::unique_ptr<UINT[]> Icon2Image(HICON hIcon)
101 if (hIcon == nullptr)
102 return nullptr;
104 ICONINFO iconInfo;
105 if (!GetIconInfo(hIcon, &iconInfo))
106 return nullptr;
108 BITMAP bm;
109 if (!GetObject(iconInfo.hbmColor, sizeof(BITMAP), &bm))
110 return nullptr;
112 int width = bm.bmWidth;
113 int height = bm.bmHeight;
114 int bytesPerScanLine = (width * 3 + 3) & 0xFFFFFFFC;
115 int size = bytesPerScanLine * height;
116 BITMAPINFO infoheader;
117 infoheader.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
118 infoheader.bmiHeader.biWidth = width;
119 infoheader.bmiHeader.biHeight = height;
120 infoheader.bmiHeader.biPlanes = 1;
121 infoheader.bmiHeader.biBitCount = 24;
122 infoheader.bmiHeader.biCompression = BI_RGB;
123 infoheader.bmiHeader.biSizeImage = size;
125 auto ptrb = std::make_unique<BYTE[]>(size * 2 + height * width * 4);
126 LPBYTE pixelsIconRGB = ptrb.get();
127 LPBYTE alphaPixels = pixelsIconRGB + size;
128 HDC hDC = CreateCompatibleDC(nullptr);
129 SCOPE_EXIT { DeleteDC(hDC); };
130 HBITMAP hBmpOld = static_cast<HBITMAP>(SelectObject(hDC, iconInfo.hbmColor));
131 if (!GetDIBits(hDC, iconInfo.hbmColor, 0, height, static_cast<LPVOID>(pixelsIconRGB), &infoheader, DIB_RGB_COLORS))
132 return nullptr;
134 SelectObject(hDC, hBmpOld);
135 if (!GetDIBits(hDC, iconInfo.hbmMask, 0, height, static_cast<LPVOID>(alphaPixels), &infoheader, DIB_RGB_COLORS))
136 return nullptr;
138 auto imagePixels = std::make_unique<UINT[]>(height * width);
139 int lsSrc = width * 3;
140 int vsDest = height - 1;
141 for (int y = 0; y < height; y++)
143 int linePosSrc = (vsDest - y) * lsSrc;
144 int linePosDest = y * width;
145 for (int x = 0; x < width; x++)
147 int currentDestPos = linePosDest + x;
148 int currentSrcPos = linePosSrc + x * 3;
149 imagePixels[currentDestPos] = ((static_cast<UINT>(
151 ((pixelsIconRGB[currentSrcPos + 2] /*Red*/)
152 | (pixelsIconRGB[currentSrcPos + 1] << 8 /*Green*/))
153 | pixelsIconRGB[currentSrcPos] << 16 /*Blue*/
155 | ((alphaPixels[currentSrcPos] ? 0 : 0xff) << 24))) & 0xffffffff);
158 return imagePixels;
161 void CSciEdit::SetColors(bool recolorize)
163 if (CTheme::Instance().IsDarkTheme())
165 SetClassLongPtr(GetSafeHwnd(), GCLP_HBRBACKGROUND, reinterpret_cast<LONG_PTR>(GetStockObject(BLACK_BRUSH)));
166 Call(SCI_STYLESETFORE, STYLE_DEFAULT, !IsWindowEnabled() ? ::CTheme::darkDisabledTextColor : CTheme::darkTextColor);
167 Call(SCI_STYLESETBACK, STYLE_DEFAULT, !IsWindowEnabled() ? ::GetSysColor(COLOR_BTNFACE) : CTheme::darkBkColor);
168 Call(SCI_SETCARETFORE, CTheme::darkTextColor);
170 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST, RGB(187, 187, 187));
171 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST_BACK, RGB(15, 15, 15));
172 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED, RGB(187, 187, 187));
173 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED_BACK, RGB(80, 80, 80));
175 else
177 SetClassLongPtr(GetSafeHwnd(), GCLP_HBRBACKGROUND, reinterpret_cast<LONG_PTR>(GetSysColorBrush(COLOR_3DFACE)));
178 Call(SCI_STYLESETFORE, STYLE_DEFAULT, ::GetSysColor(!IsWindowEnabled() ? COLOR_GRAYTEXT : COLOR_WINDOWTEXT));
179 Call(SCI_STYLESETBACK, STYLE_DEFAULT, ::GetSysColor(!IsWindowEnabled() ? COLOR_BTNFACE : COLOR_WINDOW));
180 Call(SCI_SETCARETFORE, ::GetSysColor(COLOR_WINDOWTEXT));
182 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST);
183 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST_BACK);
184 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED);
185 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED_BACK);
187 Call(SCI_SETSELFORE, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHTTEXT)));
188 Call(SCI_SETSELBACK, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHT)));
190 if (!m_bNoAutomaticStyling)
192 Call(SCI_STYLECLEARALL);
194 LPARAM color = ::GetSysColor(COLOR_HOTLIGHT);
195 // set the styles for the bug ID strings
196 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLD, TRUE);
197 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLD, color);
198 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLDITALIC, TRUE);
199 Call(SCI_STYLESETITALIC, STYLE_ISSUEBOLDITALIC, TRUE);
200 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLDITALIC, color);
201 Call(SCI_STYLESETHOTSPOT, STYLE_ISSUEBOLDITALIC, TRUE);
203 // set the formatted text styles
204 Call(SCI_STYLESETBOLD, STYLE_BOLD, TRUE);
205 Call(SCI_STYLESETITALIC, STYLE_ITALIC, TRUE);
206 Call(SCI_STYLESETUNDERLINE, STYLE_UNDERLINED, TRUE);
208 // set the style for URLs
209 Call(SCI_STYLESETFORE, STYLE_URL, color);
210 Call(SCI_STYLESETHOTSPOT, STYLE_URL, TRUE);
212 Call(SCI_SETHOTSPOTACTIVEUNDERLINE, TRUE);
215 if (recolorize)
216 Call(SCI_COLOURISE, 0, -1);
219 void CSciEdit::Init(LONG lLanguage)
221 //Setup the direct access data
222 m_DirectFunction = SendMessage(SCI_GETDIRECTFUNCTION, 0, 0);
223 m_DirectPointer = SendMessage(SCI_GETDIRECTPOINTER, 0, 0);
224 Call(SCI_SETMARGINWIDTHN, 1, 0);
225 Call(SCI_SETUSETABS, 0); //pressing TAB inserts spaces
226 Call(SCI_SETWRAPVISUALFLAGS, SC_WRAPVISUALFLAG_END);
227 Call(SCI_AUTOCSETIGNORECASE, 1);
228 Call(SCI_SETILEXER, 0, reinterpret_cast<sptr_t>(nullptr));
229 Call(SCI_SETCODEPAGE, SC_CP_UTF8);
230 Call(SCI_AUTOCSETFILLUPS, 0, reinterpret_cast<LPARAM>("\t(["));
231 Call(SCI_AUTOCSETMAXWIDTH, 0);
232 //Set the default windows colors for edit controls
233 SetColors(false);
234 Call(SCI_SETMODEVENTMASK, SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_PERFORMED_UNDO | SC_PERFORMED_REDO);
235 Call(SCI_INDICSETSTYLE, INDIC_MISSPELLED, INDIC_SQUIGGLE);
236 Call(SCI_INDICSETFORE, INDIC_MISSPELLED, RGB(255,0,0));
237 CStringA sWordChars;
238 CStringA sWhiteSpace;
239 for (int i=0; i<255; ++i)
241 if (i == '\r' || i == '\n')
242 continue;
243 else if (i < 0x20 || i == ' ')
244 sWhiteSpace += static_cast<char>(i);
245 else if (isalnum(i) || i == '\'' || i == '_' || i == '-')
246 sWordChars += static_cast<char>(i);
248 Call(SCI_SETWORDCHARS, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sWordChars)));
249 Call(SCI_SETWHITESPACECHARS, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sWhiteSpace)));
250 m_bDoStyle = static_cast<DWORD>(CRegStdDWORD(L"Software\\TortoiseGit\\StyleCommitMessages", TRUE)) == TRUE;
251 m_nAutoCompleteMinChars = static_cast<int>(CRegStdDWORD(L"Software\\TortoiseGit\\AutoCompleteMinChars", 3));
252 // look for dictionary files and use them if found
253 if (lLanguage >= 0 && static_cast<DWORD>(CRegStdDWORD(L"Software\\TortoiseGit\\Spellchecker", TRUE)) == TRUE)
255 long langId = GetUserDefaultLCID();
256 long origLangId = langId;
257 if (lLanguage > 0)
259 // if a specific language is requested, then use that
260 langId = lLanguage;
261 origLangId = lLanguage;
264 // first try the Win8 spell checker
265 BOOL supported = FALSE;
266 HRESULT hr = CoCreateInstance(__uuidof(SpellCheckerFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_spellCheckerFactory));
267 bool bFallbackUsed = false;
268 if (SUCCEEDED(hr) && static_cast<DWORD>(CRegDWORD(L"Software\\TortoiseGit\\Win8SpellChecker", FALSE)) == TRUE)
270 wchar_t localename[LOCALE_NAME_MAX_LENGTH] = { 0 };
273 LCIDToLocaleName(langId, localename, _countof(localename), 0);
274 supported = FALSE;
275 hr = m_spellCheckerFactory->IsSupported(localename, &supported);
276 if (supported)
278 hr = m_spellCheckerFactory->CreateSpellChecker(localename, &m_SpellChecker);
279 if (SUCCEEDED(hr))
281 m_personalDict.Init(langId);
282 break;
285 DWORD lid = SUBLANGID(langId);
286 --lid;
287 if (lid > 0)
288 langId = MAKELANGID(PRIMARYLANGID(langId), lid);
289 else if (langId == 1033)
290 langId = 0;
291 else
293 langId = 1033;
294 bFallbackUsed = true;
296 } while (langId && (!supported || FAILED(hr)));
298 if (FAILED(hr) || !supported || bFallbackUsed)
300 if (bFallbackUsed)
301 langId = origLangId;
302 if ((lLanguage == 0) || (lLanguage && !LoadDictionaries(lLanguage)))
306 LoadDictionaries(langId);
307 DWORD lid = SUBLANGID(langId);
308 --lid;
309 if (lid > 0)
310 langId = MAKELANGID(PRIMARYLANGID(langId), lid);
311 else if (langId == 1033)
312 langId = 0;
313 else
315 if (bFallbackUsed && supported)
316 langId = 0;
317 else
318 langId = 1033;
320 } while (langId && !pChecker);
322 if (bFallbackUsed && pChecker)
323 m_SpellChecker = nullptr;
327 Call(SCI_SETEDGEMODE, EDGE_NONE);
328 Call(SCI_SETWRAPMODE, SC_WRAP_WORD);
329 Call(SCI_ASSIGNCMDKEY, SCK_END, SCI_LINEENDWRAP);
330 Call(SCI_ASSIGNCMDKEY, SCK_END + (SCMOD_SHIFT << 16), SCI_LINEENDWRAPEXTEND);
331 Call(SCI_ASSIGNCMDKEY, SCK_HOME, SCI_HOMEWRAP);
332 Call(SCI_ASSIGNCMDKEY, SCK_HOME + (SCMOD_SHIFT << 16), SCI_HOMEWRAPEXTEND);
333 if (CRegStdDWORD(L"Software\\TortoiseGit\\ScintillaDirect2D", FALSE) != FALSE)
335 // set font quality for the popup window, since that window does not use D2D
336 Call(SCI_SETFONTQUALITY, SC_EFF_QUALITY_LCD_OPTIMIZED);
337 // now enable D2D
338 Call(SCI_SETTECHNOLOGY, SC_TECHNOLOGY_DIRECTWRITERETAIN);
339 Call(SCI_SETBUFFEREDDRAW, 0);
341 m_themeCallbackId = CTheme::Instance().RegisterThemeChangeCallback([this]() { OnSysColorChange(); });
345 void CSciEdit::Init(const ProjectProperties& props)
347 Init(props.lProjectLanguage);
348 m_sCommand = CUnicodeUtils::GetUTF8(props.GetCheckRe());
349 m_sBugID = CUnicodeUtils::GetUTF8(props.GetBugIDRe());
350 m_sUrl = CUnicodeUtils::GetUTF8(props.sUrl);
352 Call(SCI_SETMOUSEDWELLTIME, 333);
354 if (props.nLogWidthMarker)
356 Call(SCI_SETWRAPMODE, SC_WRAP_NONE);
357 Call(SCI_SETEDGEMODE, EDGE_LINE);
358 Call(SCI_SETEDGECOLUMN, props.nLogWidthMarker);
359 Call(SCI_SETSCROLLWIDTHTRACKING, TRUE);
360 Call(SCI_SETSCROLLWIDTH, 1);
362 else
364 Call(SCI_SETEDGEMODE, EDGE_NONE);
365 Call(SCI_SETWRAPMODE, SC_WRAP_WORD);
369 void CSciEdit::SetIcon(const std::map<int, UINT> &icons)
371 int iconWidth = GetSystemMetrics(SM_CXSMICON);
372 int iconHeight = GetSystemMetrics(SM_CYSMICON);
373 Call(SCI_RGBAIMAGESETWIDTH, iconWidth);
374 Call(SCI_RGBAIMAGESETHEIGHT, iconHeight);
375 for (auto icon : icons)
377 CAutoIcon hIcon = LoadIconEx(AfxGetInstanceHandle(), MAKEINTRESOURCE(icon.second), iconWidth, iconHeight);
378 auto bytes = Icon2Image(hIcon);
379 Call(SCI_REGISTERRGBAIMAGE, icon.first, reinterpret_cast<LPARAM>(bytes.get()));
383 BOOL CSciEdit::LoadDictionaries(LONG lLanguageID)
385 // Setup the spell checker
386 wchar_t buf[6] = { 0 };
387 CString sFolderUp = CPathUtils::GetAppParentDirectory();
388 CString sFolderAppData = CPathUtils::GetAppDataDirectory();
389 CString sFile;
391 GetLocaleInfo(MAKELCID(lLanguageID, SORT_DEFAULT), LOCALE_SISO639LANGNAME, buf, _countof(buf));
392 sFile = buf;
393 if (lLanguageID == 2074)
394 sFile += L"-Latn";
395 sFile += L'_';
396 GetLocaleInfo(MAKELCID(lLanguageID, SORT_DEFAULT), LOCALE_SISO3166CTRYNAME, buf, _countof(buf));
397 sFile += buf;
398 if (!pChecker)
400 if ((PathFileExists(sFolderAppData + L"dic\\" + sFile + L".aff")) &&
401 (PathFileExists(sFolderAppData + L"dic\\" + sFile + L".dic")))
403 pChecker = std::make_unique<Hunspell>(CStringA(sFolderAppData + L"dic\\" + sFile + L".aff"), CStringA(sFolderAppData + L"dic\\" + sFile + L".dic"));
405 else if ((PathFileExists(sFolderUp + L"Languages\\" + sFile + L".aff")) &&
406 (PathFileExists(sFolderUp + L"Languages\\" + sFile + L".dic")))
408 pChecker = std::make_unique<Hunspell>(CStringA(sFolderUp + L"Languages\\" + sFile + L".aff"), CStringA(sFolderUp + L"Languages\\" + sFile + L".dic"));
410 if (pChecker)
412 const char* encoding = pChecker->get_dic_encoding();
413 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": %s\n", encoding);
414 m_spellcodepage = 0;
415 for (int i = 0; i < _countof(enc2locale); ++i)
417 if (strcmp(encoding, enc2locale[i].def_enc) == 0)
418 m_spellcodepage = atoi(enc2locale[i].cp);
420 m_personalDict.Init(lLanguageID);
423 if (pChecker)
424 return TRUE;
425 return FALSE;
428 LRESULT CSciEdit::Call(UINT message, WPARAM wParam, LPARAM lParam)
430 ASSERT(::IsWindow(m_hWnd)); //Window must be valid
431 ASSERT(m_DirectFunction); //Direct function must be valid
432 return reinterpret_cast<SciFnDirect>(m_DirectFunction)(m_DirectPointer, message, wParam, lParam);
435 CString CSciEdit::StringFromControl(const CStringA& text)
437 CString sText;
438 #ifdef UNICODE
439 int codepage = static_cast<int>(Call(SCI_GETCODEPAGE));
440 int reslen = MultiByteToWideChar(codepage, 0, text, text.GetLength(), 0, 0);
441 MultiByteToWideChar(codepage, 0, text, text.GetLength(), sText.GetBuffer(reslen+1), reslen+1);
442 sText.ReleaseBuffer(reslen);
443 #else
444 sText = text;
445 #endif
446 return sText;
449 CStringA CSciEdit::StringForControl(const CString& text)
451 CStringA sTextA;
452 #ifdef UNICODE
453 int codepage = static_cast<int>(SendMessage(SCI_GETCODEPAGE));
454 int reslen = WideCharToMultiByte(codepage, 0, text, text.GetLength(), 0, 0, 0, 0);
455 WideCharToMultiByte(codepage, 0, text, text.GetLength(), sTextA.GetBuffer(reslen), reslen, 0, 0);
456 sTextA.ReleaseBuffer(reslen);
457 #else
458 sTextA = text;
459 #endif
460 ATLTRACE("string length %d\n", sTextA.GetLength());
461 return sTextA;
464 void CSciEdit::SetText(const CString& sText)
466 auto readonly = m_bReadOnly;
467 SCOPE_EXIT { SetReadOnly(readonly); };
468 SetReadOnly(false);
470 CStringA sTextA = StringForControl(sText);
471 Call(SCI_SETTEXT, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sTextA)));
473 if (Call(SCI_GETSCROLLWIDTHTRACKING) != 0)
474 Call(SCI_SETSCROLLWIDTH, 1);
476 // Scintilla seems to have problems with strings that
477 // aren't terminated by a newline char. Once that char
478 // is there, it can be removed without problems.
479 // So we add here a newline, then remove it again.
480 Call(SCI_DOCUMENTEND);
481 Call(SCI_NEWLINE);
482 Call(SCI_DELETEBACK);
485 void CSciEdit::InsertText(const CString& sText, bool bNewLine)
487 auto readonly = m_bReadOnly;
488 SCOPE_EXIT { SetReadOnly(readonly); };
489 SetReadOnly(false);
491 CStringA sTextA = StringForControl(sText);
492 Call(SCI_REPLACESEL, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sTextA)));
493 if (bNewLine)
494 Call(SCI_REPLACESEL, 0, reinterpret_cast<LPARAM>("\n"));
497 CString CSciEdit::GetText()
499 auto len = static_cast<int>(Call(SCI_GETTEXT, 0, 0));
500 CStringA sTextA;
501 Call(SCI_GETTEXT, len + 1, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(CStrBufA(sTextA, len + 1))));
502 return StringFromControl(sTextA);
505 CString CSciEdit::GetWordUnderCursor(bool bSelectWord, bool allchars)
507 Sci_TextRange textrange;
508 auto pos = static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS));
509 textrange.chrg.cpMin = static_cast<int>(Call(SCI_WORDSTARTPOSITION, pos, TRUE));
510 if ((pos == textrange.chrg.cpMin)||(textrange.chrg.cpMin < 0))
511 return CString();
512 textrange.chrg.cpMax = static_cast<int>(Call(SCI_WORDENDPOSITION, textrange.chrg.cpMin, TRUE));
514 auto textbuffer = std::make_unique<char[]>(textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
515 textrange.lpstrText = textbuffer.get();
516 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
517 CString sRet = StringFromControl(textbuffer.get());
518 if (m_bDoStyle && !allchars)
520 for (const auto styleindicator : { '*', '_', '^' })
522 if (sRet.IsEmpty())
523 break;
524 if (sRet[sRet.GetLength() - 1] == styleindicator)
526 --textrange.chrg.cpMax;
527 sRet.Truncate(sRet.GetLength() - 1);
529 if (sRet.IsEmpty())
530 break;
531 if (sRet[0] == styleindicator)
533 ++textrange.chrg.cpMin;
534 sRet = sRet.Right(sRet.GetLength() - 1);
538 if (bSelectWord)
539 Call(SCI_SETSEL, textrange.chrg.cpMin, textrange.chrg.cpMax);
540 return sRet;
543 void CSciEdit::SetFont(CString sFontName, int iFontSizeInPoints)
545 Call(SCI_STYLESETFONT, STYLE_DEFAULT, reinterpret_cast<LPARAM>(CUnicodeUtils::GetUTF8(sFontName).GetBuffer()));
546 Call(SCI_STYLESETSIZE, STYLE_DEFAULT, iFontSizeInPoints);
547 Call(SCI_STYLECLEARALL);
549 LPARAM color = GetSysColor(COLOR_HOTLIGHT);
550 // set the styles for the bug ID strings
551 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLD, TRUE);
552 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLD, color);
553 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLDITALIC, TRUE);
554 Call(SCI_STYLESETITALIC, STYLE_ISSUEBOLDITALIC, TRUE);
555 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLDITALIC, color);
556 Call(SCI_STYLESETHOTSPOT, STYLE_ISSUEBOLDITALIC, TRUE);
558 // set the formatted text styles
559 Call(SCI_STYLESETBOLD, STYLE_BOLD, TRUE);
560 Call(SCI_STYLESETITALIC, STYLE_ITALIC, TRUE);
561 Call(SCI_STYLESETUNDERLINE, STYLE_UNDERLINED, TRUE);
563 // set the style for URLs
564 Call(SCI_STYLESETFORE, STYLE_URL, color);
565 Call(SCI_STYLESETHOTSPOT, STYLE_URL, TRUE);
567 Call(SCI_SETHOTSPOTACTIVEUNDERLINE, TRUE);
570 void CSciEdit::SetAutoCompletionList(std::map<CString, int>&& list, wchar_t separator, wchar_t typeSeparator)
572 //copy the auto completion list.
574 //SK: instead of creating a copy of that list, we could accept a pointer
575 //to the list and use that instead. But then the caller would have to make
576 //sure that the list persists over the lifetime of the control!
577 m_autolist.clear();
578 m_autolist = std::move(list);
579 m_separator = separator;
580 m_typeSeparator = typeSeparator;
583 // Helper for CSciEdit::IsMisspelled()
584 // Returns TRUE if sWord has spelling errors.
585 BOOL CSciEdit::CheckWordSpelling(const CString& sWord)
587 if (m_SpellChecker)
589 IEnumSpellingErrorPtr enumSpellingError = nullptr;
590 if (SUCCEEDED(m_SpellChecker->Check(sWord, &enumSpellingError)))
592 ISpellingErrorPtr spellingError = nullptr;
593 if (enumSpellingError->Next(&spellingError) == S_OK)
595 CORRECTIVE_ACTION action = CORRECTIVE_ACTION_NONE;
596 spellingError->get_CorrectiveAction(&action);
597 if (action != CORRECTIVE_ACTION_NONE)
598 return TRUE;
602 else if (pChecker)
604 // convert the string from the control to the encoding of the spell checker module.
605 auto sWordA = GetWordForSpellChecker(sWord);
606 if (!pChecker->spell(sWordA))
607 return TRUE;
610 return FALSE;
613 BOOL CSciEdit::IsMisspelled(const CString& sWord)
615 // words starting with a digit are treated as correctly spelled
616 if (_istdigit(sWord.GetAt(0)))
617 return FALSE;
618 // words in the personal dictionary are correct too
619 if (m_personalDict.FindWord(sWord))
620 return FALSE;
622 // Check spell checking cache first.
623 const BOOL *cacheResult = m_SpellingCache.try_get(std::wstring(sWord, sWord.GetLength()));
624 if (cacheResult)
625 return *cacheResult;
627 // convert the string from the control to the encoding of the spell checker module.
628 auto sWordA = GetWordForSpellChecker(sWord);
630 // now we actually check the spelling...
631 BOOL misspelled = CheckWordSpelling(sWord);
632 if (misspelled)
634 // the word is marked as misspelled, we now check whether the word
635 // is maybe a composite identifier
636 // a composite identifier consists of multiple words, with each word
637 // separated by a change in lower to uppercase letters
638 misspelled = FALSE;
639 if (sWord.GetLength() > 1)
641 int wordstart = 0;
642 int wordend = 1;
643 while (wordend < sWord.GetLength())
645 while ((wordend < sWord.GetLength())&&(!_istupper(sWord[wordend])))
646 wordend++;
647 if ((wordstart == 0)&&(wordend == sWord.GetLength()))
649 // words in the auto list are also assumed correctly spelled
650 if (m_autolist.find(sWord) != m_autolist.end())
651 misspelled = FALSE;
652 else
653 misspelled = TRUE;
654 break;
656 CString token(sWord.Mid(wordstart, wordend - wordstart));
657 if (token.GetLength() > 2 && CheckWordSpelling(token))
659 misspelled = TRUE;
660 break;
662 wordstart = wordend;
663 wordend++;
668 // Update cache.
669 m_SpellingCache.insert_or_assign(std::wstring(sWord, sWord.GetLength()), misspelled);
670 return misspelled;
673 void CSciEdit::CheckSpelling(Sci_Position startpos, Sci_Position endpos)
675 if (m_bReadOnly)
676 return;
678 if (!pChecker && !m_SpellChecker)
679 return;
681 Sci_TextRange textrange;
682 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(startpos);
683 textrange.chrg.cpMax = textrange.chrg.cpMin;
684 auto lastpos = endpos;
685 if (lastpos < 0)
686 lastpos = static_cast<int>(Call(SCI_GETLENGTH)) - textrange.chrg.cpMin;
687 Call(SCI_SETINDICATORCURRENT, INDIC_MISSPELLED);
688 while (textrange.chrg.cpMax < lastpos)
690 textrange.chrg.cpMin = static_cast<int>(Call(SCI_WORDSTARTPOSITION, textrange.chrg.cpMax + 1, TRUE));
691 if (textrange.chrg.cpMin < textrange.chrg.cpMax)
692 break;
693 textrange.chrg.cpMax = static_cast<int>(Call(SCI_WORDENDPOSITION, textrange.chrg.cpMin, TRUE));
694 if (textrange.chrg.cpMin == textrange.chrg.cpMax)
696 textrange.chrg.cpMax++;
697 // since Scintilla squiggles to the end of the text even if told to stop one char before it,
698 // we have to clear here the squiggly lines to the end.
699 if (textrange.chrg.cpMin)
700 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin-1, textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
701 continue;
703 ATLASSERT(textrange.chrg.cpMax >= textrange.chrg.cpMin);
704 auto textbuffer = std::make_unique<char[]>(textrange.chrg.cpMax - textrange.chrg.cpMin + 2);
705 textrange.lpstrText = textbuffer.get();
706 textrange.chrg.cpMax++;
707 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
708 auto len = static_cast<int>(strlen(textrange.lpstrText));
709 if (len == 0)
711 textrange.chrg.cpMax--;
712 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
713 len = static_cast<int>(strlen(textrange.lpstrText));
714 textrange.chrg.cpMax++;
715 len++;
717 if (len && textrange.lpstrText[len - 1] == '.')
719 // Try to ignore file names from the auto list.
720 // Do do this, for each word ending with '.' we extract next word and check
721 // whether the combined string is present in auto list.
722 Sci_TextRange twoWords;
723 twoWords.chrg.cpMin = textrange.chrg.cpMin;
724 twoWords.chrg.cpMax = static_cast<int>(Call(SCI_WORDENDPOSITION, textrange.chrg.cpMax + 1, TRUE));
725 auto twoWordsBuffer = std::make_unique<char[]>(twoWords.chrg.cpMax - twoWords.chrg.cpMin + 1);
726 twoWords.lpstrText = twoWordsBuffer.get();
727 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&twoWords));
728 CString sWord = StringFromControl(twoWords.lpstrText);
729 if (m_autolist.find(sWord) != m_autolist.end())
731 //mark word as correct (remove the squiggle line)
732 Call(SCI_INDICATORCLEARRANGE, twoWords.chrg.cpMin, twoWords.chrg.cpMax - twoWords.chrg.cpMin);
733 textrange.chrg.cpMax = twoWords.chrg.cpMax;
734 continue;
737 if (len)
738 textrange.lpstrText[len - 1] = '\0';
739 textrange.chrg.cpMax--;
740 if (textrange.lpstrText[0])
742 CString sWord = StringFromControl(textrange.lpstrText);
743 if ((GetStyleAt(textrange.chrg.cpMin) != STYLE_URL) && IsMisspelled(sWord))
745 //mark word as misspelled
746 Call(SCI_INDICATORFILLRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin);
748 else
750 //mark word as correct (remove the squiggle line)
751 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin);
752 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
758 void CSciEdit::SuggestSpellingAlternatives()
760 if (!pChecker && !m_SpellChecker)
761 return;
762 CString word = GetWordUnderCursor(true);
763 Call(SCI_SETCURRENTPOS, Call(SCI_WORDSTARTPOSITION, Call(SCI_GETCURRENTPOS), TRUE));
764 if (word.IsEmpty())
765 return;
767 CString suggestions;
768 if (m_SpellChecker)
770 IEnumStringPtr enumSpellingSuggestions = nullptr;
771 if (SUCCEEDED(m_SpellChecker->Suggest(word, &enumSpellingSuggestions)))
773 LPOLESTR string = nullptr;
774 while (enumSpellingSuggestions->Next(1, &string, nullptr) == S_OK)
776 suggestions.AppendFormat(L"%s%c%d%c", (LPCWSTR)CString(string), m_typeSeparator, AUTOCOMPLETE_SPELLING, m_separator);
777 CoTaskMemFree(string);
781 else if (pChecker)
783 auto wlst = pChecker->suggest(GetWordForSpellChecker(word));
784 if (!wlst.empty())
786 for (const auto& alternative : wlst)
787 suggestions.AppendFormat(L"%s%c%d%c", static_cast<LPCWSTR>(GetWordFromSpellChecker(alternative)), m_typeSeparator, AUTOCOMPLETE_SPELLING, m_separator);
791 suggestions.TrimRight(m_separator);
792 if (suggestions.IsEmpty())
793 return;
794 Call(SCI_AUTOCSETSEPARATOR, CStringA(m_separator).GetAt(0));
795 Call(SCI_AUTOCSETTYPESEPARATOR, m_typeSeparator);
796 Call(SCI_AUTOCSETDROPRESTOFWORD, 1);
797 Call(SCI_AUTOCSHOW, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(StringForControl(suggestions))));
798 SetWindowStylesForAutocompletionPopup();
801 void CSciEdit::DoAutoCompletion(Sci_Position nMinPrefixLength)
803 if (m_autolist.empty())
804 return;
805 auto pos = static_cast<int>(static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS)));
806 if (pos != static_cast<int>(Call(SCI_WORDENDPOSITION, pos, TRUE)))
807 return; // don't auto complete if we're not at the end of a word
808 CString word = GetWordUnderCursor();
809 if (word.GetLength() < nMinPrefixLength)
811 word = GetWordUnderCursor(false, true);
812 if (word.GetLength() < nMinPrefixLength)
813 return; // don't auto complete yet, word is too short
815 CString sAutoCompleteList;
817 for (int i = 0; i < 2; ++i)
819 std::vector<CString> words;
821 pos = word.Find('-');
823 CString wordLower = word;
824 wordLower.MakeLower();
825 CString wordHigher = word;
826 wordHigher.MakeUpper();
828 words.push_back(word);
829 words.push_back(wordLower);
830 words.push_back(wordHigher);
832 if (pos >= 0)
834 CString s = wordLower.Left(pos);
835 if (s.GetLength() >= nMinPrefixLength)
836 words.push_back(s);
837 s = wordLower.Mid(pos + 1);
838 if (s.GetLength() >= nMinPrefixLength)
839 words.push_back(s);
840 s = wordHigher.Left(pos);
841 if (s.GetLength() >= nMinPrefixLength)
842 words.push_back(wordHigher.Left(pos));
843 s = wordHigher.Mid(pos + 1);
844 if (s.GetLength() >= nMinPrefixLength)
845 words.push_back(wordHigher.Mid(pos+1));
848 // note: the m_autolist is case-sensitive because
849 // its contents are also used to mark words in it
850 // as correctly spelled. If it would be case-insensitive,
851 // case spelling mistakes would not show up as misspelled.
852 std::map<CString, int> wordset;
853 for (const auto& w : words)
855 for (auto lowerit = m_autolist.lower_bound(w);
856 lowerit != m_autolist.end(); ++lowerit)
858 int compare = w.CompareNoCase(lowerit->first.Left(w.GetLength()));
859 if (compare > 0)
860 continue;
861 else if (compare == 0)
862 wordset.emplace(lowerit->first, lowerit->second);
863 else
864 break;
868 for (const auto& w : wordset)
869 sAutoCompleteList.AppendFormat(L"%s%c%d%c", static_cast<LPCWSTR>(w.first), m_typeSeparator, w.second, m_separator);
871 sAutoCompleteList.TrimRight(m_separator);
873 if (i == 0)
875 if (sAutoCompleteList.IsEmpty())
877 // retry with all chars
878 word = GetWordUnderCursor(false, true);
880 else
881 break;
883 if (i == 1)
885 if (sAutoCompleteList.IsEmpty())
886 return;
890 Call(SCI_AUTOCSETSEPARATOR, CStringA(m_separator).GetAt(0));
891 Call(SCI_AUTOCSETTYPESEPARATOR, (m_typeSeparator));
892 auto sForControl = StringForControl(sAutoCompleteList);
893 Call(SCI_AUTOCSHOW, StringForControl(word).GetLength(), reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sForControl)));
894 SetWindowStylesForAutocompletionPopup();
897 BOOL CSciEdit::OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult)
899 if (message != WM_NOTIFY)
900 return CWnd::OnChildNotify(message, wParam, lParam, pLResult);
902 auto lpnmhdr = reinterpret_cast<LPNMHDR>(lParam);
903 auto lpSCN = reinterpret_cast<SCNotification*>(lParam);
905 if(lpnmhdr->hwndFrom==m_hWnd)
907 switch(lpnmhdr->code)
909 case SCN_CHARADDED:
911 if ((lpSCN->ch < 32)&&(lpSCN->ch != 13)&&(lpSCN->ch != 10))
912 Call(SCI_DELETEBACK);
913 else
914 DoAutoCompletion(m_nAutoCompleteMinChars);
915 return TRUE;
917 break;
918 case SCN_AUTOCSELECTION:
920 CString text = StringFromControl(lpSCN->text);
921 if (m_autolist[text] == AUTOCOMPLETE_SNIPPET)
923 Call(SCI_AUTOCCANCEL);
924 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
926 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
927 pHandler->HandleSnippet(m_autolist[text], text, this);
930 return TRUE;
932 case SCN_STYLENEEDED:
934 auto startpos = static_cast<Sci_Position>(Call(SCI_GETENDSTYLED));
935 auto endpos = reinterpret_cast<SCNotification*>(lpnmhdr)->position;
937 auto startwordpos = static_cast<int>(Call(SCI_WORDSTARTPOSITION, startpos, true));
938 auto endwordpos = static_cast<int>(Call(SCI_WORDENDPOSITION, endpos, true));
940 MarkEnteredBugID(startwordpos, endwordpos);
941 if (m_bDoStyle)
942 StyleEnteredText(startwordpos, endwordpos);
944 StyleURLs(startwordpos, endwordpos);
945 CheckSpelling(startwordpos, endwordpos);
947 // Tell scintilla editor that we styled all requested range.
948 Call(SCI_STARTSTYLING, endwordpos);
949 Call(SCI_SETSTYLING, 0, 0);
951 break;
952 case SCN_MODIFIED:
954 if (!m_blockModifiedHandler && (lpSCN->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)))
956 auto firstline = static_cast<int>(Call(SCI_GETFIRSTVISIBLELINE));
957 auto lastline = firstline + static_cast<int>(Call(SCI_LINESONSCREEN));
958 auto firstpos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, firstline));
959 auto lastpos = static_cast<Sci_Position>(Call(SCI_GETLINEENDPOSITION, lastline));
960 auto pos1 = lpSCN->position;
961 auto pos2 = pos1 + lpSCN->length;
962 // always use the bigger range
963 firstpos = min(firstpos, pos1);
964 lastpos = max(lastpos, pos2);
966 WrapLines(firstpos, lastpos);
968 break;
970 case SCN_DWELLSTART:
971 case SCN_HOTSPOTRELEASECLICK:
973 Sci_TextRange textrange;
974 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(lpSCN->position);
975 textrange.chrg.cpMax = static_cast<Sci_PositionCR>(lpSCN->position);
976 auto style = GetStyleAt(lpSCN->position);
977 if (style != STYLE_ISSUEBOLDITALIC && style != STYLE_URL)
978 break;
979 while (GetStyleAt(textrange.chrg.cpMin - 1) == style)
980 --textrange.chrg.cpMin;
981 while (GetStyleAt(textrange.chrg.cpMax + 1) == style)
982 ++textrange.chrg.cpMax;
983 ++textrange.chrg.cpMax;
984 auto textbuffer = std::make_unique<char[]>(textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
985 textrange.lpstrText = textbuffer.get();
986 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
987 CString url;
988 if (style == STYLE_URL)
990 url = StringFromControl(textbuffer.get());
991 if (url.Find(L'@') > 0 && !PathIsURL(url))
992 url = L"mailto:" + url;
994 else
996 url = m_sUrl;
997 ProjectProperties::ReplaceBugIDPlaceholder(url, StringFromControl(textbuffer.get()));
999 if (!url.IsEmpty())
1001 if (lpnmhdr->code == SCN_HOTSPOTRELEASECLICK)
1002 ShellExecute(GetParent()->GetSafeHwnd(), L"open", url, nullptr, nullptr, SW_SHOWDEFAULT);
1003 else
1005 CStringA sTextA = StringForControl(url);
1006 Call(SCI_CALLTIPSHOW, lpSCN->position + 3, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sTextA)));
1010 break;
1011 case SCN_DWELLEND:
1012 Call(SCI_CALLTIPCANCEL);
1013 break;
1016 return CWnd::OnChildNotify(message, wParam, lParam, pLResult);
1019 BEGIN_MESSAGE_MAP(CSciEdit, CWnd)
1020 ON_WM_KEYDOWN()
1021 ON_WM_CONTEXTMENU()
1022 ON_WM_SYSCOLORCHANGE()
1023 END_MESSAGE_MAP()
1025 void CSciEdit::OnSysColorChange()
1027 __super::OnSysColorChange();
1028 CTheme::Instance().OnSysColorChanged();
1029 SetColors(true);
1030 if (m_bUDiffmode)
1031 SetUDiffStyle();
1034 void CSciEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
1036 switch (nChar)
1038 case (VK_ESCAPE):
1040 if ((Call(SCI_AUTOCACTIVE)==0)&&(Call(SCI_CALLTIPACTIVE)==0))
1041 ::SendMessage(GetParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
1043 break;
1045 CWnd::OnKeyDown(nChar, nRepCnt, nFlags);
1048 BOOL CSciEdit::PreTranslateMessage(MSG* pMsg)
1050 if (pMsg->message == WM_KEYDOWN)
1052 switch (pMsg->wParam)
1054 case VK_SPACE:
1056 if ((GetKeyState(VK_CONTROL) & 0x8000) && ((GetKeyState(VK_MENU) & 0x8000) == 0))
1058 DoAutoCompletion(1);
1059 return TRUE;
1062 break;
1063 case VK_TAB:
1064 // The TAB cannot be handled in OnKeyDown because it is too late by then.
1066 if ((GetKeyState(VK_CONTROL) & 0x8000) && ((GetKeyState(VK_MENU) & 0x8000) == 0))
1068 //Ctrl-Tab was pressed, this means we should provide the user with
1069 //a list of possible spell checking alternatives to the word under
1070 //the cursor
1071 SuggestSpellingAlternatives();
1072 return TRUE;
1074 else if (!Call(SCI_AUTOCACTIVE))
1076 ::PostMessage(GetParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
1077 return TRUE;
1080 break;
1083 return CWnd::PreTranslateMessage(pMsg);
1086 void CSciEdit::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
1088 auto anchor = static_cast<Sci_Position>(Call(SCI_GETANCHOR));
1089 auto currentpos = static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS));
1090 auto selstart = static_cast<int>(static_cast<Sci_Position>(Call(SCI_GETSELECTIONSTART)));
1091 auto selend = static_cast<int>(static_cast<Sci_Position>(Call(SCI_GETSELECTIONEND)));
1092 Sci_Position pointpos = 0;
1093 if ((point.x == -1) && (point.y == -1))
1095 CRect rect;
1096 GetClientRect(&rect);
1097 ClientToScreen(&rect);
1098 point = rect.CenterPoint();
1099 pointpos = static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS));
1101 else
1103 // change the cursor position to the point where the user
1104 // right-clicked.
1105 CPoint clientpoint = point;
1106 ScreenToClient(&clientpoint);
1107 pointpos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMPOINT, clientpoint.x, clientpoint.y));
1109 CString sMenuItemText;
1110 CMenu popup;
1111 bool bRestoreCursor = true;
1112 if (popup.CreatePopupMenu())
1114 bool bCanUndo = !!Call(SCI_CANUNDO);
1115 bool bCanRedo = !!Call(SCI_CANREDO);
1116 bool bHasSelection = (selend-selstart > 0);
1117 bool bCanPaste = !!Call(SCI_CANPASTE);
1118 bool bIsReadOnly = !!Call(SCI_GETREADONLY);
1119 UINT uEnabledMenu = MF_STRING | MF_ENABLED;
1120 UINT uDisabledMenu = MF_STRING | MF_GRAYED;
1122 // find the word under the cursor
1123 CString sWord;
1124 if (pointpos)
1126 // setting the cursor clears the selection
1127 Call(SCI_SETANCHOR, pointpos);
1128 Call(SCI_SETCURRENTPOS, pointpos);
1129 sWord = GetWordUnderCursor();
1130 // restore the selection
1131 Call(SCI_SETSELECTIONSTART, selstart);
1132 Call(SCI_SETSELECTIONEND, selend);
1134 else
1135 sWord = GetWordUnderCursor();
1136 auto worda = GetWordForSpellChecker(sWord);
1138 int nCorrections = 1;
1139 // check if the word under the cursor is spelled wrong
1140 if (!bIsReadOnly && (pChecker || m_SpellChecker) && !worda.empty())
1142 if (m_SpellChecker)
1144 IEnumStringPtr enumSpellingSuggestions = nullptr;
1145 if (SUCCEEDED(m_SpellChecker->Suggest(sWord, &enumSpellingSuggestions)))
1147 LPOLESTR string = nullptr;
1148 while (enumSpellingSuggestions->Next(1, &string, nullptr) == S_OK)
1150 popup.InsertMenu((UINT)-1, 0, nCorrections++, string);
1151 CoTaskMemFree(string);
1153 popup.AppendMenu(MF_SEPARATOR);
1156 else if (pChecker)
1158 // get the spell suggestions
1159 auto wlst = pChecker->suggest(worda);
1160 if (!wlst.empty())
1162 // add the suggestions to the context menu
1163 for (const auto& alternative : wlst)
1165 CString sug = GetWordFromSpellChecker(alternative);
1166 popup.InsertMenu(static_cast<UINT>(-1), 0, nCorrections++, sug);
1168 popup.AppendMenu(MF_SEPARATOR);
1173 // also allow the user to add the word to the custom dictionary so
1174 // it won't show up as misspelled anymore
1175 if (!bIsReadOnly && (sWord.GetLength() < PDICT_MAX_WORD_LENGTH) && (m_autolist.find(sWord) == m_autolist.end() && IsMisspelled(sWord)) &&
1176 (!_istdigit(sWord.GetAt(0))) && (!m_personalDict.FindWord(sWord)))
1178 sMenuItemText.Format(IDS_SCIEDIT_ADDWORD, static_cast<LPCWSTR>(sWord));
1179 popup.AppendMenu(uEnabledMenu, SCI_ADDWORD, sMenuItemText);
1180 // another separator
1181 popup.AppendMenu(MF_SEPARATOR);
1184 // add the 'default' entries
1185 sMenuItemText.LoadString(IDS_SCIEDIT_UNDO);
1186 popup.AppendMenu(bCanUndo ? uEnabledMenu : uDisabledMenu, SCI_UNDO, sMenuItemText);
1187 sMenuItemText.LoadString(IDS_SCIEDIT_REDO);
1188 popup.AppendMenu(bCanRedo ? uEnabledMenu : uDisabledMenu, SCI_REDO, sMenuItemText);
1190 popup.AppendMenu(MF_SEPARATOR);
1192 sMenuItemText.LoadString(IDS_SCIEDIT_CUT);
1193 popup.AppendMenu(!bIsReadOnly && bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_CUT, sMenuItemText);
1194 sMenuItemText.LoadString(IDS_SCIEDIT_COPY);
1195 popup.AppendMenu(bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_COPY, sMenuItemText);
1196 sMenuItemText.LoadString(IDS_SCIEDIT_PASTE);
1197 popup.AppendMenu(bCanPaste ? uEnabledMenu : uDisabledMenu, SCI_PASTE, sMenuItemText);
1199 popup.AppendMenu(MF_SEPARATOR);
1201 sMenuItemText.LoadString(IDS_SCIEDIT_SELECTALL);
1202 popup.AppendMenu(uEnabledMenu, SCI_SELECTALL, sMenuItemText);
1204 if (!bIsReadOnly && Call(SCI_GETEDGECOLUMN))
1206 popup.AppendMenu(MF_SEPARATOR);
1208 sMenuItemText.LoadString(IDS_SCIEDIT_SPLITLINES);
1209 popup.AppendMenu(bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_LINESSPLIT, sMenuItemText);
1212 if (m_arContextHandlers.GetCount() > 0)
1213 popup.AppendMenu(MF_SEPARATOR);
1215 int nCustoms = nCorrections;
1216 // now add any custom context menus
1217 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
1219 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
1220 pHandler->InsertMenuItems(popup, nCustoms);
1223 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1224 switch (cmd)
1226 case 0:
1227 break; // no command selected
1228 case SCI_SELECTALL:
1229 bRestoreCursor = false;
1230 [[fallthrough]];
1231 case SCI_UNDO:
1232 case SCI_REDO:
1233 case SCI_CUT:
1234 case SCI_COPY:
1235 case SCI_PASTE:
1236 Call(cmd);
1237 break;
1238 case SCI_ADDWORD:
1239 m_personalDict.AddWord(sWord);
1240 CheckSpelling(static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, static_cast<int>(Call(SCI_GETFIRSTVISIBLELINE)))), static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, static_cast<int>(Call(SCI_GETFIRSTVISIBLELINE)) + static_cast<int>(Call(SCI_LINESONSCREEN)))));
1241 break;
1242 case SCI_LINESSPLIT:
1244 auto marker = static_cast<int>(Call(SCI_GETEDGECOLUMN) * static_cast<int>(Call(SCI_TEXTWIDTH, 0, reinterpret_cast<LPARAM>(" "))));
1245 if (marker)
1247 m_blockModifiedHandler = true;
1248 SCOPE_EXIT{ m_blockModifiedHandler = false; };
1249 Call(SCI_TARGETFROMSELECTION);
1250 Call(SCI_LINESJOIN);
1251 Call(SCI_LINESSPLIT, marker);
1254 break;
1255 default:
1256 if (cmd < nCorrections)
1258 Call(SCI_SETANCHOR, pointpos);
1259 Call(SCI_SETCURRENTPOS, pointpos);
1260 GetWordUnderCursor(true);
1261 CString temp;
1262 popup.GetMenuString(cmd, temp, 0);
1263 // setting the cursor clears the selection
1264 Call(SCI_REPLACESEL, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(StringForControl(temp))));
1266 else if (cmd < (nCorrections+nCustoms))
1268 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
1270 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
1271 if (pHandler->HandleMenuItemClick(cmd, this))
1272 break;
1277 if (bRestoreCursor)
1279 // restore the anchor and cursor position
1280 Call(SCI_SETCURRENTPOS, currentpos);
1281 Call(SCI_SETANCHOR, anchor);
1285 bool CSciEdit::StyleEnteredText(Sci_Position startstylepos, Sci_Position endstylepos)
1287 bool bStyled = false;
1288 const auto line = static_cast<int>(Call(SCI_LINEFROMPOSITION, startstylepos));
1289 const auto line_number_end = static_cast<int>(Call(SCI_LINEFROMPOSITION, endstylepos));
1290 for (auto line_number = line; line_number <= line_number_end; ++line_number)
1292 auto offset = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, line_number));
1293 auto line_len = static_cast<int>(Call(SCI_LINELENGTH, line_number));
1294 auto linebuffer = std::make_unique<char[]>(line_len + 1);
1295 Call(SCI_GETLINE, line_number, reinterpret_cast<LPARAM>(linebuffer.get()));
1296 linebuffer[line_len] = '\0';
1297 Sci_Position start = 0;
1298 Sci_Position end = 0;
1299 while (FindStyleChars(linebuffer.get(), '*', start, end))
1301 Call(SCI_STARTSTYLING, start + offset, STYLE_BOLD);
1302 Call(SCI_SETSTYLING, end-start, STYLE_BOLD);
1303 bStyled = true;
1304 start = end;
1306 start = 0;
1307 end = 0;
1308 while (FindStyleChars(linebuffer.get(), '^', start, end))
1310 Call(SCI_STARTSTYLING, start + offset, STYLE_ITALIC);
1311 Call(SCI_SETSTYLING, end-start, STYLE_ITALIC);
1312 bStyled = true;
1313 start = end;
1315 start = 0;
1316 end = 0;
1317 while (FindStyleChars(linebuffer.get(), '_', start, end))
1319 Call(SCI_STARTSTYLING, start + offset, STYLE_UNDERLINED);
1320 Call(SCI_SETSTYLING, end-start, STYLE_UNDERLINED);
1321 bStyled = true;
1322 start = end;
1325 return bStyled;
1328 bool CSciEdit::WrapLines(Sci_Position startpos, Sci_Position endpos)
1330 auto markerX = static_cast<Sci_Position>(Call(SCI_GETEDGECOLUMN) * static_cast<int>(Call(SCI_TEXTWIDTH, 0, reinterpret_cast<LPARAM>(" "))));
1331 if (markerX)
1333 Call(SCI_SETTARGETSTART, startpos);
1334 Call(SCI_SETTARGETEND, endpos);
1335 Call(SCI_LINESSPLIT, markerX);
1336 return true;
1338 return false;
1341 void CSciEdit::AdvanceUTF8(const char * str, int& pos)
1343 if ((str[pos] & 0xE0)==0xC0)
1345 // utf8 2-byte sequence
1346 pos += 2;
1348 else if ((str[pos] & 0xF0)==0xE0)
1350 // utf8 3-byte sequence
1351 pos += 3;
1353 else if ((str[pos] & 0xF8)==0xF0)
1355 // utf8 4-byte sequence
1356 pos += 4;
1358 else
1359 pos++;
1362 bool CSciEdit::FindStyleChars(const char* line, char styler, Sci_Position& start, Sci_Position& end)
1364 int i=0;
1365 int u=0;
1366 while (i < start)
1368 AdvanceUTF8(line, i);
1369 u++;
1372 bool bFoundMarker = false;
1373 CString sULine = CUnicodeUtils::GetUnicode(line);
1374 // find a starting marker
1375 while (line[i] != 0)
1377 if (line[i] == styler)
1379 if ((line[i+1]!=0)&&(IsCharAlphaNumeric(sULine[u+1]))&&
1380 (((u>0)&&(!IsCharAlphaNumeric(sULine[u-1]))) || (u==0)))
1382 start = i+1;
1383 AdvanceUTF8(line, i);
1384 u++;
1385 bFoundMarker = true;
1386 break;
1389 AdvanceUTF8(line, i);
1390 u++;
1392 if (!bFoundMarker)
1393 return false;
1394 // find ending marker
1395 bFoundMarker = false;
1396 while (line[i])
1398 if (line[i] == styler)
1400 if ((IsCharAlphaNumeric(sULine[u-1]))&&
1401 ((((u+1)<sULine.GetLength())&&(!IsCharAlphaNumeric(sULine[u+1]))) || ((u+1) == sULine.GetLength()))
1404 end = i;
1405 i++;
1406 bFoundMarker = true;
1407 break;
1410 AdvanceUTF8(line, i);
1411 u++;
1413 return bFoundMarker;
1416 BOOL CSciEdit::MarkEnteredBugID(Sci_Position startstylepos, Sci_Position endstylepos)
1418 if (m_sCommand.IsEmpty())
1419 return FALSE;
1420 // get the text between the start and end position we have to style
1421 const auto line_number = static_cast<int>(Call(SCI_LINEFROMPOSITION, startstylepos));
1422 auto start_pos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, line_number));
1423 auto end_pos = endstylepos;
1425 if (start_pos == end_pos)
1426 return FALSE;
1427 if (start_pos > end_pos)
1429 auto switchtemp = start_pos;
1430 start_pos = end_pos;
1431 end_pos = switchtemp;
1434 auto textbuffer = std::make_unique<char[]>(end_pos - start_pos + 2);
1435 Sci_TextRange textrange;
1436 textrange.lpstrText = textbuffer.get();
1437 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(start_pos);
1438 textrange.chrg.cpMax = static_cast<Sci_PositionCR>(end_pos);
1439 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
1440 CStringA msg = CStringA(textbuffer.get());
1442 Call(SCI_STARTSTYLING, start_pos, STYLE_MASK);
1446 if (!m_sBugID.IsEmpty())
1448 // match with two regex strings (without grouping!)
1449 const std::regex regCheck(m_sCommand);
1450 const std::regex regBugID(m_sBugID);
1451 const std::sregex_iterator end;
1452 std::string s { static_cast<LPCSTR>(msg) };
1453 LONG pos = 0;
1454 // note:
1455 // if start_pos is 0, we're styling from the beginning and let the ^ char match the beginning of the line
1456 // that way, the ^ matches the very beginning of the log message and not the beginning of further lines.
1457 // problem is: this only works *while* entering log messages. If a log message is pasted in whole or
1458 // multiple lines are pasted, start_pos can be 0 and styling goes over multiple lines. In that case, those
1459 // additional line starts also match ^
1460 for (std::sregex_iterator it(s.cbegin(), s.cend(), regCheck, start_pos != 0 ? std::regex_constants::match_not_bol : std::regex_constants::match_default); it != end; ++it)
1462 // clear the styles up to the match position
1463 Call(SCI_SETSTYLING, it->position(0)-pos, STYLE_DEFAULT);
1465 // (*it)[0] is the matched string
1466 std::string matchedString = (*it)[0];
1467 LONG matchedpos = 0;
1468 for (std::sregex_iterator it2(matchedString.cbegin(), matchedString.cend(), regBugID); it2 != end; ++it2)
1470 ATLTRACE("matched id : %s\n", std::string((*it2)[0]).c_str());
1472 // bold style up to the id match
1473 ATLTRACE("position = %ld\n", it2->position(0));
1474 if (it2->position(0))
1475 Call(SCI_SETSTYLING, it2->position(0) - matchedpos, STYLE_ISSUEBOLD);
1476 // bold and recursive style for the bug ID itself
1477 if ((*it2)[0].str().size())
1478 Call(SCI_SETSTYLING, (*it2)[0].str().size(), STYLE_ISSUEBOLDITALIC);
1479 matchedpos = static_cast<LONG>(it2->position(0) + (*it2)[0].str().size());
1481 if (matchedpos&& matchedpos < static_cast<LONG>(matchedString.size()))
1483 Call(SCI_SETSTYLING, matchedString.size() - matchedpos, STYLE_ISSUEBOLD);
1485 pos = static_cast<LONG>(it->position(0) + matchedString.size());
1487 // bold style for the rest of the string which isn't matched
1488 if (s.size()-pos)
1489 Call(SCI_SETSTYLING, s.size()-pos, STYLE_DEFAULT);
1491 else
1493 const std::regex regCheck(m_sCommand);
1494 const std::sregex_iterator end;
1495 std::string s { static_cast<LPCSTR>(msg) };
1496 LONG pos = 0;
1497 for (std::sregex_iterator it(s.cbegin(), s.cend(), regCheck); it != end; ++it)
1499 // clear the styles up to the match position
1500 if (it->position(0) - pos >= 0)
1501 Call(SCI_SETSTYLING, it->position(0) - pos, STYLE_DEFAULT);
1502 pos = static_cast<LONG>(it->position(0));
1504 const std::smatch match = *it;
1505 // we define group 1 as the whole issue text and
1506 // group 2 as the bug ID
1507 if (match.size() >= 2)
1509 ATLTRACE("matched id : %s\n", std::string(match[1]).c_str());
1510 if (match[1].first - s.cbegin() - pos >= 0)
1511 Call(SCI_SETSTYLING, match[1].first - s.cbegin() - pos, STYLE_ISSUEBOLD);
1512 Call(SCI_SETSTYLING, std::string(match[1]).size(), STYLE_ISSUEBOLDITALIC);
1513 pos = static_cast<LONG>(match[1].second - s.cbegin());
1518 catch (std::exception&) {}
1520 return FALSE;
1523 //similar code in AppUtils.cpp
1524 bool CSciEdit::IsValidURLChar(unsigned char ch)
1526 return isalnum(ch) ||
1527 ch == '_' || ch == '/' || ch == ';' || ch == '?' || ch == '&' || ch == '=' ||
1528 ch == '%' || ch == ':' || ch == '.' || ch == '#' || ch == '-' || ch == '+' ||
1529 ch == '|' || ch == '>' || ch == '<' || ch == '!' || ch == '@' || ch == '~';
1532 //similar code in AppUtils.cpp
1533 void CSciEdit::StyleURLs(Sci_Position startstylepos, Sci_Position endstylepos)
1535 const auto line_number = static_cast<int>(Call(SCI_LINEFROMPOSITION, startstylepos));
1536 startstylepos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, line_number));
1538 auto len = endstylepos - startstylepos + 1;
1539 auto textbuffer = std::make_unique<char[]>(len + 1);
1540 Sci_TextRange textrange;
1541 textrange.lpstrText = textbuffer.get();
1542 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(startstylepos);
1543 textrange.chrg.cpMax = static_cast<Sci_PositionCR>(endstylepos);
1544 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
1545 // we're dealing with utf8 encoded text here, which means one glyph is
1546 // not necessarily one byte/wchar_t
1547 // that's why we use CStringA to still get a correct char index
1548 CStringA msg = textbuffer.get();
1550 int starturl = -1;
1551 for (int i = 0; i <= msg.GetLength(); AdvanceUTF8(msg, i))
1553 if ((i < len) && IsValidURLChar(msg[i]))
1555 if (starturl < 0)
1556 starturl = i;
1558 else
1560 if (starturl >= 0)
1562 bool strip = true;
1563 if (msg[starturl] == '<' && i < len) // try to detect and do not strip URLs put within <>
1565 while (starturl <= i && msg[starturl] == '<') // strip leading '<'
1566 ++starturl;
1567 strip = false;
1568 i = starturl;
1569 while (i < len && msg[i] != '\r' && msg[i] != '\n' && msg[i] != '>') // find first '>' or new line after resetting i to start position
1570 AdvanceUTF8(msg, i);
1573 int skipTrailing = 0;
1574 while (strip && i - skipTrailing - 1 > starturl && (msg[i - skipTrailing - 1] == '.' || msg[i - skipTrailing - 1] == '-' || msg[i - skipTrailing - 1] == '?' || msg[i - skipTrailing - 1] == ';' || msg[i - skipTrailing - 1] == ':' || msg[i - skipTrailing - 1] == '>' || msg[i - skipTrailing - 1] == '<' || msg[i - skipTrailing - 1] == '!'))
1575 ++skipTrailing;
1577 if (!IsUrlOrEmail(msg.Mid(starturl, i - starturl - skipTrailing)))
1579 starturl = -1;
1580 continue;
1583 ASSERT(startstylepos + i - skipTrailing <= endstylepos);
1584 Call(SCI_STARTSTYLING, startstylepos + starturl, STYLE_URL);
1585 Call(SCI_SETSTYLING, i - starturl - skipTrailing, STYLE_URL);
1587 starturl = -1;
1592 bool CSciEdit::IsUrlOrEmail(const CStringA& sText)
1594 if (!PathIsURLA(sText))
1596 auto atpos = sText.Find('@');
1597 if (atpos <= 0)
1598 return false;
1599 if (sText.Find('.', atpos) <= atpos + 1) // a dot must follow after the @, but not directly after it
1600 return false;
1601 if (sText.Find(':', atpos) < 0) // do not detect git@example.com:something as an email address
1602 return true;
1603 return false;
1605 for (const CStringA& prefix : { "http://", "https://", "git://", "ftp://", "file://", "mailto:" })
1607 if (strncmp(sText, prefix, prefix.GetLength()) == 0 && sText.GetLength() != prefix.GetLength())
1608 return true;
1610 return false;
1613 std::string CSciEdit::GetWordForSpellChecker(const CString& sWord)
1615 // convert the string from the control to the encoding of the spell checker module.
1616 std::string sWordA;
1617 if (m_spellcodepage)
1619 int lengthIncTerminator = WideCharToMultiByte(m_spellcodepage, 0, sWord, -1, nullptr, 0, nullptr, nullptr);
1620 if (lengthIncTerminator <= 1)
1621 return ""; // converting to the codepage failed
1622 sWordA.resize(lengthIncTerminator - 1);
1623 WideCharToMultiByte(m_spellcodepage, 0, sWord, -1, sWordA.data(), lengthIncTerminator - 1, nullptr, nullptr);
1625 else
1626 sWordA = std::string(reinterpret_cast<LPCSTR>(static_cast<LPCWSTR>(sWord)));
1628 sWordA.erase(sWordA.find_last_not_of("\'\".,") + 1);
1629 sWordA.erase(0, sWordA.find_first_not_of("\'\".,"));
1631 if (m_bDoStyle)
1633 for (const auto styleindicator : { '*', '_', '^' })
1635 if (sWordA.empty())
1636 break;
1637 if (sWordA[sWordA.size() - 1] == styleindicator)
1638 sWordA.resize(sWordA.size() - 1);
1639 if (sWordA.empty())
1640 break;
1641 if (sWordA[0] == styleindicator)
1642 sWordA = sWordA.substr(sWordA.size() - 1);
1646 return sWordA;
1649 CString CSciEdit::GetWordFromSpellChecker(const std::string& sWordA)
1651 CString sWord;
1652 if (m_spellcodepage)
1654 wchar_t* buf = sWord.GetBuffer(static_cast<int>(sWordA.size()) * 2);
1655 int lengthIncTerminator = MultiByteToWideChar(m_spellcodepage, 0, sWordA.c_str(), -1, buf, static_cast<int>(sWordA.size()) * 2);
1656 if (lengthIncTerminator == 0)
1657 return L"";
1658 sWord.ReleaseBuffer(lengthIncTerminator - 1);
1660 else
1661 sWord = CString(sWordA.c_str());
1663 sWord.Trim(L"\'\".,");
1665 return sWord;
1668 bool CSciEdit::IsUTF8(LPVOID pBuffer, size_t cb)
1670 if (cb < 2)
1671 return true;
1672 auto pVal = static_cast<UINT16*>(pBuffer);
1673 auto pVal2 = reinterpret_cast<UINT8*>(pVal + 1);
1674 // scan the whole buffer for a 0x0000 sequence
1675 // if found, we assume a binary file
1676 for (size_t i=0; i<(cb-2); i=i+2)
1678 if (0x0000 == *pVal++)
1679 return false;
1681 pVal = static_cast<UINT16*>(pBuffer);
1682 if (*pVal == 0xFEFF)
1683 return false;
1684 if (cb < 3)
1685 return false;
1686 if (*pVal == 0xBBEF)
1688 if (*pVal2 == 0xBF)
1689 return true;
1691 // check for illegal UTF8 chars
1692 pVal2 = static_cast<UINT8*>(pBuffer);
1693 for (size_t i=0; i<cb; ++i)
1695 if ((*pVal2 == 0xC0)||(*pVal2 == 0xC1)||(*pVal2 >= 0xF5))
1696 return false;
1697 pVal2++;
1699 pVal2 = static_cast<UINT8*>(pBuffer);
1700 bool bUTF8 = false;
1701 for (size_t i=0; i<(cb-3); ++i)
1703 if ((*pVal2 & 0xE0)==0xC0)
1705 pVal2++;i++;
1706 if ((*pVal2 & 0xC0)!=0x80)
1707 return false;
1708 bUTF8 = true;
1710 if ((*pVal2 & 0xF0)==0xE0)
1712 pVal2++;i++;
1713 if ((*pVal2 & 0xC0)!=0x80)
1714 return false;
1715 pVal2++;i++;
1716 if ((*pVal2 & 0xC0)!=0x80)
1717 return false;
1718 bUTF8 = true;
1720 if ((*pVal2 & 0xF8)==0xF0)
1722 pVal2++;i++;
1723 if ((*pVal2 & 0xC0)!=0x80)
1724 return false;
1725 pVal2++;i++;
1726 if ((*pVal2 & 0xC0)!=0x80)
1727 return false;
1728 pVal2++;i++;
1729 if ((*pVal2 & 0xC0)!=0x80)
1730 return false;
1731 bUTF8 = true;
1733 pVal2++;
1735 if (bUTF8)
1736 return true;
1737 return false;
1740 void CSciEdit::SetAStyle(int style, COLORREF fore, COLORREF back, int size, const char *face)
1742 Call(SCI_STYLESETFORE, style, fore);
1743 Call(SCI_STYLESETBACK, style, back);
1744 if (size >= 1)
1745 Call(SCI_STYLESETSIZE, style, size);
1746 if (face)
1747 Call(SCI_STYLESETFONT, style, reinterpret_cast<LPARAM>(face));
1750 void CSciEdit::SetUDiffStyle()
1752 m_bUDiffmode = true;
1753 m_bDoStyle = false;
1754 Call(SCI_CLEARDOCUMENTSTYLE);
1755 Call(SCI_SETTABWIDTH, CRegStdDWORD(L"Software\\TortoiseGit\\UDiffTabSize", 4));
1757 SetReadOnly(true);
1759 //Set the default windows colors for edit controls
1760 SetColors(false);
1762 //SendEditor(SCI_SETREADONLY, FALSE);
1763 Call(SCI_CLEARALL);
1764 Call(SCI_EMPTYUNDOBUFFER);
1765 Call(SCI_SETSAVEPOINT);
1766 Call(SCI_CANCEL);
1767 Call(SCI_SETUNDOCOLLECTION, 0);
1769 Call(SCI_SETWRAPMODE,SC_WRAP_NONE);
1771 Call(SCI_GOTOPOS, 0);
1773 Call(SCI_SETVIEWWS, 1);
1774 Call(SCI_SETWHITESPACESIZE, 2);
1775 Call(SCI_SETWHITESPACEFORE, true, ::GetSysColor(COLOR_3DSHADOW));
1776 Call(SCI_STYLESETVISIBLE, STYLE_CONTROLCHAR, TRUE);
1778 Call(SCI_CLEARDOCUMENTSTYLE, 0, 0);
1780 if (CTheme::Instance().IsDarkTheme())
1782 Call(SCI_STYLESETFORE, STYLE_DEFAULT, UDiffTextColorDark);
1783 Call(SCI_STYLESETBACK, STYLE_DEFAULT, UDiffBackColorDark);
1784 Call(SCI_SETSELFORE, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHTTEXT)));
1785 Call(SCI_SETSELBACK, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHT)));
1786 Call(SCI_SETCARETFORE, UDiffTextColorDark);
1787 Call(SCI_SETWHITESPACEFORE, true, RGB(180, 180, 180));
1788 SetAStyle(STYLE_DEFAULT, UDiffTextColorDark, UDiffBackColorDark,
1789 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1790 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1791 SetAStyle(SCE_DIFF_DEFAULT, UDiffTextColorDark, UDiffBackColorDark,
1792 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1793 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1794 Call(SCI_STYLECLEARALL);
1795 SetAStyle(SCE_DIFF_COMMAND,
1796 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeCommandColor", UDIFF_COLORFORECOMMAND_DARK),
1797 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackCommandColor", UDIFF_COLORBACKCOMMAND_DARK));
1798 SetAStyle(SCE_DIFF_POSITION,
1799 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForePositionColor", UDIFF_COLORFOREPOSITION_DARK),
1800 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackPositionColor", UDIFF_COLORBACKPOSITION_DARK));
1801 SetAStyle(SCE_DIFF_HEADER,
1802 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeHeaderColor", UDIFF_COLORFOREHEADER_DARK),
1803 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackHeaderColor", UDIFF_COLORBACKHEADER_DARK));
1804 SetAStyle(SCE_DIFF_COMMENT,
1805 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeCommentColor", UDIFF_COLORFORECOMMENT_DARK),
1806 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackCommentColor", UDIFF_COLORBACKCOMMENT_DARK));
1807 for (int style : { SCE_DIFF_ADDED, SCE_DIFF_PATCH_ADD, SCE_DIFF_PATCH_DELETE })
1809 SetAStyle(style,
1810 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeAddedColor", UDIFF_COLORFOREADDED_DARK),
1811 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackAddedColor", UDIFF_COLORBACKADDED_DARK));
1813 for (int style : { SCE_DIFF_DELETED, SCE_DIFF_REMOVED_PATCH_ADD, SCE_DIFF_REMOVED_PATCH_DELETE })
1815 SetAStyle(style,
1816 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeRemovedColor", UDIFF_COLORFOREREMOVED_DARK),
1817 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackRemovedColor", UDIFF_COLORBACKREMOVED_DARK));
1820 else
1822 Call(SCI_STYLESETFORE, STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT));
1823 Call(SCI_STYLESETBACK, STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOW));
1824 Call(SCI_SETSELFORE, TRUE, ::GetSysColor(COLOR_HIGHLIGHTTEXT));
1825 Call(SCI_SETSELBACK, TRUE, ::GetSysColor(COLOR_HIGHLIGHT));
1826 Call(SCI_SETCARETFORE, ::GetSysColor(COLOR_WINDOWTEXT));
1827 Call(SCI_SETWHITESPACEFORE, true, ::GetSysColor(COLOR_3DSHADOW));
1828 SetAStyle(STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT), ::GetSysColor(COLOR_WINDOW),
1829 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1830 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1831 SetAStyle(SCE_DIFF_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT), ::GetSysColor(COLOR_WINDOW),
1832 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1833 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1834 Call(SCI_STYLECLEARALL);
1835 SetAStyle(SCE_DIFF_COMMAND,
1836 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeCommandColor", UDIFF_COLORFORECOMMAND),
1837 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackCommandColor", UDIFF_COLORBACKCOMMAND));
1838 SetAStyle(SCE_DIFF_POSITION,
1839 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForePositionColor", UDIFF_COLORFOREPOSITION),
1840 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackPositionColor", UDIFF_COLORBACKPOSITION));
1841 SetAStyle(SCE_DIFF_HEADER,
1842 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeHeaderColor", UDIFF_COLORFOREHEADER),
1843 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackHeaderColor", UDIFF_COLORBACKHEADER));
1844 SetAStyle(SCE_DIFF_COMMENT,
1845 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeCommentColor", UDIFF_COLORFORECOMMENT),
1846 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackCommentColor", UDIFF_COLORBACKCOMMENT));
1847 for (int style : { SCE_DIFF_ADDED, SCE_DIFF_PATCH_ADD, SCE_DIFF_PATCH_DELETE })
1849 SetAStyle(style,
1850 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeAddedColor", UDIFF_COLORFOREADDED),
1851 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackAddedColor", UDIFF_COLORBACKADDED));
1853 for (int style : { SCE_DIFF_DELETED, SCE_DIFF_REMOVED_PATCH_ADD, SCE_DIFF_REMOVED_PATCH_DELETE })
1855 SetAStyle(style,
1856 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeRemovedColor", UDIFF_COLORFOREREMOVED),
1857 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackRemovedColor", UDIFF_COLORBACKREMOVED));
1860 Call(SCI_SETFOLDMARGINCOLOUR, true, RGB(240, 240, 240));
1861 Call(SCI_SETFOLDMARGINHICOLOUR, true, RGB(255, 255, 255));
1862 Call(SCI_STYLESETFORE, STYLE_LINENUMBER, RGB(109, 109, 109));
1863 Call(SCI_STYLESETBACK, STYLE_LINENUMBER, RGB(230, 230, 230));
1865 Call(SCI_STYLESETFORE, STYLE_BRACELIGHT, RGB(0, 150, 0));
1866 Call(SCI_STYLESETBOLD, STYLE_BRACELIGHT, 1);
1867 Call(SCI_STYLESETFORE, STYLE_BRACEBAD, RGB(255, 0, 0));
1868 Call(SCI_STYLESETBOLD, STYLE_BRACEBAD, 1);
1869 if (CTheme::Instance().IsDarkTheme() || CTheme::Instance().IsHighContrastModeDark())
1871 Call(SCI_SETFOLDMARGINCOLOUR, true, UDiffTextColorDark);
1872 Call(SCI_SETFOLDMARGINHICOLOUR, true, CTheme::darkBkColor);
1873 Call(SCI_STYLESETFORE, STYLE_LINENUMBER, RGB(140, 140, 140));
1874 Call(SCI_STYLESETBACK, STYLE_LINENUMBER, UDiffBackColorDark);
1877 auto curlexer = Call(SCI_GETLEXER);
1878 if (CTheme::Instance().IsHighContrastMode() && curlexer != SCLEX_NULL)
1880 Call(SCI_CLEARDOCUMENTSTYLE, 0, 0);
1881 Call(SCI_SETILEXER, reinterpret_cast<sptr_t>(nullptr));
1882 Call(SCI_COLOURISE, 0, -1);
1884 else if (!CTheme::Instance().IsHighContrastMode() && curlexer != SCLEX_DIFF)
1886 Call(SCI_CLEARDOCUMENTSTYLE, 0, 0);
1887 Call(SCI_STYLESETBOLD, SCE_DIFF_COMMENT, TRUE);
1888 Call(SCI_SETILEXER, 0, reinterpret_cast<sptr_t>(CreateLexer("diff")));
1889 Call(SCI_STYLESETBOLD, SCE_DIFF_COMMENT, TRUE);
1890 Call(SCI_SETKEYWORDS, 0, reinterpret_cast<LPARAM>("revision"));
1891 Call(SCI_COLOURISE, 0, -1);
1895 int CSciEdit::LoadFromFile(CString &filename)
1897 CAutoFILE fp = _wfsopen(filename, L"rb", _SH_DENYWR);
1898 if (!fp)
1899 return -1;
1901 auto readonly = m_bReadOnly;
1902 SCOPE_EXIT { SetReadOnly(readonly); };
1903 SetReadOnly(false);
1905 Call(SCI_CLEARALL);
1906 Call(SCI_CANCEL);
1908 char data[4096] = { 0 };
1909 size_t lenFile = fread(data, 1, sizeof(data), fp);
1910 bool bUTF8 = IsUTF8(data, lenFile);
1911 while (lenFile > 0)
1913 Call(SCI_ADDTEXT, lenFile, reinterpret_cast<LPARAM>(static_cast<char *>(data)));
1914 lenFile = fread(data, 1, sizeof(data), fp);
1916 Call(SCI_SETCODEPAGE, bUTF8 ? SC_CP_UTF8 : GetACP());
1917 return 0;
1920 void CSciEdit::RestyleBugIDs()
1922 auto endstylepos = static_cast<int>(Call(SCI_GETLENGTH));
1923 // clear all styles
1924 Call(SCI_STARTSTYLING, 0, STYLE_MASK);
1925 Call(SCI_SETSTYLING, endstylepos, STYLE_DEFAULT);
1926 // style the bug IDs
1927 MarkEnteredBugID(0, endstylepos);
1930 ULONG CSciEdit::GetGestureStatus(CPoint /*ptTouch*/)
1932 return 0;
1935 BOOL CSciEdit::EnableWindow(BOOL bEnable /*= TRUE*/)
1937 auto ret = __super::EnableWindow(bEnable);
1938 SetColors(true);
1939 return ret;
1942 void CSciEdit::SetReadOnly(bool bReadOnly)
1944 m_bReadOnly = bReadOnly;
1945 Call(SCI_SETREADONLY, m_bReadOnly);
1948 void CSciEdit::ClearUndoBuffer()
1950 Call(SCI_EMPTYUNDOBUFFER);
1951 Call(SCI_SETSAVEPOINT);
1954 void CSciEdit::SetWindowStylesForAutocompletionPopup()
1956 if (CTheme::Instance().IsDarkTheme())
1958 EnumThreadWindows(GetCurrentThreadId(), AdjustThemeProc, 0);
1962 BOOL CSciEdit::AdjustThemeProc(HWND hwnd, LPARAM /*lParam*/)
1964 wchar_t szWndClassName[MAX_PATH] = { 0 };
1965 GetClassName(hwnd, szWndClassName, _countof(szWndClassName));
1966 if ((wcscmp(szWndClassName, L"ListBoxX") == 0) ||
1967 (wcscmp(szWndClassName, WC_LISTBOX) == 0))
1969 // in dark mode, the resizing border is visible at the top
1970 // of the popup, and it's white and ugly.
1971 // this removes the border, but that also means that the
1972 // popup is not resizable anymore - which I think is not
1973 // really necessary anyway.
1974 auto dwCurStyle = static_cast<DWORD>(GetWindowLongPtr(hwnd, GWL_STYLE));
1975 dwCurStyle &= ~WS_THICKFRAME;
1976 dwCurStyle |= WS_BORDER;
1977 SetWindowLongPtr(hwnd, GWL_STYLE, dwCurStyle);
1979 DarkModeHelper::Instance().AllowDarkModeForWindow(hwnd, TRUE);
1980 SetWindowTheme(hwnd, L"Explorer", nullptr);
1981 EnumChildWindows(hwnd, AdjustThemeProc, 0);
1984 return TRUE;