Prepare release and bump version numbers to 2.17.0.2
[TortoiseGit.git] / src / Utils / MiscUI / SciEdit.cpp
bloba67bc12b404657b4eba7ab84d81f5382ca4b4afa
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2009-2024 - 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"
33 #include "URLFinder.h"
35 void CSciEditContextMenuInterface::InsertMenuItems(CMenu&, int&) {return;}
36 bool CSciEditContextMenuInterface::HandleMenuItemClick(int, CSciEdit *) {return false;}
37 void CSciEditContextMenuInterface::HandleSnippet(int, const CString &, CSciEdit *) { return; }
40 #define STYLE_ISSUEBOLD 11
41 #define STYLE_ISSUEBOLDITALIC 12
42 #define STYLE_BOLD 14
43 #define STYLE_ITALIC 15
44 #define STYLE_UNDERLINED 16
45 #define STYLE_URL 17
46 #define INDIC_MISSPELLED 18
48 #define STYLE_MASK 0x1f
50 #define SCI_ADDWORD 2000
52 struct loc_map {
53 const char * cp;
54 const char * def_enc;
57 struct loc_map enc2locale[] = {
58 {"28591","ISO8859-1"},
59 {"28592","ISO8859-2"},
60 {"28593","ISO8859-3"},
61 {"28594","ISO8859-4"},
62 {"28595","ISO8859-5"},
63 {"28596","ISO8859-6"},
64 {"28597","ISO8859-7"},
65 {"28598","ISO8859-8"},
66 {"28599","ISO8859-9"},
67 {"28605","ISO8859-15"},
68 {"20866","KOI8-R"},
69 {"21866","KOI8-U"},
70 {"1251","microsoft-cp1251"},
71 {"65001","UTF-8"},
75 IMPLEMENT_DYNAMIC(CSciEdit, CWnd)
77 CSciEdit::CSciEdit()
79 m_hModule = ::LoadLibrary(L"SciLexer_tgit.dll");
82 CSciEdit::~CSciEdit()
84 CTheme::Instance().RemoveRegisteredCallback(m_themeCallbackId);
85 m_personalDict.Save();
88 static std::unique_ptr<UINT[]> Icon2Image(HICON hIcon)
90 if (hIcon == nullptr)
91 return nullptr;
93 ICONINFO iconInfo;
94 if (!GetIconInfo(hIcon, &iconInfo))
95 return nullptr;
96 SCOPE_EXIT
98 DeleteObject(iconInfo.hbmColor);
99 DeleteObject(iconInfo.hbmMask);
102 BITMAP bm;
103 if (!GetObject(iconInfo.hbmColor, sizeof(BITMAP), &bm))
104 return nullptr;
106 int width = bm.bmWidth;
107 int height = bm.bmHeight;
108 int bytesPerScanLine = (width * 3 + 3) & 0xFFFFFFFC;
109 int size = bytesPerScanLine * height;
110 BITMAPINFO infoheader;
111 infoheader.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
112 infoheader.bmiHeader.biWidth = width;
113 infoheader.bmiHeader.biHeight = height;
114 infoheader.bmiHeader.biPlanes = 1;
115 infoheader.bmiHeader.biBitCount = 24;
116 infoheader.bmiHeader.biCompression = BI_RGB;
117 infoheader.bmiHeader.biSizeImage = size;
119 auto ptrb = std::make_unique<BYTE[]>(size * 2 + height * width * 4);
120 LPBYTE pixelsIconRGB = ptrb.get();
121 LPBYTE alphaPixels = pixelsIconRGB + size;
122 HDC hDC = CreateCompatibleDC(nullptr);
123 SCOPE_EXIT { DeleteDC(hDC); };
124 HBITMAP hBmpOld = static_cast<HBITMAP>(SelectObject(hDC, iconInfo.hbmColor));
125 if (!GetDIBits(hDC, iconInfo.hbmColor, 0, height, static_cast<LPVOID>(pixelsIconRGB), &infoheader, DIB_RGB_COLORS))
126 return nullptr;
128 SelectObject(hDC, hBmpOld);
129 if (!GetDIBits(hDC, iconInfo.hbmMask, 0, height, static_cast<LPVOID>(alphaPixels), &infoheader, DIB_RGB_COLORS))
130 return nullptr;
132 auto imagePixels = std::make_unique<UINT[]>(height * width);
133 int lsSrc = width * 3;
134 int vsDest = height - 1;
135 for (int y = 0; y < height; y++)
137 int linePosSrc = (vsDest - y) * lsSrc;
138 int linePosDest = y * width;
139 for (int x = 0; x < width; x++)
141 int currentDestPos = linePosDest + x;
142 int currentSrcPos = linePosSrc + x * 3;
143 imagePixels[currentDestPos] = ((static_cast<UINT>(
145 ((pixelsIconRGB[currentSrcPos + 2] /*Red*/)
146 | (pixelsIconRGB[currentSrcPos + 1] << 8 /*Green*/))
147 | pixelsIconRGB[currentSrcPos] << 16 /*Blue*/
149 | ((alphaPixels[currentSrcPos] ? 0 : 0xff) << 24))) & 0xffffffff);
152 return imagePixels;
155 void CSciEdit::SetColors(bool recolorize)
157 if (CTheme::Instance().IsDarkTheme())
159 SetClassLongPtr(GetSafeHwnd(), GCLP_HBRBACKGROUND, reinterpret_cast<LONG_PTR>(GetStockObject(BLACK_BRUSH)));
160 Call(SCI_STYLESETFORE, STYLE_DEFAULT, !IsWindowEnabled() ? ::CTheme::darkDisabledTextColor : CTheme::darkTextColor);
161 Call(SCI_STYLESETBACK, STYLE_DEFAULT, !IsWindowEnabled() ? ::GetSysColor(COLOR_BTNFACE) : CTheme::darkBkColor);
162 Call(SCI_SETCARETFORE, CTheme::darkTextColor);
164 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST, RGB(187, 187, 187));
165 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST_BACK, RGB(15, 15, 15));
166 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED, RGB(187, 187, 187));
167 Call(SCI_SETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED_BACK, RGB(80, 80, 80));
169 else
171 SetClassLongPtr(GetSafeHwnd(), GCLP_HBRBACKGROUND, reinterpret_cast<LONG_PTR>(GetSysColorBrush(COLOR_3DFACE)));
172 Call(SCI_STYLESETFORE, STYLE_DEFAULT, ::GetSysColor(!IsWindowEnabled() ? COLOR_GRAYTEXT : COLOR_WINDOWTEXT));
173 Call(SCI_STYLESETBACK, STYLE_DEFAULT, ::GetSysColor(!IsWindowEnabled() ? COLOR_BTNFACE : COLOR_WINDOW));
174 Call(SCI_SETCARETFORE, ::GetSysColor(COLOR_WINDOWTEXT));
176 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST);
177 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST_BACK);
178 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED);
179 Call(SCI_RESETELEMENTCOLOUR, SC_ELEMENT_LIST_SELECTED_BACK);
181 Call(SCI_SETSELFORE, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHTTEXT)));
182 Call(SCI_SETSELBACK, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHT)));
184 if (!m_bNoAutomaticStyling)
186 Call(SCI_STYLECLEARALL);
188 LPARAM color = ::GetSysColor(COLOR_HOTLIGHT);
189 // set the styles for the bug ID strings
190 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLD, TRUE);
191 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLD, color);
192 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLDITALIC, TRUE);
193 Call(SCI_STYLESETITALIC, STYLE_ISSUEBOLDITALIC, TRUE);
194 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLDITALIC, color);
195 Call(SCI_STYLESETHOTSPOT, STYLE_ISSUEBOLDITALIC, TRUE);
197 // set the formatted text styles
198 Call(SCI_STYLESETBOLD, STYLE_BOLD, TRUE);
199 Call(SCI_STYLESETITALIC, STYLE_ITALIC, TRUE);
200 Call(SCI_STYLESETUNDERLINE, STYLE_UNDERLINED, TRUE);
202 // set the style for URLs
203 Call(SCI_STYLESETFORE, STYLE_URL, color);
204 Call(SCI_STYLESETHOTSPOT, STYLE_URL, TRUE);
206 Call(SCI_SETHOTSPOTACTIVEUNDERLINE, TRUE);
209 if (recolorize)
210 Call(SCI_COLOURISE, 0, -1);
213 void CSciEdit::Init(LONG lLanguage)
215 //Setup the direct access data
216 m_DirectFunction = SendMessage(SCI_GETDIRECTFUNCTION, 0, 0);
217 m_DirectPointer = SendMessage(SCI_GETDIRECTPOINTER, 0, 0);
218 Call(SCI_SETMARGINWIDTHN, 1, 0);
219 Call(SCI_SETUSETABS, 0); //pressing TAB inserts spaces
220 Call(SCI_SETWRAPVISUALFLAGS, SC_WRAPVISUALFLAG_END);
221 Call(SCI_AUTOCSETIGNORECASE, 1);
222 Call(SCI_SETILEXER, 0, reinterpret_cast<sptr_t>(nullptr));
223 Call(SCI_SETCODEPAGE, SC_CP_UTF8);
224 Call(SCI_AUTOCSETFILLUPS, 0, reinterpret_cast<LPARAM>("\t(["));
225 Call(SCI_AUTOCSETMAXWIDTH, 0);
226 //Set the default windows colors for edit controls
227 SetColors(false);
228 Call(SCI_SETMODEVENTMASK, SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_PERFORMED_UNDO | SC_PERFORMED_REDO);
229 Call(SCI_INDICSETSTYLE, INDIC_MISSPELLED, INDIC_SQUIGGLE);
230 Call(SCI_INDICSETFORE, INDIC_MISSPELLED, RGB(255,0,0));
231 CStringA sWordChars;
232 CStringA sWhiteSpace;
233 for (int i=0; i<255; ++i)
235 if (i == '\r' || i == '\n')
236 continue;
237 else if (i < 0x20 || i == ' ')
238 sWhiteSpace += static_cast<char>(i);
239 else if (isalnum(i) || i == '\'' || i == '_' || i == '-')
240 sWordChars += static_cast<char>(i);
242 Call(SCI_SETWORDCHARS, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sWordChars)));
243 Call(SCI_SETWHITESPACECHARS, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sWhiteSpace)));
244 m_bDoStyle = static_cast<DWORD>(CRegStdDWORD(L"Software\\TortoiseGit\\StyleCommitMessages", TRUE)) == TRUE;
245 m_nAutoCompleteMinChars = static_cast<int>(CRegStdDWORD(L"Software\\TortoiseGit\\AutoCompleteMinChars", 3));
246 // look for dictionary files and use them if found
247 if (lLanguage >= 0 && static_cast<DWORD>(CRegStdDWORD(L"Software\\TortoiseGit\\Spellchecker", TRUE)) == TRUE)
249 long langId = GetUserDefaultLCID();
250 long origLangId = langId;
251 if (lLanguage > 0)
253 // if a specific language is requested, then use that
254 langId = lLanguage;
255 origLangId = lLanguage;
258 // first try the Win8 spell checker
259 BOOL supported = FALSE;
260 HRESULT hr = CoCreateInstance(__uuidof(SpellCheckerFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_spellCheckerFactory));
261 bool bFallbackUsed = false;
262 if (SUCCEEDED(hr) && static_cast<DWORD>(CRegDWORD(L"Software\\TortoiseGit\\Win8SpellChecker", FALSE)) == TRUE)
264 wchar_t localename[LOCALE_NAME_MAX_LENGTH] = { 0 };
267 LCIDToLocaleName(langId, localename, _countof(localename), 0);
268 supported = FALSE;
269 hr = m_spellCheckerFactory->IsSupported(localename, &supported);
270 if (supported)
272 hr = m_spellCheckerFactory->CreateSpellChecker(localename, &m_SpellChecker);
273 if (SUCCEEDED(hr))
275 m_personalDict.Init(langId);
276 break;
279 DWORD lid = SUBLANGID(langId);
280 --lid;
281 if (lid > 0)
282 langId = MAKELANGID(PRIMARYLANGID(langId), lid);
283 else if (langId == 1033)
284 langId = 0;
285 else
287 langId = 1033;
288 bFallbackUsed = true;
290 } while (langId && (!supported || FAILED(hr)));
292 if (FAILED(hr) || !supported || bFallbackUsed)
294 if (bFallbackUsed)
295 langId = origLangId;
296 if ((lLanguage == 0) || (lLanguage && !LoadDictionaries(lLanguage)))
300 LoadDictionaries(langId);
301 DWORD lid = SUBLANGID(langId);
302 --lid;
303 if (lid > 0)
304 langId = MAKELANGID(PRIMARYLANGID(langId), lid);
305 else if (langId == 1033)
306 langId = 0;
307 else
309 if (bFallbackUsed && supported)
310 langId = 0;
311 else
312 langId = 1033;
314 } while (langId && !pChecker);
316 if (bFallbackUsed && pChecker)
317 m_SpellChecker = nullptr;
321 Call(SCI_SETEDGEMODE, EDGE_NONE);
322 Call(SCI_SETWRAPMODE, SC_WRAP_WORD);
323 Call(SCI_ASSIGNCMDKEY, SCK_END, SCI_LINEENDWRAP);
324 Call(SCI_ASSIGNCMDKEY, SCK_END + (SCMOD_SHIFT << 16), SCI_LINEENDWRAPEXTEND);
325 Call(SCI_ASSIGNCMDKEY, SCK_HOME, SCI_HOMEWRAP);
326 Call(SCI_ASSIGNCMDKEY, SCK_HOME + (SCMOD_SHIFT << 16), SCI_HOMEWRAPEXTEND);
327 if (CRegStdDWORD(L"Software\\TortoiseGit\\ScintillaDirect2D", FALSE) != FALSE)
329 // set font quality for the popup window, since that window does not use D2D
330 Call(SCI_SETFONTQUALITY, SC_EFF_QUALITY_LCD_OPTIMIZED);
331 // now enable D2D
332 Call(SCI_SETTECHNOLOGY, SC_TECHNOLOGY_DIRECTWRITERETAIN);
333 Call(SCI_SETBUFFEREDDRAW, 0);
335 m_themeCallbackId = CTheme::Instance().RegisterThemeChangeCallback([this]() { OnSysColorChange(); });
339 void CSciEdit::Init(const ProjectProperties& props)
341 Init(props.lProjectLanguage);
342 m_sCommand = CUnicodeUtils::GetUTF8(props.GetCheckRe());
343 m_sBugID = CUnicodeUtils::GetUTF8(props.GetBugIDRe());
344 m_sUrl = CUnicodeUtils::GetUTF8(props.sUrl);
346 Call(SCI_SETMOUSEDWELLTIME, 333);
348 if (props.nLogWidthMarker)
350 Call(SCI_SETWRAPMODE, SC_WRAP_NONE);
351 Call(SCI_SETEDGEMODE, EDGE_LINE);
352 Call(SCI_SETEDGECOLUMN, props.nLogWidthMarker);
353 Call(SCI_SETSCROLLWIDTHTRACKING, TRUE);
354 Call(SCI_SETSCROLLWIDTH, 1);
356 else
358 Call(SCI_SETEDGEMODE, EDGE_NONE);
359 Call(SCI_SETWRAPMODE, SC_WRAP_WORD);
363 void CSciEdit::SetIcon(const std::map<int, UINT> &icons)
365 int iconWidth = GetSystemMetrics(SM_CXSMICON);
366 int iconHeight = GetSystemMetrics(SM_CYSMICON);
367 Call(SCI_RGBAIMAGESETWIDTH, iconWidth);
368 Call(SCI_RGBAIMAGESETHEIGHT, iconHeight);
369 for (auto icon : icons)
371 CAutoIcon hIcon = LoadIconEx(AfxGetInstanceHandle(), MAKEINTRESOURCE(icon.second), iconWidth, iconHeight);
372 auto bytes = Icon2Image(hIcon);
373 Call(SCI_REGISTERRGBAIMAGE, icon.first, reinterpret_cast<LPARAM>(bytes.get()));
377 BOOL CSciEdit::LoadDictionaries(LONG lLanguageID)
379 // Setup the spell checker
380 wchar_t buf[6] = { 0 };
381 CString sFolderUp = CPathUtils::GetAppParentDirectory();
382 CString sFolderAppData = CPathUtils::GetAppDataDirectory();
383 CString sFile;
385 GetLocaleInfo(MAKELCID(lLanguageID, SORT_DEFAULT), LOCALE_SISO639LANGNAME, buf, _countof(buf));
386 sFile = buf;
387 if (lLanguageID == 2074)
388 sFile += L"-Latn";
389 sFile += L'_';
390 GetLocaleInfo(MAKELCID(lLanguageID, SORT_DEFAULT), LOCALE_SISO3166CTRYNAME, buf, _countof(buf));
391 sFile += buf;
392 if (!pChecker)
394 if ((PathFileExists(sFolderAppData + L"dic\\" + sFile + L".aff")) &&
395 (PathFileExists(sFolderAppData + L"dic\\" + sFile + L".dic")))
397 pChecker = std::make_unique<Hunspell>(CStringA(sFolderAppData + L"dic\\" + sFile + L".aff"), CStringA(sFolderAppData + L"dic\\" + sFile + L".dic"));
399 else if ((PathFileExists(sFolderUp + L"Languages\\" + sFile + L".aff")) &&
400 (PathFileExists(sFolderUp + L"Languages\\" + sFile + L".dic")))
402 pChecker = std::make_unique<Hunspell>(CStringA(sFolderUp + L"Languages\\" + sFile + L".aff"), CStringA(sFolderUp + L"Languages\\" + sFile + L".dic"));
404 if (pChecker)
406 const char* encoding = pChecker->get_dic_encoding();
407 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": %s\n", encoding);
408 m_spellcodepage = 0;
409 for (int i = 0; i < _countof(enc2locale); ++i)
411 if (strcmp(encoding, enc2locale[i].def_enc) == 0)
412 m_spellcodepage = atoi(enc2locale[i].cp);
414 m_personalDict.Init(lLanguageID);
417 if (pChecker)
418 return TRUE;
419 return FALSE;
422 LRESULT CSciEdit::Call(UINT message, WPARAM wParam, LPARAM lParam)
424 ASSERT(::IsWindow(m_hWnd)); //Window must be valid
425 ASSERT(m_DirectFunction); //Direct function must be valid
426 return reinterpret_cast<SciFnDirect>(m_DirectFunction)(m_DirectPointer, message, wParam, lParam);
429 CString CSciEdit::StringFromControl(const CStringA& text)
431 CString sText;
432 #ifdef UNICODE
433 int codepage = static_cast<int>(Call(SCI_GETCODEPAGE));
434 int reslen = MultiByteToWideChar(codepage, 0, text, text.GetLength(), 0, 0);
435 MultiByteToWideChar(codepage, 0, text, text.GetLength(), sText.GetBuffer(reslen+1), reslen+1);
436 sText.ReleaseBuffer(reslen);
437 #else
438 sText = text;
439 #endif
440 return sText;
443 CStringA CSciEdit::StringForControl(const CString& text)
445 CStringA sTextA;
446 #ifdef UNICODE
447 int codepage = static_cast<int>(SendMessage(SCI_GETCODEPAGE));
448 int reslen = WideCharToMultiByte(codepage, 0, text, text.GetLength(), 0, 0, 0, 0);
449 WideCharToMultiByte(codepage, 0, text, text.GetLength(), sTextA.GetBuffer(reslen), reslen, 0, 0);
450 sTextA.ReleaseBuffer(reslen);
451 #else
452 sTextA = text;
453 #endif
454 ATLTRACE("string length %d\n", sTextA.GetLength());
455 return sTextA;
458 void CSciEdit::SetText(const CString& sText)
460 auto readonly = m_bReadOnly;
461 SCOPE_EXIT { SetReadOnly(readonly); };
462 SetReadOnly(false);
464 CStringA sTextA = StringForControl(sText);
465 Call(SCI_SETTEXT, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sTextA)));
467 if (Call(SCI_GETSCROLLWIDTHTRACKING) != 0)
468 Call(SCI_SETSCROLLWIDTH, 1);
470 // Scintilla seems to have problems with strings that
471 // aren't terminated by a newline char. Once that char
472 // is there, it can be removed without problems.
473 // So we add here a newline, then remove it again.
474 Call(SCI_DOCUMENTEND);
475 Call(SCI_NEWLINE);
476 Call(SCI_DELETEBACK);
479 void CSciEdit::InsertText(const CString& sText, bool bNewLine)
481 auto readonly = m_bReadOnly;
482 SCOPE_EXIT { SetReadOnly(readonly); };
483 SetReadOnly(false);
485 CStringA sTextA = StringForControl(sText);
486 Call(SCI_REPLACESEL, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sTextA)));
487 if (bNewLine)
488 Call(SCI_REPLACESEL, 0, reinterpret_cast<LPARAM>("\n"));
491 CString CSciEdit::GetText()
493 auto len = static_cast<int>(Call(SCI_GETTEXT, 0, 0));
494 CStringA sTextA;
495 Call(SCI_GETTEXT, len + 1, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(CStrBufA(sTextA, len, 0))));
496 return StringFromControl(sTextA);
499 CString CSciEdit::GetWordUnderCursor(bool bSelectWord, bool allchars)
501 Sci_TextRange textrange;
502 auto pos = static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS));
503 textrange.chrg.cpMin = static_cast<int>(Call(SCI_WORDSTARTPOSITION, pos, TRUE));
504 if ((pos == textrange.chrg.cpMin)||(textrange.chrg.cpMin < 0))
505 return CString();
506 textrange.chrg.cpMax = static_cast<int>(Call(SCI_WORDENDPOSITION, textrange.chrg.cpMin, TRUE));
508 CStringA textbuffer;
509 textrange.lpstrText = textbuffer.GetBuffer(textrange.chrg.cpMax - textrange.chrg.cpMin);
510 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
511 textbuffer.ReleaseBufferSetLength(textrange.chrg.cpMax - textrange.chrg.cpMin);
512 CString sRet = StringFromControl(textbuffer);
513 if (m_bDoStyle && !allchars)
515 for (const auto styleindicator : { '*', '_', '^' })
517 if (sRet.IsEmpty())
518 break;
519 if (sRet[sRet.GetLength() - 1] == styleindicator)
521 --textrange.chrg.cpMax;
522 sRet.Truncate(sRet.GetLength() - 1);
524 if (sRet.IsEmpty())
525 break;
526 if (sRet[0] == styleindicator)
528 ++textrange.chrg.cpMin;
529 sRet = sRet.Right(sRet.GetLength() - 1);
533 if (bSelectWord)
534 Call(SCI_SETSEL, textrange.chrg.cpMin, textrange.chrg.cpMax);
535 return sRet;
538 void CSciEdit::SetFont(CString sFontName, int iFontSizeInPoints)
540 Call(SCI_STYLESETFONT, STYLE_DEFAULT, reinterpret_cast<LPARAM>(CUnicodeUtils::GetUTF8(sFontName).GetBuffer()));
541 Call(SCI_STYLESETSIZE, STYLE_DEFAULT, iFontSizeInPoints);
542 Call(SCI_STYLECLEARALL);
544 LPARAM color = GetSysColor(COLOR_HOTLIGHT);
545 // set the styles for the bug ID strings
546 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLD, TRUE);
547 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLD, color);
548 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLDITALIC, TRUE);
549 Call(SCI_STYLESETITALIC, STYLE_ISSUEBOLDITALIC, TRUE);
550 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLDITALIC, color);
551 Call(SCI_STYLESETHOTSPOT, STYLE_ISSUEBOLDITALIC, TRUE);
553 // set the formatted text styles
554 Call(SCI_STYLESETBOLD, STYLE_BOLD, TRUE);
555 Call(SCI_STYLESETITALIC, STYLE_ITALIC, TRUE);
556 Call(SCI_STYLESETUNDERLINE, STYLE_UNDERLINED, TRUE);
558 // set the style for URLs
559 Call(SCI_STYLESETFORE, STYLE_URL, color);
560 Call(SCI_STYLESETHOTSPOT, STYLE_URL, TRUE);
562 Call(SCI_SETHOTSPOTACTIVEUNDERLINE, TRUE);
565 void CSciEdit::SetAutoCompletionList(std::map<CString, int>&& list, wchar_t separator, wchar_t typeSeparator)
567 //copy the auto completion list.
569 //SK: instead of creating a copy of that list, we could accept a pointer
570 //to the list and use that instead. But then the caller would have to make
571 //sure that the list persists over the lifetime of the control!
572 m_autolist.clear();
573 m_autolist = std::move(list);
574 m_separator = separator;
575 m_typeSeparator = typeSeparator;
578 // Helper for CSciEdit::IsMisspelled()
579 // Returns TRUE if sWord has spelling errors.
580 BOOL CSciEdit::CheckWordSpelling(const CString& sWord)
582 if (m_SpellChecker)
584 IEnumSpellingErrorPtr enumSpellingError = nullptr;
585 if (SUCCEEDED(m_SpellChecker->Check(sWord, &enumSpellingError)))
587 ISpellingErrorPtr spellingError = nullptr;
588 if (enumSpellingError->Next(&spellingError) == S_OK)
590 CORRECTIVE_ACTION action = CORRECTIVE_ACTION_NONE;
591 spellingError->get_CorrectiveAction(&action);
592 if (action != CORRECTIVE_ACTION_NONE)
593 return TRUE;
597 else if (pChecker)
599 // convert the string from the control to the encoding of the spell checker module.
600 auto sWordA = GetWordForSpellChecker(sWord);
601 if (!pChecker->spell(sWordA))
602 return TRUE;
605 return FALSE;
608 BOOL CSciEdit::IsMisspelled(const CString& sWord)
610 // words starting with a digit are treated as correctly spelled
611 if (_istdigit(sWord.GetAt(0)))
612 return FALSE;
613 // words in the personal dictionary are correct too
614 if (m_personalDict.FindWord(sWord))
615 return FALSE;
617 // Check spell checking cache first.
618 const BOOL *cacheResult = m_SpellingCache.try_get(std::wstring(sWord, sWord.GetLength()));
619 if (cacheResult)
620 return *cacheResult;
622 // convert the string from the control to the encoding of the spell checker module.
623 auto sWordA = GetWordForSpellChecker(sWord);
625 // now we actually check the spelling...
626 BOOL misspelled = CheckWordSpelling(sWord);
627 if (misspelled)
629 // the word is marked as misspelled, we now check whether the word
630 // is maybe a composite identifier
631 // a composite identifier consists of multiple words, with each word
632 // separated by a change in lower to uppercase letters
633 misspelled = FALSE;
634 if (sWord.GetLength() > 1)
636 int wordstart = 0;
637 int wordend = 1;
638 while (wordend < sWord.GetLength())
640 while ((wordend < sWord.GetLength())&&(!_istupper(sWord[wordend])))
641 wordend++;
642 if ((wordstart == 0)&&(wordend == sWord.GetLength()))
644 // words in the auto list are also assumed correctly spelled
645 if (m_autolist.find(sWord) != m_autolist.end())
646 misspelled = FALSE;
647 else
648 misspelled = TRUE;
649 break;
651 CString token(sWord.Mid(wordstart, wordend - wordstart));
652 if (token.GetLength() > 2 && CheckWordSpelling(token))
654 misspelled = TRUE;
655 break;
657 wordstart = wordend;
658 wordend++;
663 // Update cache.
664 m_SpellingCache.insert_or_assign(std::wstring(sWord, sWord.GetLength()), misspelled);
665 return misspelled;
668 void CSciEdit::CheckSpelling(Sci_Position startpos, Sci_Position endpos)
670 if (m_bReadOnly)
671 return;
673 if (!pChecker && !m_SpellChecker)
674 return;
676 Sci_TextRange textrange;
677 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(startpos);
678 textrange.chrg.cpMax = textrange.chrg.cpMin;
679 auto lastpos = endpos;
680 if (lastpos < 0)
681 lastpos = static_cast<int>(Call(SCI_GETLENGTH)) - textrange.chrg.cpMin;
682 Call(SCI_SETINDICATORCURRENT, INDIC_MISSPELLED);
683 while (textrange.chrg.cpMax < lastpos)
685 textrange.chrg.cpMin = static_cast<int>(Call(SCI_WORDSTARTPOSITION, textrange.chrg.cpMax + 1, TRUE));
686 if (textrange.chrg.cpMin < textrange.chrg.cpMax)
687 break;
688 textrange.chrg.cpMax = static_cast<int>(Call(SCI_WORDENDPOSITION, textrange.chrg.cpMin, TRUE));
689 if (textrange.chrg.cpMin == textrange.chrg.cpMax)
691 textrange.chrg.cpMax++;
692 // since Scintilla squiggles to the end of the text even if told to stop one char before it,
693 // we have to clear here the squiggly lines to the end.
694 if (textrange.chrg.cpMin)
695 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin-1, textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
696 continue;
698 ATLASSERT(textrange.chrg.cpMax >= textrange.chrg.cpMin);
699 auto textbuffer = std::make_unique<char[]>(textrange.chrg.cpMax - textrange.chrg.cpMin + 2);
700 textrange.lpstrText = textbuffer.get();
701 textrange.chrg.cpMax++;
702 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
703 auto len = static_cast<int>(strlen(textrange.lpstrText));
704 if (len == 0)
706 textrange.chrg.cpMax--;
707 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
708 len = static_cast<int>(strlen(textrange.lpstrText));
709 textrange.chrg.cpMax++;
710 len++;
712 if (len && textrange.lpstrText[len - 1] == '.')
714 // Try to ignore file names from the auto list.
715 // Do do this, for each word ending with '.' we extract next word and check
716 // whether the combined string is present in auto list.
717 Sci_TextRange twoWords;
718 twoWords.chrg.cpMin = textrange.chrg.cpMin;
719 twoWords.chrg.cpMax = static_cast<int>(Call(SCI_WORDENDPOSITION, textrange.chrg.cpMax + 1, TRUE));
720 CStringA twoWordsBuffer;
721 twoWords.lpstrText = twoWordsBuffer.GetBuffer(twoWords.chrg.cpMax - twoWords.chrg.cpMin);
722 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&twoWords));
723 twoWordsBuffer.ReleaseBufferSetLength(twoWords.chrg.cpMax - twoWords.chrg.cpMin);
724 CString sWord = StringFromControl(twoWords.lpstrText);
725 if (m_autolist.find(sWord) != m_autolist.end())
727 //mark word as correct (remove the squiggle line)
728 Call(SCI_INDICATORCLEARRANGE, twoWords.chrg.cpMin, twoWords.chrg.cpMax - twoWords.chrg.cpMin);
729 textrange.chrg.cpMax = twoWords.chrg.cpMax;
730 continue;
733 if (len)
734 textrange.lpstrText[len - 1] = '\0';
735 textrange.chrg.cpMax--;
736 if (textrange.lpstrText[0])
738 CString sWord = StringFromControl(textrange.lpstrText);
739 if ((GetStyleAt(textrange.chrg.cpMin) != STYLE_URL) && IsMisspelled(sWord))
741 //mark word as misspelled
742 Call(SCI_INDICATORFILLRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin);
744 else
746 //mark word as correct (remove the squiggle line)
747 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin);
748 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
754 void CSciEdit::SuggestSpellingAlternatives()
756 if (!pChecker && !m_SpellChecker)
757 return;
758 CString word = GetWordUnderCursor(true);
759 Call(SCI_SETCURRENTPOS, Call(SCI_WORDSTARTPOSITION, Call(SCI_GETCURRENTPOS), TRUE));
760 if (word.IsEmpty())
761 return;
763 CString suggestions;
764 if (m_SpellChecker)
766 IEnumStringPtr enumSpellingSuggestions = nullptr;
767 if (SUCCEEDED(m_SpellChecker->Suggest(word, &enumSpellingSuggestions)))
769 LPOLESTR string = nullptr;
770 while (enumSpellingSuggestions->Next(1, &string, nullptr) == S_OK)
772 suggestions.AppendFormat(L"%s%c%d%c", (LPCWSTR)CString(string), m_typeSeparator, AUTOCOMPLETE_SPELLING, m_separator);
773 CoTaskMemFree(string);
777 else if (pChecker)
779 auto wlst = pChecker->suggest(GetWordForSpellChecker(word));
780 if (!wlst.empty())
782 for (const auto& alternative : wlst)
783 suggestions.AppendFormat(L"%s%c%d%c", static_cast<LPCWSTR>(GetWordFromSpellChecker(alternative)), m_typeSeparator, AUTOCOMPLETE_SPELLING, m_separator);
787 suggestions.TrimRight(m_separator);
788 if (suggestions.IsEmpty())
789 return;
790 Call(SCI_AUTOCSETSEPARATOR, CStringA(m_separator).GetAt(0));
791 Call(SCI_AUTOCSETTYPESEPARATOR, m_typeSeparator);
792 Call(SCI_AUTOCSETDROPRESTOFWORD, 1);
793 Call(SCI_AUTOCSHOW, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(StringForControl(suggestions))));
794 SetWindowStylesForAutocompletionPopup();
797 void CSciEdit::DoAutoCompletion(Sci_Position nMinPrefixLength)
799 if (m_autolist.empty())
800 return;
801 auto pos = static_cast<int>(static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS)));
802 if (pos != static_cast<int>(Call(SCI_WORDENDPOSITION, pos, TRUE)))
803 return; // don't auto complete if we're not at the end of a word
804 CString word = GetWordUnderCursor();
805 if (word.GetLength() < nMinPrefixLength)
807 word = GetWordUnderCursor(false, true);
808 if (word.GetLength() < nMinPrefixLength)
809 return; // don't auto complete yet, word is too short
811 CString sAutoCompleteList;
813 for (int i = 0; i < 2; ++i)
815 std::vector<CString> words;
817 pos = word.Find('-');
819 CString wordLower = word;
820 wordLower.MakeLower();
821 CString wordHigher = word;
822 wordHigher.MakeUpper();
824 words.push_back(word);
825 words.push_back(wordLower);
826 words.push_back(wordHigher);
828 if (pos >= 0)
830 CString s = wordLower.Left(pos);
831 if (s.GetLength() >= nMinPrefixLength)
832 words.push_back(s);
833 s = wordLower.Mid(pos + 1);
834 if (s.GetLength() >= nMinPrefixLength)
835 words.push_back(s);
836 s = wordHigher.Left(pos);
837 if (s.GetLength() >= nMinPrefixLength)
838 words.push_back(wordHigher.Left(pos));
839 s = wordHigher.Mid(pos + 1);
840 if (s.GetLength() >= nMinPrefixLength)
841 words.push_back(wordHigher.Mid(pos+1));
844 // note: the m_autolist is case-sensitive because
845 // its contents are also used to mark words in it
846 // as correctly spelled. If it would be case-insensitive,
847 // case spelling mistakes would not show up as misspelled.
848 std::map<CString, int> wordset;
849 for (const auto& w : words)
851 for (auto lowerit = m_autolist.lower_bound(w);
852 lowerit != m_autolist.end(); ++lowerit)
854 int compare = w.CompareNoCase(lowerit->first.Left(w.GetLength()));
855 if (compare > 0)
856 continue;
857 else if (compare == 0)
858 wordset.emplace(lowerit->first, lowerit->second);
859 else
860 break;
864 for (const auto& w : wordset)
865 sAutoCompleteList.AppendFormat(L"%s%c%d%c", static_cast<LPCWSTR>(w.first), m_typeSeparator, w.second, m_separator);
867 sAutoCompleteList.TrimRight(m_separator);
869 if (i == 0)
871 if (sAutoCompleteList.IsEmpty())
873 // retry with all chars
874 word = GetWordUnderCursor(false, true);
876 else
877 break;
879 if (i == 1)
881 if (sAutoCompleteList.IsEmpty())
882 return;
886 Call(SCI_AUTOCSETSEPARATOR, CStringA(m_separator).GetAt(0));
887 Call(SCI_AUTOCSETTYPESEPARATOR, (m_typeSeparator));
888 auto sForControl = StringForControl(sAutoCompleteList);
889 Call(SCI_AUTOCSHOW, StringForControl(word).GetLength(), reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sForControl)));
890 SetWindowStylesForAutocompletionPopup();
893 BOOL CSciEdit::OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult)
895 if (message != WM_NOTIFY)
896 return CWnd::OnChildNotify(message, wParam, lParam, pLResult);
898 auto lpnmhdr = reinterpret_cast<LPNMHDR>(lParam);
899 auto lpSCN = reinterpret_cast<SCNotification*>(lParam);
901 if(lpnmhdr->hwndFrom==m_hWnd)
903 switch(lpnmhdr->code)
905 case SCN_CHARADDED:
907 if ((lpSCN->ch < 32)&&(lpSCN->ch != 13)&&(lpSCN->ch != 10))
908 Call(SCI_DELETEBACK);
909 else
910 DoAutoCompletion(m_nAutoCompleteMinChars);
911 return TRUE;
913 break;
914 case SCN_AUTOCSELECTION:
916 CString text = StringFromControl(lpSCN->text);
917 if (m_autolist[text] == AUTOCOMPLETE_SNIPPET)
919 Call(SCI_AUTOCCANCEL);
920 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
922 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
923 pHandler->HandleSnippet(m_autolist[text], text, this);
926 return TRUE;
928 case SCN_STYLENEEDED:
930 auto startpos = static_cast<Sci_Position>(Call(SCI_GETENDSTYLED));
931 auto endpos = reinterpret_cast<SCNotification*>(lpnmhdr)->position;
933 auto startwordpos = static_cast<int>(Call(SCI_WORDSTARTPOSITION, startpos, true));
934 auto endwordpos = static_cast<int>(Call(SCI_WORDENDPOSITION, endpos, true));
936 MarkEnteredBugID(startwordpos, endwordpos);
937 if (m_bDoStyle)
938 StyleEnteredText(startwordpos, endwordpos);
940 StyleURLs(startwordpos, endwordpos);
941 CheckSpelling(startwordpos, endwordpos);
943 // Tell scintilla editor that we styled all requested range.
944 Call(SCI_STARTSTYLING, endwordpos);
945 Call(SCI_SETSTYLING, 0, 0);
947 break;
948 case SCN_MODIFIED:
950 if (!m_blockModifiedHandler && (lpSCN->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)))
952 auto firstline = static_cast<int>(Call(SCI_GETFIRSTVISIBLELINE));
953 auto lastline = firstline + static_cast<int>(Call(SCI_LINESONSCREEN));
954 auto firstpos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, firstline));
955 auto lastpos = static_cast<Sci_Position>(Call(SCI_GETLINEENDPOSITION, lastline));
956 auto pos1 = lpSCN->position;
957 auto pos2 = pos1 + lpSCN->length;
958 // always use the bigger range
959 firstpos = min(firstpos, pos1);
960 lastpos = max(lastpos, pos2);
962 WrapLines(firstpos, lastpos);
964 break;
966 case SCN_DWELLSTART:
967 case SCN_HOTSPOTRELEASECLICK:
969 Sci_TextRange textrange;
970 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(lpSCN->position);
971 textrange.chrg.cpMax = static_cast<Sci_PositionCR>(lpSCN->position);
972 auto style = GetStyleAt(lpSCN->position);
973 if (style != STYLE_ISSUEBOLDITALIC && style != STYLE_URL)
974 break;
975 while (GetStyleAt(textrange.chrg.cpMin - 1) == style)
976 --textrange.chrg.cpMin;
977 while (GetStyleAt(textrange.chrg.cpMax + 1) == style)
978 ++textrange.chrg.cpMax;
979 ++textrange.chrg.cpMax;
980 CStringA textbuffer;
981 textrange.lpstrText = textbuffer.GetBuffer(textrange.chrg.cpMax - textrange.chrg.cpMin);
982 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
983 textbuffer.ReleaseBufferSetLength(textrange.chrg.cpMax - textrange.chrg.cpMin);
984 CString url;
985 if (style == STYLE_URL)
987 url = StringFromControl(textbuffer);
988 if (url.Find(L'@') > 0 && !PathIsURL(url))
989 url = L"mailto:" + url;
991 else
993 url = m_sUrl;
994 ProjectProperties::ReplaceBugIDPlaceholder(url, StringFromControl(textbuffer));
996 if (!url.IsEmpty())
998 if (lpnmhdr->code == SCN_HOTSPOTRELEASECLICK)
999 ShellExecute(GetParent()->GetSafeHwnd(), L"open", url, nullptr, nullptr, SW_SHOWDEFAULT);
1000 else
1002 CStringA sTextA = StringForControl(url);
1003 Call(SCI_CALLTIPSHOW, lpSCN->position + 3, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(sTextA)));
1007 break;
1008 case SCN_DWELLEND:
1009 Call(SCI_CALLTIPCANCEL);
1010 break;
1013 return CWnd::OnChildNotify(message, wParam, lParam, pLResult);
1016 BEGIN_MESSAGE_MAP(CSciEdit, CWnd)
1017 ON_WM_KEYDOWN()
1018 ON_WM_CONTEXTMENU()
1019 ON_WM_SYSCOLORCHANGE()
1020 END_MESSAGE_MAP()
1022 void CSciEdit::OnSysColorChange()
1024 __super::OnSysColorChange();
1025 CTheme::Instance().OnSysColorChanged();
1026 SetColors(true);
1027 if (m_bUDiffmode)
1028 SetUDiffStyle();
1031 void CSciEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
1033 switch (nChar)
1035 case (VK_ESCAPE):
1037 if ((Call(SCI_AUTOCACTIVE)==0)&&(Call(SCI_CALLTIPACTIVE)==0))
1038 ::SendMessage(GetParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
1040 break;
1042 CWnd::OnKeyDown(nChar, nRepCnt, nFlags);
1045 BOOL CSciEdit::PreTranslateMessage(MSG* pMsg)
1047 if (pMsg->message == WM_KEYDOWN)
1049 switch (pMsg->wParam)
1051 case VK_SPACE:
1053 if ((GetKeyState(VK_CONTROL) & 0x8000) && ((GetKeyState(VK_MENU) & 0x8000) == 0))
1055 DoAutoCompletion(1);
1056 return TRUE;
1059 break;
1060 case VK_TAB:
1061 // The TAB cannot be handled in OnKeyDown because it is too late by then.
1063 if ((GetKeyState(VK_CONTROL) & 0x8000) && ((GetKeyState(VK_MENU) & 0x8000) == 0))
1065 //Ctrl-Tab was pressed, this means we should provide the user with
1066 //a list of possible spell checking alternatives to the word under
1067 //the cursor
1068 SuggestSpellingAlternatives();
1069 return TRUE;
1071 else if (!Call(SCI_AUTOCACTIVE))
1073 ::PostMessage(GetParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
1074 return TRUE;
1077 break;
1080 return CWnd::PreTranslateMessage(pMsg);
1083 void CSciEdit::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
1085 auto anchor = static_cast<Sci_Position>(Call(SCI_GETANCHOR));
1086 auto currentpos = static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS));
1087 auto selstart = static_cast<int>(static_cast<Sci_Position>(Call(SCI_GETSELECTIONSTART)));
1088 auto selend = static_cast<int>(static_cast<Sci_Position>(Call(SCI_GETSELECTIONEND)));
1089 Sci_Position pointpos = 0;
1090 if ((point.x == -1) && (point.y == -1))
1092 CRect rect;
1093 GetClientRect(&rect);
1094 ClientToScreen(&rect);
1095 point = rect.CenterPoint();
1096 pointpos = static_cast<Sci_Position>(Call(SCI_GETCURRENTPOS));
1098 else
1100 // change the cursor position to the point where the user
1101 // right-clicked.
1102 CPoint clientpoint = point;
1103 ScreenToClient(&clientpoint);
1104 pointpos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMPOINT, clientpoint.x, clientpoint.y));
1106 CString sMenuItemText;
1107 CMenu popup;
1108 bool bRestoreCursor = true;
1109 if (popup.CreatePopupMenu())
1111 bool bCanUndo = !!Call(SCI_CANUNDO);
1112 bool bCanRedo = !!Call(SCI_CANREDO);
1113 bool bHasSelection = (selend-selstart > 0);
1114 bool bCanPaste = !!Call(SCI_CANPASTE);
1115 bool bIsReadOnly = !!Call(SCI_GETREADONLY);
1116 UINT uEnabledMenu = MF_STRING | MF_ENABLED;
1117 UINT uDisabledMenu = MF_STRING | MF_GRAYED;
1119 // find the word under the cursor
1120 CString sWord;
1121 if (pointpos)
1123 // setting the cursor clears the selection
1124 Call(SCI_SETANCHOR, pointpos);
1125 Call(SCI_SETCURRENTPOS, pointpos);
1126 sWord = GetWordUnderCursor();
1127 // restore the selection
1128 Call(SCI_SETSELECTIONSTART, selstart);
1129 Call(SCI_SETSELECTIONEND, selend);
1131 else
1132 sWord = GetWordUnderCursor();
1133 auto worda = GetWordForSpellChecker(sWord);
1135 int nCorrections = 1;
1136 // check if the word under the cursor is spelled wrong
1137 if (!bIsReadOnly && (pChecker || m_SpellChecker) && !worda.empty())
1139 if (m_SpellChecker)
1141 IEnumStringPtr enumSpellingSuggestions = nullptr;
1142 if (SUCCEEDED(m_SpellChecker->Suggest(sWord, &enumSpellingSuggestions)))
1144 LPOLESTR string = nullptr;
1145 while (enumSpellingSuggestions->Next(1, &string, nullptr) == S_OK)
1147 popup.InsertMenu((UINT)-1, 0, nCorrections++, string);
1148 CoTaskMemFree(string);
1150 popup.AppendMenu(MF_SEPARATOR);
1153 else if (pChecker)
1155 // get the spell suggestions
1156 auto wlst = pChecker->suggest(worda);
1157 if (!wlst.empty())
1159 // add the suggestions to the context menu
1160 for (const auto& alternative : wlst)
1162 CString sug = GetWordFromSpellChecker(alternative);
1163 popup.InsertMenu(static_cast<UINT>(-1), 0, nCorrections++, sug);
1165 popup.AppendMenu(MF_SEPARATOR);
1170 // also allow the user to add the word to the custom dictionary so
1171 // it won't show up as misspelled anymore
1172 if (!bIsReadOnly && (sWord.GetLength() < PDICT_MAX_WORD_LENGTH) && (m_autolist.find(sWord) == m_autolist.end() && IsMisspelled(sWord)) &&
1173 (!_istdigit(sWord.GetAt(0))) && (!m_personalDict.FindWord(sWord)))
1175 sMenuItemText.Format(IDS_SCIEDIT_ADDWORD, static_cast<LPCWSTR>(sWord));
1176 popup.AppendMenu(uEnabledMenu, SCI_ADDWORD, sMenuItemText);
1177 // another separator
1178 popup.AppendMenu(MF_SEPARATOR);
1181 // add the 'default' entries
1182 sMenuItemText.LoadString(IDS_SCIEDIT_UNDO);
1183 popup.AppendMenu(bCanUndo ? uEnabledMenu : uDisabledMenu, SCI_UNDO, sMenuItemText);
1184 sMenuItemText.LoadString(IDS_SCIEDIT_REDO);
1185 popup.AppendMenu(bCanRedo ? uEnabledMenu : uDisabledMenu, SCI_REDO, sMenuItemText);
1187 popup.AppendMenu(MF_SEPARATOR);
1189 sMenuItemText.LoadString(IDS_SCIEDIT_CUT);
1190 popup.AppendMenu(!bIsReadOnly && bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_CUT, sMenuItemText);
1191 sMenuItemText.LoadString(IDS_SCIEDIT_COPY);
1192 popup.AppendMenu(bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_COPY, sMenuItemText);
1193 sMenuItemText.LoadString(IDS_SCIEDIT_PASTE);
1194 popup.AppendMenu(bCanPaste ? uEnabledMenu : uDisabledMenu, SCI_PASTE, sMenuItemText);
1196 popup.AppendMenu(MF_SEPARATOR);
1198 sMenuItemText.LoadString(IDS_SCIEDIT_SELECTALL);
1199 popup.AppendMenu(uEnabledMenu, SCI_SELECTALL, sMenuItemText);
1201 if (!bIsReadOnly && Call(SCI_GETEDGECOLUMN))
1203 popup.AppendMenu(MF_SEPARATOR);
1205 sMenuItemText.LoadString(IDS_SCIEDIT_SPLITLINES);
1206 popup.AppendMenu(bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_LINESSPLIT, sMenuItemText);
1209 if (m_arContextHandlers.GetCount() > 0)
1210 popup.AppendMenu(MF_SEPARATOR);
1212 int nCustoms = nCorrections;
1213 // now add any custom context menus
1214 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
1216 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
1217 pHandler->InsertMenuItems(popup, nCustoms);
1220 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1221 switch (cmd)
1223 case 0:
1224 break; // no command selected
1225 case SCI_SELECTALL:
1226 bRestoreCursor = false;
1227 [[fallthrough]];
1228 case SCI_UNDO:
1229 case SCI_REDO:
1230 case SCI_CUT:
1231 case SCI_COPY:
1232 case SCI_PASTE:
1233 Call(cmd);
1234 break;
1235 case SCI_ADDWORD:
1236 m_personalDict.AddWord(sWord);
1237 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)))));
1238 break;
1239 case SCI_LINESSPLIT:
1241 auto marker = static_cast<int>(Call(SCI_GETEDGECOLUMN) * static_cast<int>(Call(SCI_TEXTWIDTH, 0, reinterpret_cast<LPARAM>(" "))));
1242 if (marker)
1244 m_blockModifiedHandler = true;
1245 SCOPE_EXIT{ m_blockModifiedHandler = false; };
1246 Call(SCI_TARGETFROMSELECTION);
1247 Call(SCI_LINESJOIN);
1248 Call(SCI_LINESSPLIT, marker);
1251 break;
1252 default:
1253 if (cmd < nCorrections)
1255 Call(SCI_SETANCHOR, pointpos);
1256 Call(SCI_SETCURRENTPOS, pointpos);
1257 GetWordUnderCursor(true);
1258 CString temp;
1259 popup.GetMenuString(cmd, temp, 0);
1260 // setting the cursor clears the selection
1261 Call(SCI_REPLACESEL, 0, reinterpret_cast<LPARAM>(static_cast<LPCSTR>(StringForControl(temp))));
1263 else if (cmd < (nCorrections+nCustoms))
1265 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
1267 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
1268 if (pHandler->HandleMenuItemClick(cmd, this))
1269 break;
1274 if (bRestoreCursor)
1276 // restore the anchor and cursor position
1277 Call(SCI_SETCURRENTPOS, currentpos);
1278 Call(SCI_SETANCHOR, anchor);
1282 bool CSciEdit::StyleEnteredText(Sci_Position startstylepos, Sci_Position endstylepos)
1284 bool bStyled = false;
1285 const auto line = static_cast<int>(Call(SCI_LINEFROMPOSITION, startstylepos));
1286 const auto line_number_end = static_cast<int>(Call(SCI_LINEFROMPOSITION, endstylepos));
1287 for (auto line_number = line; line_number <= line_number_end; ++line_number)
1289 auto offset = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, line_number));
1290 auto line_len = static_cast<int>(Call(SCI_LINELENGTH, line_number));
1291 auto linebuffer = std::make_unique<char[]>(line_len + 1);
1292 Call(SCI_GETLINE, line_number, reinterpret_cast<LPARAM>(linebuffer.get()));
1293 linebuffer[line_len] = '\0';
1294 Sci_Position start = 0;
1295 Sci_Position end = 0;
1296 while (FindStyleChars(linebuffer.get(), '*', start, end))
1298 Call(SCI_STARTSTYLING, start + offset, STYLE_BOLD);
1299 Call(SCI_SETSTYLING, end-start, STYLE_BOLD);
1300 bStyled = true;
1301 start = end;
1303 start = 0;
1304 end = 0;
1305 while (FindStyleChars(linebuffer.get(), '^', start, end))
1307 Call(SCI_STARTSTYLING, start + offset, STYLE_ITALIC);
1308 Call(SCI_SETSTYLING, end-start, STYLE_ITALIC);
1309 bStyled = true;
1310 start = end;
1312 start = 0;
1313 end = 0;
1314 while (FindStyleChars(linebuffer.get(), '_', start, end))
1316 Call(SCI_STARTSTYLING, start + offset, STYLE_UNDERLINED);
1317 Call(SCI_SETSTYLING, end-start, STYLE_UNDERLINED);
1318 bStyled = true;
1319 start = end;
1322 return bStyled;
1325 bool CSciEdit::WrapLines(Sci_Position startpos, Sci_Position endpos)
1327 auto markerX = static_cast<Sci_Position>(Call(SCI_GETEDGECOLUMN) * static_cast<int>(Call(SCI_TEXTWIDTH, 0, reinterpret_cast<LPARAM>(" "))));
1328 if (markerX)
1330 Call(SCI_SETTARGETSTART, startpos);
1331 Call(SCI_SETTARGETEND, endpos);
1332 Call(SCI_LINESSPLIT, markerX);
1333 return true;
1335 return false;
1338 void CSciEdit::AdvanceUTF8(const char * str, int& pos)
1340 if ((str[pos] & 0xE0)==0xC0)
1342 // utf8 2-byte sequence
1343 pos += 2;
1345 else if ((str[pos] & 0xF0)==0xE0)
1347 // utf8 3-byte sequence
1348 pos += 3;
1350 else if ((str[pos] & 0xF8)==0xF0)
1352 // utf8 4-byte sequence
1353 pos += 4;
1355 else
1356 pos++;
1359 bool CSciEdit::FindStyleChars(const char* line, char styler, Sci_Position& start, Sci_Position& end)
1361 int i=0;
1362 int u=0;
1363 while (i < start)
1365 AdvanceUTF8(line, i);
1366 u++;
1369 bool bFoundMarker = false;
1370 CString sULine = CUnicodeUtils::GetUnicode(line);
1371 // find a starting marker
1372 while (line[i] != 0)
1374 if (line[i] == styler)
1376 if ((line[i+1]!=0)&&(IsCharAlphaNumeric(sULine[u+1]))&&
1377 (((u>0)&&(!IsCharAlphaNumeric(sULine[u-1]))) || (u==0)))
1379 start = i+1;
1380 AdvanceUTF8(line, i);
1381 u++;
1382 bFoundMarker = true;
1383 break;
1386 AdvanceUTF8(line, i);
1387 u++;
1389 if (!bFoundMarker)
1390 return false;
1391 // find ending marker
1392 bFoundMarker = false;
1393 while (line[i])
1395 if (line[i] == styler)
1397 if ((IsCharAlphaNumeric(sULine[u-1]))&&
1398 ((((u+1)<sULine.GetLength())&&(!IsCharAlphaNumeric(sULine[u+1]))) || ((u+1) == sULine.GetLength()))
1401 end = i;
1402 i++;
1403 bFoundMarker = true;
1404 break;
1407 AdvanceUTF8(line, i);
1408 u++;
1410 return bFoundMarker;
1413 BOOL CSciEdit::MarkEnteredBugID(Sci_Position startstylepos, Sci_Position endstylepos)
1415 if (m_sCommand.IsEmpty())
1416 return FALSE;
1417 // get the text between the start and end position we have to style
1418 const auto line_number = static_cast<int>(Call(SCI_LINEFROMPOSITION, startstylepos));
1419 auto start_pos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, line_number));
1420 auto end_pos = endstylepos;
1422 if (start_pos == end_pos)
1423 return FALSE;
1424 if (start_pos > end_pos)
1426 auto switchtemp = start_pos;
1427 start_pos = end_pos;
1428 end_pos = switchtemp;
1431 CStringA msg;
1432 const auto len = SafeSizeToInt(end_pos - start_pos);
1433 Sci_TextRange textrange;
1434 textrange.lpstrText = msg.GetBuffer(len);
1435 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(start_pos);
1436 textrange.chrg.cpMax = static_cast<Sci_PositionCR>(end_pos);
1437 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
1438 msg.ReleaseBufferSetLength(len);
1440 Call(SCI_STARTSTYLING, start_pos, STYLE_MASK);
1444 if (!m_sBugID.IsEmpty())
1446 // match with two regex strings (without grouping!)
1447 const std::regex regCheck(m_sCommand);
1448 const std::regex regBugID(m_sBugID);
1449 const std::sregex_iterator end;
1450 std::string s { static_cast<LPCSTR>(msg) };
1451 LONG pos = 0;
1452 // note:
1453 // if start_pos is 0, we're styling from the beginning and let the ^ char match the beginning of the line
1454 // that way, the ^ matches the very beginning of the log message and not the beginning of further lines.
1455 // problem is: this only works *while* entering log messages. If a log message is pasted in whole or
1456 // multiple lines are pasted, start_pos can be 0 and styling goes over multiple lines. In that case, those
1457 // additional line starts also match ^
1458 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)
1460 // clear the styles up to the match position
1461 Call(SCI_SETSTYLING, it->position(0)-pos, STYLE_DEFAULT);
1463 // (*it)[0] is the matched string
1464 std::string matchedString = (*it)[0];
1465 LONG matchedpos = 0;
1466 for (std::sregex_iterator it2(matchedString.cbegin(), matchedString.cend(), regBugID); it2 != end; ++it2)
1468 ATLTRACE("matched id : %s\n", std::string((*it2)[0]).c_str());
1470 // bold style up to the id match
1471 ATLTRACE("position = %ld\n", it2->position(0));
1472 if (it2->position(0))
1473 Call(SCI_SETSTYLING, it2->position(0) - matchedpos, STYLE_ISSUEBOLD);
1474 // bold and recursive style for the bug ID itself
1475 if ((*it2)[0].str().size())
1476 Call(SCI_SETSTYLING, (*it2)[0].str().size(), STYLE_ISSUEBOLDITALIC);
1477 matchedpos = static_cast<LONG>(it2->position(0) + (*it2)[0].str().size());
1479 if (matchedpos&& matchedpos < static_cast<LONG>(matchedString.size()))
1481 Call(SCI_SETSTYLING, matchedString.size() - matchedpos, STYLE_ISSUEBOLD);
1483 pos = static_cast<LONG>(it->position(0) + matchedString.size());
1485 // bold style for the rest of the string which isn't matched
1486 if (s.size()-pos)
1487 Call(SCI_SETSTYLING, s.size()-pos, STYLE_DEFAULT);
1489 else
1491 const std::regex regCheck(m_sCommand);
1492 const std::sregex_iterator end;
1493 std::string s { static_cast<LPCSTR>(msg) };
1494 LONG pos = 0;
1495 for (std::sregex_iterator it(s.cbegin(), s.cend(), regCheck); it != end; ++it)
1497 // clear the styles up to the match position
1498 if (it->position(0) - pos >= 0)
1499 Call(SCI_SETSTYLING, it->position(0) - pos, STYLE_DEFAULT);
1500 pos = static_cast<LONG>(it->position(0));
1502 const std::smatch match = *it;
1503 // we define group 1 as the whole issue text and
1504 // group 2 as the bug ID
1505 if (match.size() >= 2)
1507 ATLTRACE("matched id : %s\n", std::string(match[1]).c_str());
1508 if (match[1].first - s.cbegin() - pos >= 0)
1509 Call(SCI_SETSTYLING, match[1].first - s.cbegin() - pos, STYLE_ISSUEBOLD);
1510 Call(SCI_SETSTYLING, std::string(match[1]).size(), STYLE_ISSUEBOLDITALIC);
1511 pos = static_cast<LONG>(match[1].second - s.cbegin());
1516 catch (std::exception&) {}
1518 return FALSE;
1521 void CSciEdit::StyleURLs(Sci_Position startstylepos, Sci_Position endstylepos)
1523 const auto line_number = static_cast<int>(Call(SCI_LINEFROMPOSITION, startstylepos));
1524 startstylepos = static_cast<Sci_Position>(Call(SCI_POSITIONFROMLINE, line_number));
1526 const auto len = SafeSizeToInt(endstylepos - startstylepos);
1527 CStringA msg;
1528 Sci_TextRange textrange;
1529 textrange.lpstrText = msg.GetBuffer(len);
1530 textrange.chrg.cpMin = static_cast<Sci_PositionCR>(startstylepos);
1531 textrange.chrg.cpMax = static_cast<Sci_PositionCR>(endstylepos);
1532 Call(SCI_GETTEXTRANGE, 0, reinterpret_cast<LPARAM>(&textrange));
1533 // we're dealing with utf8 encoded text here, which means one glyph is
1534 // not necessarily one byte/wchar_t
1535 // that's why we use CStringA to still get a correct char index
1536 msg.ReleaseBufferSetLength(len);
1538 ::FindURLMatches(msg, AdvanceUTF8, [&](int start, int end) {
1539 ASSERT(startstylepos + end <= endstylepos);
1540 Call(SCI_STARTSTYLING, startstylepos + start, STYLE_URL);
1541 Call(SCI_SETSTYLING, end - start, STYLE_URL); });
1544 std::string CSciEdit::GetWordForSpellChecker(const CString& sWord)
1546 // convert the string from the control to the encoding of the spell checker module.
1547 std::string sWordA;
1548 if (m_spellcodepage)
1550 int lengthIncTerminator = WideCharToMultiByte(m_spellcodepage, 0, sWord, -1, nullptr, 0, nullptr, nullptr);
1551 if (lengthIncTerminator <= 1)
1552 return ""; // converting to the codepage failed
1553 sWordA.resize(lengthIncTerminator - 1);
1554 WideCharToMultiByte(m_spellcodepage, 0, sWord, -1, sWordA.data(), lengthIncTerminator - 1, nullptr, nullptr);
1556 else
1557 sWordA = std::string(reinterpret_cast<LPCSTR>(static_cast<LPCWSTR>(sWord)));
1559 sWordA.erase(sWordA.find_last_not_of("\'\".,") + 1);
1560 sWordA.erase(0, sWordA.find_first_not_of("\'\".,"));
1562 if (m_bDoStyle)
1564 for (const auto styleindicator : { '*', '_', '^' })
1566 if (sWordA.empty())
1567 break;
1568 if (sWordA[sWordA.size() - 1] == styleindicator)
1569 sWordA.resize(sWordA.size() - 1);
1570 if (sWordA.empty())
1571 break;
1572 if (sWordA[0] == styleindicator)
1573 sWordA = sWordA.substr(sWordA.size() - 1);
1577 return sWordA;
1580 CString CSciEdit::GetWordFromSpellChecker(const std::string& sWordA)
1582 CString sWord;
1583 if (m_spellcodepage)
1585 wchar_t* buf = sWord.GetBuffer(static_cast<int>(sWordA.size()) * 2);
1586 int lengthIncTerminator = MultiByteToWideChar(m_spellcodepage, 0, sWordA.c_str(), -1, buf, static_cast<int>(sWordA.size()) * 2);
1587 if (lengthIncTerminator == 0)
1588 return L"";
1589 sWord.ReleaseBuffer(lengthIncTerminator - 1);
1591 else
1592 sWord = CString(sWordA.c_str());
1594 sWord.Trim(L"\'\".,");
1596 return sWord;
1599 bool CSciEdit::IsUTF8(LPVOID pBuffer, size_t cb)
1601 if (cb < 2)
1602 return true;
1603 auto pVal = static_cast<UINT16*>(pBuffer);
1604 auto pVal2 = reinterpret_cast<UINT8*>(pVal + 1);
1605 // scan the whole buffer for a 0x0000 sequence
1606 // if found, we assume a binary file
1607 for (size_t i=0; i<(cb-2); i=i+2)
1609 if (0x0000 == *pVal++)
1610 return false;
1612 pVal = static_cast<UINT16*>(pBuffer);
1613 if (*pVal == 0xFEFF)
1614 return false;
1615 if (cb < 3)
1616 return false;
1617 if (*pVal == 0xBBEF)
1619 if (*pVal2 == 0xBF)
1620 return true;
1622 // check for illegal UTF8 chars
1623 pVal2 = static_cast<UINT8*>(pBuffer);
1624 for (size_t i=0; i<cb; ++i)
1626 if ((*pVal2 == 0xC0)||(*pVal2 == 0xC1)||(*pVal2 >= 0xF5))
1627 return false;
1628 pVal2++;
1630 pVal2 = static_cast<UINT8*>(pBuffer);
1631 bool bUTF8 = false;
1632 for (size_t i=0; i<(cb-3); ++i)
1634 if ((*pVal2 & 0xE0)==0xC0)
1636 pVal2++;i++;
1637 if ((*pVal2 & 0xC0)!=0x80)
1638 return false;
1639 bUTF8 = true;
1641 if ((*pVal2 & 0xF0)==0xE0)
1643 pVal2++;i++;
1644 if ((*pVal2 & 0xC0)!=0x80)
1645 return false;
1646 pVal2++;i++;
1647 if ((*pVal2 & 0xC0)!=0x80)
1648 return false;
1649 bUTF8 = true;
1651 if ((*pVal2 & 0xF8)==0xF0)
1653 pVal2++;i++;
1654 if ((*pVal2 & 0xC0)!=0x80)
1655 return false;
1656 pVal2++;i++;
1657 if ((*pVal2 & 0xC0)!=0x80)
1658 return false;
1659 pVal2++;i++;
1660 if ((*pVal2 & 0xC0)!=0x80)
1661 return false;
1662 bUTF8 = true;
1664 pVal2++;
1666 if (bUTF8)
1667 return true;
1668 return false;
1671 void CSciEdit::SetAStyle(int style, COLORREF fore, COLORREF back, int size, const char *face)
1673 Call(SCI_STYLESETFORE, style, fore);
1674 Call(SCI_STYLESETBACK, style, back);
1675 if (size >= 1)
1676 Call(SCI_STYLESETSIZE, style, size);
1677 if (face)
1678 Call(SCI_STYLESETFONT, style, reinterpret_cast<LPARAM>(face));
1681 void CSciEdit::SetUDiffStyle()
1683 m_bUDiffmode = true;
1684 m_bDoStyle = false;
1685 Call(SCI_CLEARDOCUMENTSTYLE);
1686 Call(SCI_SETTABWIDTH, CRegStdDWORD(L"Software\\TortoiseGit\\UDiffTabSize", 4));
1688 SetReadOnly(true);
1690 //Set the default windows colors for edit controls
1691 SetColors(false);
1693 //SendEditor(SCI_SETREADONLY, FALSE);
1694 Call(SCI_CLEARALL);
1695 Call(SCI_EMPTYUNDOBUFFER);
1696 Call(SCI_SETSAVEPOINT);
1697 Call(SCI_CANCEL);
1698 Call(SCI_SETUNDOCOLLECTION, 0);
1700 Call(SCI_SETWRAPMODE,SC_WRAP_NONE);
1702 Call(SCI_GOTOPOS, 0);
1704 Call(SCI_SETVIEWWS, 1);
1705 Call(SCI_SETWHITESPACESIZE, 2);
1706 Call(SCI_SETWHITESPACEFORE, true, ::GetSysColor(COLOR_3DSHADOW));
1707 Call(SCI_STYLESETVISIBLE, STYLE_CONTROLCHAR, TRUE);
1709 Call(SCI_CLEARDOCUMENTSTYLE, 0, 0);
1711 if (CTheme::Instance().IsDarkTheme())
1713 Call(SCI_STYLESETFORE, STYLE_DEFAULT, UDiffTextColorDark);
1714 Call(SCI_STYLESETBACK, STYLE_DEFAULT, UDiffBackColorDark);
1715 Call(SCI_SETSELFORE, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHTTEXT)));
1716 Call(SCI_SETSELBACK, TRUE, CTheme::Instance().GetThemeColor(::GetSysColor(COLOR_HIGHLIGHT)));
1717 Call(SCI_SETCARETFORE, UDiffTextColorDark);
1718 Call(SCI_SETWHITESPACEFORE, true, RGB(180, 180, 180));
1719 SetAStyle(STYLE_DEFAULT, UDiffTextColorDark, UDiffBackColorDark,
1720 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1721 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1722 SetAStyle(SCE_DIFF_DEFAULT, UDiffTextColorDark, UDiffBackColorDark,
1723 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1724 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1725 Call(SCI_STYLECLEARALL);
1726 SetAStyle(SCE_DIFF_COMMAND,
1727 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeCommandColor", UDIFF_COLORFORECOMMAND_DARK),
1728 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackCommandColor", UDIFF_COLORBACKCOMMAND_DARK));
1729 SetAStyle(SCE_DIFF_POSITION,
1730 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForePositionColor", UDIFF_COLORFOREPOSITION_DARK),
1731 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackPositionColor", UDIFF_COLORBACKPOSITION_DARK));
1732 SetAStyle(SCE_DIFF_HEADER,
1733 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeHeaderColor", UDIFF_COLORFOREHEADER_DARK),
1734 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackHeaderColor", UDIFF_COLORBACKHEADER_DARK));
1735 SetAStyle(SCE_DIFF_COMMENT,
1736 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeCommentColor", UDIFF_COLORFORECOMMENT_DARK),
1737 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackCommentColor", UDIFF_COLORBACKCOMMENT_DARK));
1738 for (int style : { SCE_DIFF_ADDED, SCE_DIFF_PATCH_ADD, SCE_DIFF_PATCH_DELETE })
1740 SetAStyle(style,
1741 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeAddedColor", UDIFF_COLORFOREADDED_DARK),
1742 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackAddedColor", UDIFF_COLORBACKADDED_DARK));
1744 for (int style : { SCE_DIFF_DELETED, SCE_DIFF_REMOVED_PATCH_ADD, SCE_DIFF_REMOVED_PATCH_DELETE })
1746 SetAStyle(style,
1747 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffForeRemovedColor", UDIFF_COLORFOREREMOVED_DARK),
1748 CRegStdDWORD(L"Software\\TortoiseGit\\DarkUDiffBackRemovedColor", UDIFF_COLORBACKREMOVED_DARK));
1751 else
1753 Call(SCI_STYLESETFORE, STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT));
1754 Call(SCI_STYLESETBACK, STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOW));
1755 Call(SCI_SETSELFORE, TRUE, ::GetSysColor(COLOR_HIGHLIGHTTEXT));
1756 Call(SCI_SETSELBACK, TRUE, ::GetSysColor(COLOR_HIGHLIGHT));
1757 Call(SCI_SETCARETFORE, ::GetSysColor(COLOR_WINDOWTEXT));
1758 Call(SCI_SETWHITESPACEFORE, true, ::GetSysColor(COLOR_3DSHADOW));
1759 SetAStyle(STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT), ::GetSysColor(COLOR_WINDOW),
1760 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1761 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1762 SetAStyle(SCE_DIFF_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT), ::GetSysColor(COLOR_WINDOW),
1763 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1764 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1765 Call(SCI_STYLECLEARALL);
1766 SetAStyle(SCE_DIFF_COMMAND,
1767 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeCommandColor", UDIFF_COLORFORECOMMAND),
1768 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackCommandColor", UDIFF_COLORBACKCOMMAND));
1769 SetAStyle(SCE_DIFF_POSITION,
1770 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForePositionColor", UDIFF_COLORFOREPOSITION),
1771 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackPositionColor", UDIFF_COLORBACKPOSITION));
1772 SetAStyle(SCE_DIFF_HEADER,
1773 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeHeaderColor", UDIFF_COLORFOREHEADER),
1774 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackHeaderColor", UDIFF_COLORBACKHEADER));
1775 SetAStyle(SCE_DIFF_COMMENT,
1776 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeCommentColor", UDIFF_COLORFORECOMMENT),
1777 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackCommentColor", UDIFF_COLORBACKCOMMENT));
1778 for (int style : { SCE_DIFF_ADDED, SCE_DIFF_PATCH_ADD, SCE_DIFF_PATCH_DELETE })
1780 SetAStyle(style,
1781 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeAddedColor", UDIFF_COLORFOREADDED),
1782 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackAddedColor", UDIFF_COLORBACKADDED));
1784 for (int style : { SCE_DIFF_DELETED, SCE_DIFF_REMOVED_PATCH_ADD, SCE_DIFF_REMOVED_PATCH_DELETE })
1786 SetAStyle(style,
1787 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeRemovedColor", UDIFF_COLORFOREREMOVED),
1788 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackRemovedColor", UDIFF_COLORBACKREMOVED));
1791 Call(SCI_SETFOLDMARGINCOLOUR, true, RGB(240, 240, 240));
1792 Call(SCI_SETFOLDMARGINHICOLOUR, true, RGB(255, 255, 255));
1793 Call(SCI_STYLESETFORE, STYLE_LINENUMBER, RGB(109, 109, 109));
1794 Call(SCI_STYLESETBACK, STYLE_LINENUMBER, RGB(230, 230, 230));
1796 Call(SCI_STYLESETFORE, STYLE_BRACELIGHT, RGB(0, 150, 0));
1797 Call(SCI_STYLESETBOLD, STYLE_BRACELIGHT, 1);
1798 Call(SCI_STYLESETFORE, STYLE_BRACEBAD, RGB(255, 0, 0));
1799 Call(SCI_STYLESETBOLD, STYLE_BRACEBAD, 1);
1800 if (CTheme::Instance().IsDarkTheme() || CTheme::Instance().IsHighContrastModeDark())
1802 Call(SCI_SETFOLDMARGINCOLOUR, true, UDiffTextColorDark);
1803 Call(SCI_SETFOLDMARGINHICOLOUR, true, CTheme::darkBkColor);
1804 Call(SCI_STYLESETFORE, STYLE_LINENUMBER, RGB(140, 140, 140));
1805 Call(SCI_STYLESETBACK, STYLE_LINENUMBER, UDiffBackColorDark);
1808 auto curlexer = Call(SCI_GETLEXER);
1809 if (CTheme::Instance().IsHighContrastMode() && curlexer != SCLEX_NULL)
1811 Call(SCI_CLEARDOCUMENTSTYLE, 0, 0);
1812 Call(SCI_SETILEXER, reinterpret_cast<sptr_t>(nullptr));
1813 Call(SCI_COLOURISE, 0, -1);
1815 else if (!CTheme::Instance().IsHighContrastMode() && curlexer != SCLEX_DIFF)
1817 Call(SCI_CLEARDOCUMENTSTYLE, 0, 0);
1818 Call(SCI_STYLESETBOLD, SCE_DIFF_COMMENT, TRUE);
1819 Call(SCI_SETILEXER, 0, reinterpret_cast<sptr_t>(CreateLexer("diff")));
1820 Call(SCI_STYLESETBOLD, SCE_DIFF_COMMENT, TRUE);
1821 Call(SCI_SETKEYWORDS, 0, reinterpret_cast<LPARAM>("revision"));
1822 Call(SCI_COLOURISE, 0, -1);
1826 int CSciEdit::LoadFromFile(const CString& filename, UINT errorMsgId)
1828 CAutoFile hfile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
1829 if (!hfile)
1831 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_ICONEXCLAMATION);
1832 return -1;
1835 if (LARGE_INTEGER fileSize; !::GetFileSizeEx(hfile, &fileSize))
1837 MessageBox(static_cast<LPCWSTR>(CFormatMessageWrapper()), L"TortoiseGit", MB_ICONEXCLAMATION);
1838 return false;
1840 else if (fileSize.QuadPart >= 250 * 1024 * 1024) // styling gets really slow and Scintilla requires special initialization for large files
1842 CString error;
1843 error.LoadString(errorMsgId);
1844 MessageBox(error, L"TortoiseGit", MB_ICONEXCLAMATION);
1845 return false;
1848 char data[4096]{};
1849 DWORD size = 0;
1850 if (!ReadFile(hfile, data, sizeof(data), &size, nullptr))
1852 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_ICONEXCLAMATION);
1853 return -1;
1856 auto readonly = m_bReadOnly;
1857 SCOPE_EXIT { SetReadOnly(readonly); };
1858 SetReadOnly(false);
1860 int wasUndoCollection = static_cast<int>(Call(SCI_GETUNDOCOLLECTION));
1861 SCOPE_EXIT { Call(SCI_SETUNDOCOLLECTION, wasUndoCollection); };
1862 Call(SCI_SETUNDOCOLLECTION, 0);
1863 ClearUndoBuffer();
1864 Call(SCI_CLEARALL);
1865 Call(SCI_CANCEL);
1867 bool bUTF8 = IsUTF8(data, size);
1868 while (size > 0)
1870 Call(SCI_ADDTEXT, size, reinterpret_cast<LPARAM>(data));
1871 if (auto sciStatus = static_cast<int>(Call(SCI_GETSTATUS)); sciStatus > SC_STATUS_OK && sciStatus < SC_STATUS_WARN_START)
1873 if (sciStatus == SC_STATUS_BADALLOC)
1874 MessageBox(static_cast<LPCWSTR>(CFormatMessageWrapper(static_cast<DWORD>(E_OUTOFMEMORY))), L"TortoiseGit", MB_ICONEXCLAMATION);
1875 else
1876 MessageBox(static_cast<LPCWSTR>(CFormatMessageWrapper(static_cast<DWORD>(E_FAIL))), L"TortoiseGit", MB_ICONEXCLAMATION);
1878 Call(SCI_CLEARALL);
1879 Call(SCI_CANCEL);
1880 return -1;
1882 if (!ReadFile(hfile, data, sizeof(data), &size, nullptr))
1884 Call(SCI_CLEARALL);
1885 Call(SCI_CANCEL);
1886 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_ICONEXCLAMATION);
1887 return -1;
1890 Call(SCI_SETCODEPAGE, bUTF8 ? SC_CP_UTF8 : GetACP());
1891 return 0;
1894 void CSciEdit::RestyleBugIDs()
1896 auto endstylepos = static_cast<int>(Call(SCI_GETLENGTH));
1897 // clear all styles
1898 Call(SCI_STARTSTYLING, 0, STYLE_MASK);
1899 Call(SCI_SETSTYLING, endstylepos, STYLE_DEFAULT);
1900 // style the bug IDs
1901 MarkEnteredBugID(0, endstylepos);
1904 ULONG CSciEdit::GetGestureStatus(CPoint /*ptTouch*/)
1906 return 0;
1909 BOOL CSciEdit::EnableWindow(BOOL bEnable /*= TRUE*/)
1911 auto ret = __super::EnableWindow(bEnable);
1912 SetColors(true);
1913 return ret;
1916 void CSciEdit::SetReadOnly(bool bReadOnly)
1918 m_bReadOnly = bReadOnly;
1919 Call(SCI_SETREADONLY, m_bReadOnly);
1922 void CSciEdit::ClearUndoBuffer()
1924 Call(SCI_EMPTYUNDOBUFFER);
1925 Call(SCI_SETSAVEPOINT);
1928 void CSciEdit::SetWindowStylesForAutocompletionPopup()
1930 if (CTheme::Instance().IsDarkTheme())
1932 EnumThreadWindows(GetCurrentThreadId(), AdjustThemeProc, 0);
1936 BOOL CSciEdit::AdjustThemeProc(HWND hwnd, LPARAM /*lParam*/)
1938 wchar_t szWndClassName[MAX_PATH] = { 0 };
1939 GetClassName(hwnd, szWndClassName, _countof(szWndClassName));
1940 if ((wcscmp(szWndClassName, L"ListBoxX") == 0) ||
1941 (wcscmp(szWndClassName, WC_LISTBOX) == 0))
1943 // in dark mode, the resizing border is visible at the top
1944 // of the popup, and it's white and ugly.
1945 // this removes the border, but that also means that the
1946 // popup is not resizable anymore - which I think is not
1947 // really necessary anyway.
1948 auto dwCurStyle = static_cast<DWORD>(GetWindowLongPtr(hwnd, GWL_STYLE));
1949 dwCurStyle &= ~WS_THICKFRAME;
1950 dwCurStyle |= WS_BORDER;
1951 SetWindowLongPtr(hwnd, GWL_STYLE, dwCurStyle);
1953 DarkModeHelper::Instance().AllowDarkModeForWindow(hwnd, TRUE);
1954 SetWindowTheme(hwnd, L"Explorer", nullptr);
1955 EnumChildWindows(hwnd, AdjustThemeProc, 0);
1958 return TRUE;