No need for an additional CStringA construction
[TortoiseGit.git] / src / Utils / MiscUI / SciEdit.cpp
blob7aab4b9dbf137f43cb27dc21a170c588f80211f1
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2009-2017 - TortoiseGit
4 // Copyright (C) 2003-2008,2012-2016 - 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"
30 void CSciEditContextMenuInterface::InsertMenuItems(CMenu&, int&) {return;}
31 bool CSciEditContextMenuInterface::HandleMenuItemClick(int, CSciEdit *) {return false;}
32 void CSciEditContextMenuInterface::HandleSnippet(int, const CString &, CSciEdit *) { return; }
35 #define STYLE_ISSUEBOLD 11
36 #define STYLE_ISSUEBOLDITALIC 12
37 #define STYLE_BOLD 14
38 #define STYLE_ITALIC 15
39 #define STYLE_UNDERLINED 16
40 #define STYLE_URL 17
41 #define INDIC_MISSPELLED 18
43 #define STYLE_MASK 0x1f
45 #define SCI_ADDWORD 2000
47 struct loc_map {
48 const char * cp;
49 const char * def_enc;
52 struct loc_map enc2locale[] = {
53 {"28591","ISO8859-1"},
54 {"28592","ISO8859-2"},
55 {"28593","ISO8859-3"},
56 {"28594","ISO8859-4"},
57 {"28595","ISO8859-5"},
58 {"28596","ISO8859-6"},
59 {"28597","ISO8859-7"},
60 {"28598","ISO8859-8"},
61 {"28599","ISO8859-9"},
62 {"28605","ISO8859-15"},
63 {"20866","KOI8-R"},
64 {"21866","KOI8-U"},
65 {"1251","microsoft-cp1251"},
66 {"65001","UTF-8"},
70 IMPLEMENT_DYNAMIC(CSciEdit, CWnd)
72 CSciEdit::CSciEdit(void) : m_DirectFunction(NULL)
73 , m_DirectPointer(NULL)
74 , m_spellcodepage(0)
75 , m_separator(0)
76 , m_typeSeparator(1)
77 , m_bDoStyle(false)
78 , m_nAutoCompleteMinChars(3)
79 , m_SpellingCache(2000)
80 , m_blockModifiedHandler(false)
82 m_hModule = ::LoadLibrary(L"SciLexer_tgit.dll");
85 CSciEdit::~CSciEdit(void)
87 m_personalDict.Save();
90 static std::unique_ptr<UINT[]> Icon2Image(HICON hIcon)
92 if (hIcon == nullptr)
93 return nullptr;
95 ICONINFO iconInfo;
96 if (!GetIconInfo(hIcon, &iconInfo))
97 return nullptr;
99 BITMAP bm;
100 if (!GetObject(iconInfo.hbmColor, sizeof(BITMAP), &bm))
101 return nullptr;
103 int width = bm.bmWidth;
104 int height = bm.bmHeight;
105 int bytesPerScanLine = (width * 3 + 3) & 0xFFFFFFFC;
106 int size = bytesPerScanLine * height;
107 BITMAPINFO infoheader;
108 infoheader.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
109 infoheader.bmiHeader.biWidth = width;
110 infoheader.bmiHeader.biHeight = height;
111 infoheader.bmiHeader.biPlanes = 1;
112 infoheader.bmiHeader.biBitCount = 24;
113 infoheader.bmiHeader.biCompression = BI_RGB;
114 infoheader.bmiHeader.biSizeImage = size;
116 auto ptrb = std::make_unique<BYTE[]>(size * 2 + height * width * 4);
117 LPBYTE pixelsIconRGB = ptrb.get();
118 LPBYTE alphaPixels = pixelsIconRGB + size;
119 HDC hDC = CreateCompatibleDC(nullptr);
120 SCOPE_EXIT { DeleteDC(hDC); };
121 HBITMAP hBmpOld = (HBITMAP)SelectObject(hDC, (HGDIOBJ)iconInfo.hbmColor);
122 if (!GetDIBits(hDC, iconInfo.hbmColor, 0, height, (LPVOID)pixelsIconRGB, &infoheader, DIB_RGB_COLORS))
123 return nullptr;
125 SelectObject(hDC, hBmpOld);
126 if (!GetDIBits(hDC, iconInfo.hbmMask, 0,height, (LPVOID)alphaPixels, &infoheader, DIB_RGB_COLORS))
127 return nullptr;
129 auto imagePixels = std::make_unique<UINT[]>(height * width);
130 int lsSrc = width * 3;
131 int vsDest = height - 1;
132 for (int y = 0; y < height; y++)
134 int linePosSrc = (vsDest - y) * lsSrc;
135 int linePosDest = y * width;
136 for (int x = 0; x < width; x++)
138 int currentDestPos = linePosDest + x;
139 int currentSrcPos = linePosSrc + x * 3;
140 imagePixels[currentDestPos] = (((UINT)(
142 ((pixelsIconRGB[currentSrcPos + 2] /*Red*/)
143 | (pixelsIconRGB[currentSrcPos + 1] << 8 /*Green*/))
144 | pixelsIconRGB[currentSrcPos] << 16 /*Blue*/
146 | ((alphaPixels[currentSrcPos] ? 0 : 0xff) << 24))) & 0xffffffff);
149 return imagePixels;
152 void CSciEdit::SetColors(bool recolorize)
154 Call(SCI_STYLESETFORE, STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT));
155 Call(SCI_STYLESETBACK, STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOW));
156 Call(SCI_SETSELFORE, TRUE, ::GetSysColor(COLOR_HIGHLIGHTTEXT));
157 Call(SCI_SETSELBACK, TRUE, ::GetSysColor(COLOR_HIGHLIGHT));
158 Call(SCI_SETCARETFORE, ::GetSysColor(COLOR_WINDOWTEXT));
160 if (recolorize)
161 Call(SCI_COLOURISE, 0, -1);
164 void CSciEdit::Init(LONG lLanguage)
166 //Setup the direct access data
167 m_DirectFunction = SendMessage(SCI_GETDIRECTFUNCTION, 0, 0);
168 m_DirectPointer = SendMessage(SCI_GETDIRECTPOINTER, 0, 0);
169 Call(SCI_SETMARGINWIDTHN, 1, 0);
170 Call(SCI_SETUSETABS, 0); //pressing TAB inserts spaces
171 Call(SCI_SETWRAPVISUALFLAGS, SC_WRAPVISUALFLAG_END);
172 Call(SCI_AUTOCSETIGNORECASE, 1);
173 Call(SCI_SETLEXER, SCLEX_CONTAINER);
174 Call(SCI_SETCODEPAGE, SC_CP_UTF8);
175 Call(SCI_AUTOCSETFILLUPS, 0, (LPARAM)"\t([");
176 Call(SCI_AUTOCSETMAXWIDTH, 0);
177 //Set the default windows colors for edit controls
178 SetColors(false);
179 Call(SCI_SETMODEVENTMASK, SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_PERFORMED_UNDO | SC_PERFORMED_REDO);
180 Call(SCI_INDICSETSTYLE, INDIC_MISSPELLED, INDIC_SQUIGGLE);
181 Call(SCI_INDICSETFORE, INDIC_MISSPELLED, RGB(255,0,0));
182 CStringA sWordChars;
183 CStringA sWhiteSpace;
184 for (int i=0; i<255; ++i)
186 if (i == '\r' || i == '\n')
187 continue;
188 else if (i < 0x20 || i == ' ')
189 sWhiteSpace += (char)i;
190 else if (isalnum(i) || i == '\'' || i == '_' || i == '-')
191 sWordChars += (char)i;
193 Call(SCI_SETWORDCHARS, 0, (LPARAM)(LPCSTR)sWordChars);
194 Call(SCI_SETWHITESPACECHARS, 0, (LPARAM)(LPCSTR)sWhiteSpace);
195 m_bDoStyle = ((DWORD)CRegStdDWORD(L"Software\\TortoiseGit\\StyleCommitMessages", TRUE)) == TRUE;
196 m_nAutoCompleteMinChars = (int)(DWORD)CRegStdDWORD(L"Software\\TortoiseGit\\AutoCompleteMinChars", 3);
197 // look for dictionary files and use them if found
198 if ((lLanguage >= 0) && (((DWORD)CRegStdDWORD(L"Software\\TortoiseGit\\Spellchecker", TRUE)) == TRUE))
200 if (!lLanguage || (lLanguage && !LoadDictionaries(lLanguage)))
202 long langId = GetUserDefaultLCID();
205 LoadDictionaries(langId);
206 DWORD lid = SUBLANGID(langId);
207 lid--;
208 if (lid > 0)
209 langId = MAKELANGID(PRIMARYLANGID(langId), lid);
210 else if (langId == 1033)
211 langId = 0;
212 else
213 langId = 1033;
214 } while (langId && (!pChecker || !pThesaur));
218 Call(SCI_SETEDGEMODE, EDGE_NONE);
219 Call(SCI_SETWRAPMODE, SC_WRAP_WORD);
220 Call(SCI_ASSIGNCMDKEY, SCK_END, SCI_LINEENDWRAP);
221 Call(SCI_ASSIGNCMDKEY, SCK_END + (SCMOD_SHIFT << 16), SCI_LINEENDWRAPEXTEND);
222 Call(SCI_ASSIGNCMDKEY, SCK_HOME, SCI_HOMEWRAP);
223 Call(SCI_ASSIGNCMDKEY, SCK_HOME + (SCMOD_SHIFT << 16), SCI_HOMEWRAPEXTEND);
224 if (CRegStdDWORD(L"Software\\TortoiseGit\\ScintillaDirect2D", FALSE) != FALSE)
226 Call(SCI_SETTECHNOLOGY, SC_TECHNOLOGY_DIRECTWRITERETAIN);
227 Call(SCI_SETBUFFEREDDRAW, 0);
232 void CSciEdit::Init(const ProjectProperties& props)
234 Init(props.lProjectLanguage);
235 m_sCommand = CUnicodeUtils::GetUTF8(props.GetCheckRe());
236 m_sBugID = CUnicodeUtils::GetUTF8(props.GetBugIDRe());
237 m_sUrl = CUnicodeUtils::GetUTF8(props.sUrl);
239 Call(SCI_SETMOUSEDWELLTIME, 333);
241 if (props.nLogWidthMarker)
243 Call(SCI_SETWRAPMODE, SC_WRAP_NONE);
244 Call(SCI_SETEDGEMODE, EDGE_LINE);
245 Call(SCI_SETEDGECOLUMN, props.nLogWidthMarker);
246 Call(SCI_SETSCROLLWIDTHTRACKING, TRUE);
247 Call(SCI_SETSCROLLWIDTH, 1);
249 else
251 Call(SCI_SETEDGEMODE, EDGE_NONE);
252 Call(SCI_SETWRAPMODE, SC_WRAP_WORD);
256 void CSciEdit::SetIcon(const std::map<int, UINT> &icons)
258 int iconWidth = GetSystemMetrics(SM_CXSMICON);
259 int iconHeight = GetSystemMetrics(SM_CYSMICON);
260 Call(SCI_RGBAIMAGESETWIDTH, iconWidth);
261 Call(SCI_RGBAIMAGESETHEIGHT, iconHeight);
262 for (auto icon : icons)
264 auto hIcon = (HICON)::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(icon.second), IMAGE_ICON, iconWidth, iconHeight, LR_DEFAULTCOLOR);
265 auto bytes = Icon2Image(hIcon);
266 DestroyIcon(hIcon);
267 Call(SCI_REGISTERRGBAIMAGE, icon.first, (LPARAM)bytes.get());
271 BOOL CSciEdit::LoadDictionaries(LONG lLanguageID)
273 //Setup the spell checker and thesaurus
274 TCHAR buf[6] = { 0 };
275 CString sFolderUp = CPathUtils::GetAppParentDirectory();
276 CString sFolderAppData = CPathUtils::GetAppDataDirectory();
277 CString sFile;
279 GetLocaleInfo(MAKELCID(lLanguageID, SORT_DEFAULT), LOCALE_SISO639LANGNAME, buf, _countof(buf));
280 sFile = buf;
281 if (lLanguageID == 2074)
282 sFile += L"-Latn";
283 sFile += L'_';
284 GetLocaleInfo(MAKELCID(lLanguageID, SORT_DEFAULT), LOCALE_SISO3166CTRYNAME, buf, _countof(buf));
285 sFile += buf;
286 if (!pChecker)
288 if ((PathFileExists(sFolderAppData + L"dic\\" + sFile + L".aff")) &&
289 (PathFileExists(sFolderAppData + L"dic\\" + sFile + L".dic")))
291 pChecker = std::make_unique<Hunspell>(CStringA(sFolderAppData + L"dic\\" + sFile + L".aff"), CStringA(sFolderAppData + L"dic\\" + sFile + L".dic"));
293 else if ((PathFileExists(sFolderUp + L"Languages\\" + sFile + L".aff")) &&
294 (PathFileExists(sFolderUp + L"Languages\\" + sFile + L".dic")))
296 pChecker = std::make_unique<Hunspell>(CStringA(sFolderUp + L"Languages\\" + sFile + L".aff"), CStringA(sFolderUp + L"Languages\\" + sFile + L".dic"));
298 if (pChecker)
300 const char* encoding = pChecker->get_dic_encoding();
301 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": %s\n", encoding);
302 m_spellcodepage = 0;
303 for (int i = 0; i < _countof(enc2locale); ++i)
305 if (strcmp(encoding, enc2locale[i].def_enc) == 0)
306 m_spellcodepage = atoi(enc2locale[i].cp);
308 m_personalDict.Init(lLanguageID);
311 #if THESAURUS
312 if (!pThesaur)
314 if ((PathFileExists(sFolderAppData + L"dic\\th_" + sFile + L"_v2.idx")) &&
315 (PathFileExists(sFolderAppData + L"dic\\th_" + sFile + L"_v2.dat")))
317 pThesaur = std::make_unique<MyThes>(CStringA(sFolderAppData + L"dic\\th_" + sFile + L"_v2.idx"), CStringA(sFolderAppData + L"dic\\th_" + sFile + L"_v2.dat"));
319 else if ((PathFileExists(sFolderUp + L"Languages\\th_" + sFile + L"_v2.idx")) &&
320 (PathFileExists(sFolderUp + L"Languages\\th_" + sFile + L"_v2.dat")))
322 pThesaur = std::make_unique<MyThes>(CStringA(sFolderUp + L"Languages\\th_" + sFile + L"_v2.idx"), CStringA(sFolderUp + L"Languages\\th_" + sFile + L"_v2.dat"));
325 #endif
326 if ((pThesaur)||(pChecker))
327 return TRUE;
328 return FALSE;
331 LRESULT CSciEdit::Call(UINT message, WPARAM wParam, LPARAM lParam)
333 ASSERT(::IsWindow(m_hWnd)); //Window must be valid
334 ASSERT(m_DirectFunction); //Direct function must be valid
335 return ((SciFnDirect) m_DirectFunction)(m_DirectPointer, message, wParam, lParam);
338 CString CSciEdit::StringFromControl(const CStringA& text)
340 CString sText;
341 #ifdef UNICODE
342 int codepage = (int)Call(SCI_GETCODEPAGE);
343 int reslen = MultiByteToWideChar(codepage, 0, text, text.GetLength(), 0, 0);
344 MultiByteToWideChar(codepage, 0, text, text.GetLength(), sText.GetBuffer(reslen+1), reslen+1);
345 sText.ReleaseBuffer(reslen);
346 #else
347 sText = text;
348 #endif
349 return sText;
352 CStringA CSciEdit::StringForControl(const CString& text)
354 CStringA sTextA;
355 #ifdef UNICODE
356 int codepage = (int)SendMessage(SCI_GETCODEPAGE);
357 int reslen = WideCharToMultiByte(codepage, 0, text, text.GetLength(), 0, 0, 0, 0);
358 WideCharToMultiByte(codepage, 0, text, text.GetLength(), sTextA.GetBuffer(reslen), reslen, 0, 0);
359 sTextA.ReleaseBuffer(reslen);
360 #else
361 sTextA = text;
362 #endif
363 ATLTRACE("string length %d\n", sTextA.GetLength());
364 return sTextA;
367 void CSciEdit::SetText(const CString& sText)
369 CStringA sTextA = StringForControl(sText);
370 Call(SCI_SETTEXT, 0, (LPARAM)(LPCSTR)sTextA);
372 if (Call(SCI_GETSCROLLWIDTHTRACKING) != 0)
373 Call(SCI_SETSCROLLWIDTH, 1);
375 // Scintilla seems to have problems with strings that
376 // aren't terminated by a newline char. Once that char
377 // is there, it can be removed without problems.
378 // So we add here a newline, then remove it again.
379 Call(SCI_DOCUMENTEND);
380 Call(SCI_NEWLINE);
381 Call(SCI_DELETEBACK);
384 void CSciEdit::InsertText(const CString& sText, bool bNewLine)
386 CStringA sTextA = StringForControl(sText);
387 Call(SCI_REPLACESEL, 0, (LPARAM)(LPCSTR)sTextA);
388 if (bNewLine)
389 Call(SCI_REPLACESEL, 0, (LPARAM)(LPCSTR)"\n");
392 CString CSciEdit::GetText()
394 LRESULT len = Call(SCI_GETTEXT, 0, 0);
395 CStringA sTextA;
396 Call(SCI_GETTEXT, (WPARAM)(len + 1), (LPARAM)(LPCSTR)CStrBufA(sTextA, (int)len + 1));
397 return StringFromControl(sTextA);
400 CString CSciEdit::GetWordUnderCursor(bool bSelectWord)
402 TEXTRANGEA textrange;
403 int pos = (int)Call(SCI_GETCURRENTPOS);
404 textrange.chrg.cpMin = (LONG)Call(SCI_WORDSTARTPOSITION, pos, TRUE);
405 if ((pos == textrange.chrg.cpMin)||(textrange.chrg.cpMin < 0))
406 return CString();
407 textrange.chrg.cpMax = (LONG)Call(SCI_WORDENDPOSITION, textrange.chrg.cpMin, TRUE);
409 auto textbuffer = std::make_unique<char[]>(textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
410 textrange.lpstrText = textbuffer.get();
411 Call(SCI_GETTEXTRANGE, 0, (LPARAM)&textrange);
412 if (bSelectWord)
413 Call(SCI_SETSEL, textrange.chrg.cpMin, textrange.chrg.cpMax);
414 CString sRet = StringFromControl(textbuffer.get());
415 return sRet;
418 void CSciEdit::SetFont(CString sFontName, int iFontSizeInPoints)
420 Call(SCI_STYLESETFONT, STYLE_DEFAULT, (LPARAM)(LPCSTR)CUnicodeUtils::GetUTF8(sFontName).GetBuffer());
421 Call(SCI_STYLESETSIZE, STYLE_DEFAULT, iFontSizeInPoints);
422 Call(SCI_STYLECLEARALL);
424 LPARAM color = (LPARAM)GetSysColor(COLOR_HOTLIGHT);
425 // set the styles for the bug ID strings
426 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLD, (LPARAM)TRUE);
427 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLD, color);
428 Call(SCI_STYLESETBOLD, STYLE_ISSUEBOLDITALIC, (LPARAM)TRUE);
429 Call(SCI_STYLESETITALIC, STYLE_ISSUEBOLDITALIC, (LPARAM)TRUE);
430 Call(SCI_STYLESETFORE, STYLE_ISSUEBOLDITALIC, color);
431 Call(SCI_STYLESETHOTSPOT, STYLE_ISSUEBOLDITALIC, (LPARAM)TRUE);
433 // set the formatted text styles
434 Call(SCI_STYLESETBOLD, STYLE_BOLD, (LPARAM)TRUE);
435 Call(SCI_STYLESETITALIC, STYLE_ITALIC, (LPARAM)TRUE);
436 Call(SCI_STYLESETUNDERLINE, STYLE_UNDERLINED, (LPARAM)TRUE);
438 // set the style for URLs
439 Call(SCI_STYLESETFORE, STYLE_URL, color);
440 Call(SCI_STYLESETHOTSPOT, STYLE_URL, (LPARAM)TRUE);
442 Call(SCI_SETHOTSPOTACTIVEUNDERLINE, (LPARAM)TRUE);
445 void CSciEdit::SetAutoCompletionList(const std::map<CString, int>& list, TCHAR separator, TCHAR typeSeparator)
447 //copy the auto completion list.
449 //SK: instead of creating a copy of that list, we could accept a pointer
450 //to the list and use that instead. But then the caller would have to make
451 //sure that the list persists over the lifetime of the control!
452 m_autolist.clear();
453 m_autolist = list;
454 m_separator = separator;
455 m_typeSeparator = typeSeparator;
458 BOOL CSciEdit::IsMisspelled(const CString& sWord)
460 // convert the string from the control to the encoding of the spell checker module.
461 CStringA sWordA = GetWordForSpellChecker(sWord);
463 // words starting with a digit are treated as correctly spelled
464 if (_istdigit(sWord.GetAt(0)))
465 return FALSE;
466 // words in the personal dictionary are correct too
467 if (m_personalDict.FindWord(sWord))
468 return FALSE;
470 // Check spell checking cache first.
471 const BOOL *cacheResult = m_SpellingCache.try_get(std::wstring(sWord, sWord.GetLength()));
472 if (cacheResult)
473 return *cacheResult;
475 // now we actually check the spelling...
476 BOOL misspelled = !pChecker->spell(sWordA);
477 if (misspelled)
479 // the word is marked as misspelled, we now check whether the word
480 // is maybe a composite identifier
481 // a composite identifier consists of multiple words, with each word
482 // separated by a change in lower to uppercase letters
483 misspelled = FALSE;
484 if (sWord.GetLength() > 1)
486 int wordstart = 0;
487 int wordend = 1;
488 while (wordend < sWord.GetLength())
490 while ((wordend < sWord.GetLength())&&(!_istupper(sWord[wordend])))
491 wordend++;
492 if ((wordstart == 0)&&(wordend == sWord.GetLength()))
494 // words in the auto list are also assumed correctly spelled
495 if (m_autolist.find(sWord) != m_autolist.end())
496 misspelled = FALSE;
497 else
498 misspelled = TRUE;
499 break;
501 sWordA = GetWordForSpellChecker(sWord.Mid(wordstart, wordend - wordstart));
502 if ((sWordA.GetLength() > 2) && (!pChecker->spell(sWordA)))
504 misspelled = TRUE;
505 break;
507 wordstart = wordend;
508 wordend++;
513 // Update cache.
514 m_SpellingCache.insert_or_assign(std::wstring(sWord, sWord.GetLength()), misspelled);
515 return misspelled;
518 void CSciEdit::CheckSpelling(int startpos, int endpos)
520 if (!pChecker)
521 return;
523 TEXTRANGEA textrange;
524 textrange.chrg.cpMin = startpos;
525 textrange.chrg.cpMax = (LONG)textrange.chrg.cpMin;
526 LRESULT lastpos = endpos;
527 if (lastpos < 0)
528 lastpos = Call(SCI_GETLENGTH)-textrange.chrg.cpMin;
529 Call(SCI_SETINDICATORCURRENT, INDIC_MISSPELLED);
530 while (textrange.chrg.cpMax < lastpos)
532 textrange.chrg.cpMin = (LONG)Call(SCI_WORDSTARTPOSITION, textrange.chrg.cpMax+1, TRUE);
533 if (textrange.chrg.cpMin < textrange.chrg.cpMax)
534 break;
535 textrange.chrg.cpMax = (LONG)Call(SCI_WORDENDPOSITION, textrange.chrg.cpMin, TRUE);
536 if (textrange.chrg.cpMin == textrange.chrg.cpMax)
538 textrange.chrg.cpMax++;
539 // since Scintilla squiggles to the end of the text even if told to stop one char before it,
540 // we have to clear here the squiggly lines to the end.
541 if (textrange.chrg.cpMin)
542 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin-1, textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
543 continue;
545 ATLASSERT(textrange.chrg.cpMax >= textrange.chrg.cpMin);
546 auto textbuffer = std::make_unique<char[]>(textrange.chrg.cpMax - textrange.chrg.cpMin + 2);
547 SecureZeroMemory(textbuffer.get(), textrange.chrg.cpMax - textrange.chrg.cpMin + 2);
548 textrange.lpstrText = textbuffer.get();
549 textrange.chrg.cpMax++;
550 Call(SCI_GETTEXTRANGE, 0, (LPARAM)&textrange);
551 int len = (int)strlen(textrange.lpstrText);
552 if (len == 0)
554 textrange.chrg.cpMax--;
555 Call(SCI_GETTEXTRANGE, 0, (LPARAM)&textrange);
556 len = (int)strlen(textrange.lpstrText);
557 textrange.chrg.cpMax++;
558 len++;
560 if (len && textrange.lpstrText[len - 1] == '.')
562 // Try to ignore file names from the auto list.
563 // Do do this, for each word ending with '.' we extract next word and check
564 // whether the combined string is present in auto list.
565 TEXTRANGEA twoWords;
566 twoWords.chrg.cpMin = textrange.chrg.cpMin;
567 twoWords.chrg.cpMax = (LONG)Call(SCI_WORDENDPOSITION, textrange.chrg.cpMax + 1, TRUE);
568 auto twoWordsBuffer = std::make_unique<char[]>(twoWords.chrg.cpMax - twoWords.chrg.cpMin + 1);
569 twoWords.lpstrText = twoWordsBuffer.get();
570 SecureZeroMemory(twoWords.lpstrText, twoWords.chrg.cpMax - twoWords.chrg.cpMin + 1);
571 Call(SCI_GETTEXTRANGE, 0, (LPARAM)&twoWords);
572 CString sWord = StringFromControl(twoWords.lpstrText);
573 if (m_autolist.find(sWord) != m_autolist.end())
575 //mark word as correct (remove the squiggle line)
576 Call(SCI_INDICATORCLEARRANGE, twoWords.chrg.cpMin, twoWords.chrg.cpMax - twoWords.chrg.cpMin);
577 textrange.chrg.cpMax = twoWords.chrg.cpMax;
578 continue;
581 if (len)
582 textrange.lpstrText[len - 1] = '\0';
583 textrange.chrg.cpMax--;
584 if (textrange.lpstrText[0])
586 CString sWord = StringFromControl(textrange.lpstrText);
587 if ((GetStyleAt(textrange.chrg.cpMin) != STYLE_URL) && IsMisspelled(sWord))
589 //mark word as misspelled
590 Call(SCI_INDICATORFILLRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin);
592 else
594 //mark word as correct (remove the squiggle line)
595 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin);
596 Call(SCI_INDICATORCLEARRANGE, textrange.chrg.cpMin, textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
602 void CSciEdit::SuggestSpellingAlternatives()
604 if (!pChecker)
605 return;
606 CString word = GetWordUnderCursor(true);
607 Call(SCI_SETCURRENTPOS, Call(SCI_WORDSTARTPOSITION, Call(SCI_GETCURRENTPOS), TRUE));
608 if (word.IsEmpty())
609 return;
610 char ** wlst = nullptr;
611 int ns = pChecker->suggest(&wlst, GetWordForSpellChecker(word));
612 if (ns > 0)
614 CString suggestions;
615 for (int i=0; i < ns; i++)
617 suggestions.AppendFormat(L"%s%c%d%c", (LPCTSTR)GetWordFromSpellChecker(wlst[i]), m_typeSeparator, AUTOCOMPLETE_SPELLING, m_separator);
618 free(wlst[i]);
620 free(wlst);
621 suggestions.TrimRight(m_separator);
622 if (suggestions.IsEmpty())
623 return;
624 Call(SCI_AUTOCSETSEPARATOR, (WPARAM)CStringA(m_separator).GetAt(0));
625 Call(SCI_AUTOCSETTYPESEPARATOR, (WPARAM)m_typeSeparator);
626 Call(SCI_AUTOCSETDROPRESTOFWORD, 1);
627 Call(SCI_AUTOCSHOW, 0, (LPARAM)(LPCSTR)StringForControl(suggestions));
628 return;
630 free(wlst);
633 void CSciEdit::DoAutoCompletion(int nMinPrefixLength)
635 if (m_autolist.empty())
636 return;
637 CString word = GetWordUnderCursor();
638 if (word.GetLength() < nMinPrefixLength)
639 return; //don't auto complete yet, word is too short
640 int pos = (int)Call(SCI_GETCURRENTPOS);
641 if (pos != Call(SCI_WORDENDPOSITION, pos, TRUE))
642 return; //don't auto complete if we're not at the end of a word
643 CString sAutoCompleteList;
645 std::vector<CString> words;
647 pos = word.Find('-');
649 CString wordLower = word;
650 wordLower.MakeLower();
651 CString wordHigher = word;
652 wordHigher.MakeUpper();
654 words.push_back(word);
655 words.push_back(wordLower);
656 words.push_back(wordHigher);
658 if (pos >= 0)
660 CString s = wordLower.Left(pos);
661 if (s.GetLength() >= nMinPrefixLength)
662 words.push_back(s);
663 s = wordLower.Mid(pos+1);
664 if (s.GetLength() >= nMinPrefixLength)
665 words.push_back(s);
666 s = wordHigher.Left(pos);
667 if (s.GetLength() >= nMinPrefixLength)
668 words.push_back(wordHigher.Left(pos));
669 s = wordHigher.Mid(pos+1);
670 if (s.GetLength() >= nMinPrefixLength)
671 words.push_back(wordHigher.Mid(pos+1));
674 std::map<CString, int> wordset;
675 for (const auto& w : words)
677 for (auto lowerit = m_autolist.lower_bound(w);
678 lowerit != m_autolist.end(); ++lowerit)
680 int compare = w.CompareNoCase(lowerit->first.Left(w.GetLength()));
681 if (compare>0)
682 continue;
683 else if (compare == 0)
684 wordset.emplace(lowerit->first, lowerit->second);
685 else
686 break;
690 for (const auto& w : wordset)
691 sAutoCompleteList.AppendFormat(L"%s%c%d%c", (LPCTSTR)w.first, m_typeSeparator, w.second, m_separator);
693 sAutoCompleteList.TrimRight(m_separator);
694 if (sAutoCompleteList.IsEmpty())
695 return;
697 Call(SCI_AUTOCSETSEPARATOR, (WPARAM)CStringA(m_separator).GetAt(0));
698 Call(SCI_AUTOCSETTYPESEPARATOR, (WPARAM)m_typeSeparator);
699 auto sForControl = StringForControl(sAutoCompleteList);
700 Call(SCI_AUTOCSHOW, StringForControl(word).GetLength(), (LPARAM)(LPCSTR)sForControl);
703 BOOL CSciEdit::OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult)
705 if (message != WM_NOTIFY)
706 return CWnd::OnChildNotify(message, wParam, lParam, pLResult);
708 LPNMHDR lpnmhdr = (LPNMHDR) lParam;
709 SCNotification * lpSCN = (SCNotification *)lParam;
711 if(lpnmhdr->hwndFrom==m_hWnd)
713 switch(lpnmhdr->code)
715 case SCN_CHARADDED:
717 if ((lpSCN->ch < 32)&&(lpSCN->ch != 13)&&(lpSCN->ch != 10))
718 Call(SCI_DELETEBACK);
719 else
720 DoAutoCompletion(m_nAutoCompleteMinChars);
721 return TRUE;
723 break;
724 case SCN_AUTOCSELECTION:
726 CString text = StringFromControl(lpSCN->text);
727 if (m_autolist[text] == AUTOCOMPLETE_SNIPPET)
729 Call(SCI_AUTOCCANCEL);
730 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
732 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
733 pHandler->HandleSnippet(m_autolist[text], text, this);
736 return TRUE;
738 case SCN_STYLENEEDED:
740 int startpos = (int)Call(SCI_GETENDSTYLED);
741 int endpos = ((SCNotification*)lpnmhdr)->position;
743 int startwordpos = (int)Call(SCI_WORDSTARTPOSITION, startpos, true);
744 int endwordpos = (int)Call(SCI_WORDENDPOSITION, endpos, true);
746 MarkEnteredBugID(startwordpos, endwordpos);
747 if (m_bDoStyle)
748 StyleEnteredText(startwordpos, endwordpos);
750 StyleURLs(startwordpos, endwordpos);
751 CheckSpelling(startwordpos, endwordpos);
753 // Tell scintilla editor that we styled all requested range.
754 Call(SCI_STARTSTYLING, endwordpos);
755 Call(SCI_SETSTYLING, 0, 0);
757 break;
758 case SCN_MODIFIED:
760 if (!m_blockModifiedHandler && (lpSCN->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)))
762 LRESULT firstline = Call(SCI_GETFIRSTVISIBLELINE);
763 LRESULT lastline = firstline + Call(SCI_LINESONSCREEN);
764 int firstpos = (int)Call(SCI_POSITIONFROMLINE, firstline);
765 int lastpos = (int)Call(SCI_GETLINEENDPOSITION, lastline);
766 int pos1 = lpSCN->position;
767 int pos2 = pos1 + lpSCN->length;
768 // always use the bigger range
769 firstpos = min(firstpos, pos1);
770 lastpos = max(lastpos, pos2);
772 WrapLines(firstpos, lastpos);
774 break;
776 case SCN_DWELLSTART:
777 case SCN_HOTSPOTCLICK:
779 TEXTRANGEA textrange;
780 textrange.chrg.cpMin = lpSCN->position;
781 textrange.chrg.cpMax = lpSCN->position;
782 DWORD style = GetStyleAt(lpSCN->position);
783 if (style != STYLE_ISSUEBOLDITALIC && style != STYLE_URL)
784 break;
785 while (GetStyleAt(textrange.chrg.cpMin - 1) == style)
786 --textrange.chrg.cpMin;
787 while (GetStyleAt(textrange.chrg.cpMax + 1) == style)
788 ++textrange.chrg.cpMax;
789 ++textrange.chrg.cpMax;
790 auto textbuffer = std::make_unique<char[]>(textrange.chrg.cpMax - textrange.chrg.cpMin + 1);
791 textrange.lpstrText = textbuffer.get();
792 Call(SCI_GETTEXTRANGE, 0, (LPARAM)&textrange);
793 CString url;
794 if (style == STYLE_URL)
796 url = StringFromControl(textbuffer.get());
797 if (url.Find(L'@') > 0 && !PathIsURL(url))
798 url = L"mailto:" + url;
800 else
802 url = m_sUrl;
803 url.Replace(L"%BUGID%", StringFromControl(textbuffer.get()));
805 if (!url.IsEmpty())
807 if (lpnmhdr->code == SCN_HOTSPOTCLICK)
808 ShellExecute(GetParent()->GetSafeHwnd(), L"open", url, nullptr, nullptr, SW_SHOWDEFAULT);
809 else
811 CStringA sTextA = StringForControl(url);
812 Call(SCI_CALLTIPSHOW, lpSCN->position + 3, (LPARAM)(LPCSTR)sTextA);
816 break;
817 case SCN_DWELLEND:
818 Call(SCI_CALLTIPCANCEL);
819 break;
822 return CWnd::OnChildNotify(message, wParam, lParam, pLResult);
825 BEGIN_MESSAGE_MAP(CSciEdit, CWnd)
826 ON_WM_KEYDOWN()
827 ON_WM_CONTEXTMENU()
828 END_MESSAGE_MAP()
830 void CSciEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
832 switch (nChar)
834 case (VK_ESCAPE):
836 if ((Call(SCI_AUTOCACTIVE)==0)&&(Call(SCI_CALLTIPACTIVE)==0))
837 ::SendMessage(GetParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
839 break;
841 CWnd::OnKeyDown(nChar, nRepCnt, nFlags);
844 BOOL CSciEdit::PreTranslateMessage(MSG* pMsg)
846 if (pMsg->message == WM_KEYDOWN)
848 switch (pMsg->wParam)
850 case VK_SPACE:
852 if ((GetKeyState(VK_CONTROL) & 0x8000) && ((GetKeyState(VK_MENU) & 0x8000) == 0))
854 DoAutoCompletion(1);
855 return TRUE;
858 break;
859 case VK_TAB:
860 // The TAB cannot be handled in OnKeyDown because it is too late by then.
862 if ((GetKeyState(VK_CONTROL) & 0x8000) && ((GetKeyState(VK_MENU) & 0x8000) == 0))
864 //Ctrl-Tab was pressed, this means we should provide the user with
865 //a list of possible spell checking alternatives to the word under
866 //the cursor
867 SuggestSpellingAlternatives();
868 return TRUE;
870 else if (!Call(SCI_AUTOCACTIVE))
872 ::PostMessage(GetParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
873 return TRUE;
876 break;
879 return CWnd::PreTranslateMessage(pMsg);
882 void CSciEdit::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
884 int anchor = (int)Call(SCI_GETANCHOR);
885 int currentpos = (int)Call(SCI_GETCURRENTPOS);
886 int selstart = (int)Call(SCI_GETSELECTIONSTART);
887 int selend = (int)Call(SCI_GETSELECTIONEND);
888 int pointpos = 0;
889 if ((point.x == -1) && (point.y == -1))
891 CRect rect;
892 GetClientRect(&rect);
893 ClientToScreen(&rect);
894 point = rect.CenterPoint();
895 pointpos = (int)Call(SCI_GETCURRENTPOS);
897 else
899 // change the cursor position to the point where the user
900 // right-clicked.
901 CPoint clientpoint = point;
902 ScreenToClient(&clientpoint);
903 pointpos = (int)Call(SCI_POSITIONFROMPOINT, clientpoint.x, clientpoint.y);
905 CString sMenuItemText;
906 CMenu popup;
907 bool bRestoreCursor = true;
908 if (popup.CreatePopupMenu())
910 bool bCanUndo = !!Call(SCI_CANUNDO);
911 bool bCanRedo = !!Call(SCI_CANREDO);
912 bool bHasSelection = (selend-selstart > 0);
913 bool bCanPaste = !!Call(SCI_CANPASTE);
914 bool bIsReadOnly = !!Call(SCI_GETREADONLY);
915 UINT uEnabledMenu = MF_STRING | MF_ENABLED;
916 UINT uDisabledMenu = MF_STRING | MF_GRAYED;
918 // find the word under the cursor
919 CString sWord;
920 if (pointpos)
922 // setting the cursor clears the selection
923 Call(SCI_SETANCHOR, pointpos);
924 Call(SCI_SETCURRENTPOS, pointpos);
925 sWord = GetWordUnderCursor();
926 // restore the selection
927 Call(SCI_SETSELECTIONSTART, selstart);
928 Call(SCI_SETSELECTIONEND, selend);
930 else
931 sWord = GetWordUnderCursor();
932 CStringA worda = GetWordForSpellChecker(sWord);
934 int nCorrections = 1;
935 bool bSpellAdded = false;
936 // check if the word under the cursor is spelled wrong
937 if ((pChecker)&&(!worda.IsEmpty()) && !bIsReadOnly)
939 char ** wlst = nullptr;
940 // get the spell suggestions
941 int ns = pChecker->suggest(&wlst,worda);
942 if (ns > 0)
944 // add the suggestions to the context menu
945 for (int i=0; i < ns; i++)
947 bSpellAdded = true;
948 CString sug = GetWordFromSpellChecker(wlst[i]);
949 popup.InsertMenu((UINT)-1, 0, nCorrections++, sug);
950 free(wlst[i]);
952 free(wlst);
954 else
955 free(wlst);
957 // only add a separator if spelling correction suggestions were added
958 if (bSpellAdded)
959 popup.AppendMenu(MF_SEPARATOR);
961 // also allow the user to add the word to the custom dictionary so
962 // it won't show up as misspelled anymore
963 if ((sWord.GetLength()<PDICT_MAX_WORD_LENGTH)&&((pChecker)&&(m_autolist.find(sWord) == m_autolist.end())&&(!pChecker->spell(worda)))&&
964 (!_istdigit(sWord.GetAt(0)))&&(!m_personalDict.FindWord(sWord)) && !bIsReadOnly)
966 sMenuItemText.Format(IDS_SCIEDIT_ADDWORD, (LPCTSTR)sWord);
967 popup.AppendMenu(uEnabledMenu, SCI_ADDWORD, sMenuItemText);
968 // another separator
969 popup.AppendMenu(MF_SEPARATOR);
972 // add the 'default' entries
973 sMenuItemText.LoadString(IDS_SCIEDIT_UNDO);
974 popup.AppendMenu(bCanUndo ? uEnabledMenu : uDisabledMenu, SCI_UNDO, sMenuItemText);
975 sMenuItemText.LoadString(IDS_SCIEDIT_REDO);
976 popup.AppendMenu(bCanRedo ? uEnabledMenu : uDisabledMenu, SCI_REDO, sMenuItemText);
978 popup.AppendMenu(MF_SEPARATOR);
980 sMenuItemText.LoadString(IDS_SCIEDIT_CUT);
981 popup.AppendMenu(!bIsReadOnly && bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_CUT, sMenuItemText);
982 sMenuItemText.LoadString(IDS_SCIEDIT_COPY);
983 popup.AppendMenu(bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_COPY, sMenuItemText);
984 sMenuItemText.LoadString(IDS_SCIEDIT_PASTE);
985 popup.AppendMenu(bCanPaste ? uEnabledMenu : uDisabledMenu, SCI_PASTE, sMenuItemText);
987 popup.AppendMenu(MF_SEPARATOR);
989 sMenuItemText.LoadString(IDS_SCIEDIT_SELECTALL);
990 popup.AppendMenu(uEnabledMenu, SCI_SELECTALL, sMenuItemText);
992 if (!bIsReadOnly && Call(SCI_GETEDGECOLUMN))
994 popup.AppendMenu(MF_SEPARATOR);
996 sMenuItemText.LoadString(IDS_SCIEDIT_SPLITLINES);
997 popup.AppendMenu(bHasSelection ? uEnabledMenu : uDisabledMenu, SCI_LINESSPLIT, sMenuItemText);
1000 if (m_arContextHandlers.GetCount() > 0)
1001 popup.AppendMenu(MF_SEPARATOR);
1003 int nCustoms = nCorrections;
1004 // now add any custom context menus
1005 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
1007 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
1008 pHandler->InsertMenuItems(popup, nCustoms);
1010 #if THESAURUS
1011 // add found thesauri to sub menu's
1012 CMenu thesaurs;
1013 int nThesaurs = 0;
1014 CPtrArray menuArray;
1015 if (thesaurs.CreatePopupMenu())
1017 if ((nCustoms > nCorrections || m_arContextHandlers.IsEmpty()) && !bIsReadOnly)
1018 popup.AppendMenu(MF_SEPARATOR);
1019 if (pThesaur && !worda.IsEmpty() && !bIsReadOnly)
1021 mentry * pmean;
1022 worda.MakeLower();
1023 int count = pThesaur->Lookup(worda, worda.GetLength(),&pmean);
1024 if (count)
1026 mentry * pm = pmean;
1027 for (int i=0; i < count; i++)
1029 CMenu * submenu = new CMenu();
1030 menuArray.Add(submenu);
1031 submenu->CreateMenu();
1032 for (int j=0; j < pm->count; j++)
1034 CString sug = CString(pm->psyns[j]);
1035 submenu->InsertMenu((UINT)-1, 0, nCorrections + nCustoms + (nThesaurs++), sug);
1037 thesaurs.InsertMenu((UINT)-1, MF_POPUP, (UINT_PTR)(submenu->m_hMenu), CString(pm->defn));
1038 pm++;
1041 if ((count > 0)&&(point.x >= 0))
1043 #ifdef IDS_SPELLEDIT_THESAURUS
1044 sMenuItemText.LoadString(IDS_SPELLEDIT_THESAURUS);
1045 popup.InsertMenu((UINT)-1, MF_POPUP, (UINT_PTR)thesaurs.m_hMenu, sMenuItemText);
1046 #else
1047 popup.InsertMenu((UINT)-1, MF_POPUP, (UINT_PTR)thesaurs.m_hMenu, L"Thesaurus");
1048 #endif
1049 nThesaurs = nCustoms;
1051 else
1053 sMenuItemText.LoadString(IDS_SPELLEDIT_NOTHESAURUS);
1054 popup.AppendMenu(MF_DISABLED | MF_GRAYED | MF_STRING, 0, sMenuItemText);
1057 pThesaur->CleanUpAfterLookup(&pmean, count);
1059 else if (!bIsReadOnly)
1061 sMenuItemText.LoadString(IDS_SPELLEDIT_NOTHESAURUS);
1062 popup.AppendMenu(MF_DISABLED | MF_GRAYED | MF_STRING, 0, sMenuItemText);
1065 #endif
1066 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1067 switch (cmd)
1069 case 0:
1070 break; // no command selected
1071 case SCI_SELECTALL:
1072 bRestoreCursor = false;
1073 // fall through
1074 case SCI_UNDO:
1075 case SCI_REDO:
1076 case SCI_CUT:
1077 case SCI_COPY:
1078 case SCI_PASTE:
1079 Call(cmd);
1080 break;
1081 case SCI_ADDWORD:
1082 m_personalDict.AddWord(sWord);
1083 CheckSpelling((int)Call(SCI_POSITIONFROMLINE, Call(SCI_GETFIRSTVISIBLELINE)), (int)Call(SCI_POSITIONFROMLINE, Call(SCI_GETFIRSTVISIBLELINE) + Call(SCI_LINESONSCREEN)));
1084 break;
1085 case SCI_LINESSPLIT:
1087 int marker = (int)(Call(SCI_GETEDGECOLUMN) * Call(SCI_TEXTWIDTH, 0, (LPARAM)" "));
1088 if (marker)
1090 m_blockModifiedHandler = true;
1091 SCOPE_EXIT{ m_blockModifiedHandler = false; };
1092 Call(SCI_TARGETFROMSELECTION);
1093 Call(SCI_LINESJOIN);
1094 Call(SCI_LINESSPLIT, marker);
1097 break;
1098 default:
1099 if (cmd < nCorrections)
1101 Call(SCI_SETANCHOR, pointpos);
1102 Call(SCI_SETCURRENTPOS, pointpos);
1103 GetWordUnderCursor(true);
1104 CString temp;
1105 popup.GetMenuString(cmd, temp, 0);
1106 // setting the cursor clears the selection
1107 Call(SCI_REPLACESEL, 0, (LPARAM)(LPCSTR)StringForControl(temp));
1109 else if (cmd < (nCorrections+nCustoms))
1111 for (INT_PTR handlerindex = 0; handlerindex < m_arContextHandlers.GetCount(); ++handlerindex)
1113 CSciEditContextMenuInterface * pHandler = m_arContextHandlers.GetAt(handlerindex);
1114 if (pHandler->HandleMenuItemClick(cmd, this))
1115 break;
1118 #if THESAURUS
1119 else if (cmd <= (nThesaurs+nCorrections+nCustoms))
1121 Call(SCI_SETANCHOR, pointpos);
1122 Call(SCI_SETCURRENTPOS, pointpos);
1123 GetWordUnderCursor(true);
1124 CString temp;
1125 thesaurs.GetMenuString(cmd, temp, 0);
1126 Call(SCI_REPLACESEL, 0, (LPARAM)(LPCSTR)StringForControl(temp));
1128 #endif
1130 #ifdef THESAURUS
1131 for (INT_PTR index = 0; index < menuArray.GetCount(); ++index)
1133 CMenu * pMenu = (CMenu*)menuArray[index];
1134 delete pMenu;
1136 #endif
1138 if (bRestoreCursor)
1140 // restore the anchor and cursor position
1141 Call(SCI_SETCURRENTPOS, currentpos);
1142 Call(SCI_SETANCHOR, anchor);
1146 bool CSciEdit::StyleEnteredText(int startstylepos, int endstylepos)
1148 bool bStyled = false;
1149 const int line = (int)Call(SCI_LINEFROMPOSITION, startstylepos);
1150 const int line_number_end = (int)Call(SCI_LINEFROMPOSITION, endstylepos);
1151 for (int line_number = line; line_number <= line_number_end; ++line_number)
1153 int offset = (int)Call(SCI_POSITIONFROMLINE, line_number);
1154 int line_len = (int)Call(SCI_LINELENGTH, line_number);
1155 auto linebuffer = std::make_unique<char[]>(line_len + 1);
1156 Call(SCI_GETLINE, line_number, (LPARAM)linebuffer.get());
1157 linebuffer[line_len] = '\0';
1158 int start = 0;
1159 int end = 0;
1160 while (FindStyleChars(linebuffer.get(), '*', start, end))
1162 Call(SCI_STARTSTYLING, start + offset, STYLE_BOLD);
1163 Call(SCI_SETSTYLING, end-start, STYLE_BOLD);
1164 bStyled = true;
1165 start = end;
1167 start = 0;
1168 end = 0;
1169 while (FindStyleChars(linebuffer.get(), '^', start, end))
1171 Call(SCI_STARTSTYLING, start + offset, STYLE_ITALIC);
1172 Call(SCI_SETSTYLING, end-start, STYLE_ITALIC);
1173 bStyled = true;
1174 start = end;
1176 start = 0;
1177 end = 0;
1178 while (FindStyleChars(linebuffer.get(), '_', start, end))
1180 Call(SCI_STARTSTYLING, start + offset, STYLE_UNDERLINED);
1181 Call(SCI_SETSTYLING, end-start, STYLE_UNDERLINED);
1182 bStyled = true;
1183 start = end;
1186 return bStyled;
1189 bool CSciEdit::WrapLines(int startpos, int endpos)
1191 int markerX = (int)(Call(SCI_GETEDGECOLUMN) * Call(SCI_TEXTWIDTH, 0, (LPARAM)" "));
1192 if (markerX)
1194 Call(SCI_SETTARGETSTART, startpos);
1195 Call(SCI_SETTARGETEND, endpos);
1196 Call(SCI_LINESSPLIT, markerX);
1197 return true;
1199 return false;
1202 void CSciEdit::AdvanceUTF8(const char * str, int& pos)
1204 if ((str[pos] & 0xE0)==0xC0)
1206 // utf8 2-byte sequence
1207 pos += 2;
1209 else if ((str[pos] & 0xF0)==0xE0)
1211 // utf8 3-byte sequence
1212 pos += 3;
1214 else if ((str[pos] & 0xF8)==0xF0)
1216 // utf8 4-byte sequence
1217 pos += 4;
1219 else
1220 pos++;
1223 bool CSciEdit::FindStyleChars(const char * line, char styler, int& start, int& end)
1225 int i=0;
1226 int u=0;
1227 while (i < start)
1229 AdvanceUTF8(line, i);
1230 u++;
1233 bool bFoundMarker = false;
1234 CString sULine = CUnicodeUtils::GetUnicode(line);
1235 // find a starting marker
1236 while (line[i] != 0)
1238 if (line[i] == styler)
1240 if ((line[i+1]!=0)&&(IsCharAlphaNumeric(sULine[u+1]))&&
1241 (((u>0)&&(!IsCharAlphaNumeric(sULine[u-1]))) || (u==0)))
1243 start = i+1;
1244 AdvanceUTF8(line, i);
1245 u++;
1246 bFoundMarker = true;
1247 break;
1250 AdvanceUTF8(line, i);
1251 u++;
1253 if (!bFoundMarker)
1254 return false;
1255 // find ending marker
1256 bFoundMarker = false;
1257 while (line[i])
1259 if (line[i] == styler)
1261 if ((IsCharAlphaNumeric(sULine[u-1]))&&
1262 ((((u+1)<sULine.GetLength())&&(!IsCharAlphaNumeric(sULine[u+1]))) || ((u+1) == sULine.GetLength()))
1265 end = i;
1266 i++;
1267 bFoundMarker = true;
1268 break;
1271 AdvanceUTF8(line, i);
1272 u++;
1274 return bFoundMarker;
1277 BOOL CSciEdit::MarkEnteredBugID(int startstylepos, int endstylepos)
1279 if (m_sCommand.IsEmpty())
1280 return FALSE;
1281 // get the text between the start and end position we have to style
1282 const int line_number = (int)Call(SCI_LINEFROMPOSITION, startstylepos);
1283 int start_pos = (int)Call(SCI_POSITIONFROMLINE, (WPARAM)line_number);
1284 int end_pos = endstylepos;
1286 if (start_pos == end_pos)
1287 return FALSE;
1288 if (start_pos > end_pos)
1290 int switchtemp = start_pos;
1291 start_pos = end_pos;
1292 end_pos = switchtemp;
1295 auto textbuffer = std::make_unique<char[]>(end_pos - start_pos + 2);
1296 TEXTRANGEA textrange;
1297 textrange.lpstrText = textbuffer.get();
1298 textrange.chrg.cpMin = start_pos;
1299 textrange.chrg.cpMax = end_pos;
1300 Call(SCI_GETTEXTRANGE, 0, (LPARAM)&textrange);
1301 CStringA msg = CStringA(textbuffer.get());
1303 Call(SCI_STARTSTYLING, start_pos, STYLE_MASK);
1307 if (!m_sBugID.IsEmpty())
1309 // match with two regex strings (without grouping!)
1310 const std::tr1::regex regCheck(m_sCommand);
1311 const std::tr1::regex regBugID(m_sBugID);
1312 const std::tr1::sregex_iterator end;
1313 std::string s = msg;
1314 LONG pos = 0;
1315 // note:
1316 // if start_pos is 0, we're styling from the beginning and let the ^ char match the beginning of the line
1317 // that way, the ^ matches the very beginning of the log message and not the beginning of further lines.
1318 // problem is: this only works *while* entering log messages. If a log message is pasted in whole or
1319 // multiple lines are pasted, start_pos can be 0 and styling goes over multiple lines. In that case, those
1320 // additional line starts also match ^
1321 for (std::tr1::sregex_iterator it(s.cbegin(), s.cend(), regCheck, start_pos != 0 ? std::tr1::regex_constants::match_not_bol : std::tr1::regex_constants::match_default); it != end; ++it)
1323 // clear the styles up to the match position
1324 Call(SCI_SETSTYLING, it->position(0)-pos, STYLE_DEFAULT);
1326 // (*it)[0] is the matched string
1327 std::string matchedString = (*it)[0];
1328 LONG matchedpos = 0;
1329 for (std::tr1::sregex_iterator it2(matchedString.cbegin(), matchedString.cend(), regBugID); it2 != end; ++it2)
1331 ATLTRACE("matched id : %s\n", std::string((*it2)[0]).c_str());
1333 // bold style up to the id match
1334 ATLTRACE("position = %ld\n", it2->position(0));
1335 if (it2->position(0))
1336 Call(SCI_SETSTYLING, it2->position(0) - matchedpos, STYLE_ISSUEBOLD);
1337 // bold and recursive style for the bug ID itself
1338 if ((*it2)[0].str().size())
1339 Call(SCI_SETSTYLING, (*it2)[0].str().size(), STYLE_ISSUEBOLDITALIC);
1340 matchedpos = (LONG)(it2->position(0) + (*it2)[0].str().size());
1342 if ((matchedpos)&&(matchedpos < (LONG)matchedString.size()))
1344 Call(SCI_SETSTYLING, matchedString.size() - matchedpos, STYLE_ISSUEBOLD);
1346 pos = (LONG)(it->position(0) + matchedString.size());
1348 // bold style for the rest of the string which isn't matched
1349 if (s.size()-pos)
1350 Call(SCI_SETSTYLING, s.size()-pos, STYLE_DEFAULT);
1352 else
1354 const std::tr1::regex regCheck(m_sCommand);
1355 const std::tr1::sregex_iterator end;
1356 std::string s = msg;
1357 LONG pos = 0;
1358 for (std::tr1::sregex_iterator it(s.cbegin(), s.cend(), regCheck); it != end; ++it)
1360 // clear the styles up to the match position
1361 if (it->position(0) - pos >= 0)
1362 Call(SCI_SETSTYLING, it->position(0) - pos, STYLE_DEFAULT);
1363 pos = (LONG)it->position(0);
1365 const std::tr1::smatch match = *it;
1366 // we define group 1 as the whole issue text and
1367 // group 2 as the bug ID
1368 if (match.size() >= 2)
1370 ATLTRACE("matched id : %s\n", std::string(match[1]).c_str());
1371 if (match[1].first - s.cbegin() - pos >= 0)
1372 Call(SCI_SETSTYLING, match[1].first - s.cbegin() - pos, STYLE_ISSUEBOLD);
1373 Call(SCI_SETSTYLING, std::string(match[1]).size(), STYLE_ISSUEBOLDITALIC);
1374 pos = (LONG)(match[1].second - s.cbegin());
1379 catch (std::exception&) {}
1381 return FALSE;
1384 //similar code in AppUtils.cpp
1385 bool CSciEdit::IsValidURLChar(unsigned char ch)
1387 return isalnum(ch) ||
1388 ch == '_' || ch == '/' || ch == ';' || ch == '?' || ch == '&' || ch == '=' ||
1389 ch == '%' || ch == ':' || ch == '.' || ch == '#' || ch == '-' || ch == '+' ||
1390 ch == '|' || ch == '>' || ch == '<' || ch == '!' || ch == '@';
1393 //similar code in AppUtils.cpp
1394 void CSciEdit::StyleURLs(int startstylepos, int endstylepos)
1396 const int line_number = (int)Call(SCI_LINEFROMPOSITION, startstylepos);
1397 startstylepos = (int)Call(SCI_POSITIONFROMLINE, (WPARAM)line_number);
1399 int len = endstylepos - startstylepos + 1;
1400 auto textbuffer = std::make_unique<char[]>(len + 1);
1401 TEXTRANGEA textrange;
1402 textrange.lpstrText = textbuffer.get();
1403 textrange.chrg.cpMin = startstylepos;
1404 textrange.chrg.cpMax = endstylepos;
1405 Call(SCI_GETTEXTRANGE, 0, (LPARAM)&textrange);
1406 // we're dealing with utf8 encoded text here, which means one glyph is
1407 // not necessarily one byte/wchar_t
1408 // that's why we use CStringA to still get a correct char index
1409 CStringA msg = textbuffer.get();
1411 int starturl = -1;
1412 for (int i = 0; i <= msg.GetLength(); AdvanceUTF8(msg, i))
1414 if ((i < len) && IsValidURLChar(msg[i]))
1416 if (starturl < 0)
1417 starturl = i;
1419 else
1421 if (starturl >= 0)
1423 bool strip = true;
1424 if (msg[starturl] == '<' && i < len) // try to detect and do not strip URLs put within <>
1426 while (starturl <= i && msg[starturl] == '<') // strip leading '<'
1427 ++starturl;
1428 strip = false;
1429 i = starturl;
1430 while (i < len && msg[i] != '\r' && msg[i] != '\n' && msg[i] != '>') // find first '>' or new line after resetting i to start position
1431 AdvanceUTF8(msg, i);
1434 int skipTrailing = 0;
1435 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] == '!'))
1436 ++skipTrailing;
1438 if (!IsUrlOrEmail(msg.Mid(starturl, i - starturl - skipTrailing)))
1440 starturl = -1;
1441 continue;
1444 ASSERT(startstylepos + i - skipTrailing <= endstylepos);
1445 Call(SCI_STARTSTYLING, startstylepos + starturl, STYLE_URL);
1446 Call(SCI_SETSTYLING, i - starturl - skipTrailing, STYLE_URL);
1448 starturl = -1;
1453 bool CSciEdit::IsUrlOrEmail(const CStringA& sText)
1455 if (!PathIsURLA(sText))
1457 auto atpos = sText.Find('@');
1458 if (atpos <= 0)
1459 return false;
1460 if (sText.Find('.', atpos) <= atpos + 1) // a dot must follow after the @, but not directly after it
1461 return false;
1462 if (sText.Find(':', atpos) < 0) // do not detect git@example.com:something as an email address
1463 return true;
1464 return false;
1466 for (const CStringA& prefix : { "http://", "https://", "git://", "ftp://", "file://", "mailto:" })
1468 if (strncmp(sText, prefix, prefix.GetLength()) == 0 && sText.GetLength() != prefix.GetLength())
1469 return true;
1471 return false;
1474 CStringA CSciEdit::GetWordForSpellChecker(const CString& sWord)
1476 // convert the string from the control to the encoding of the spell checker module.
1477 CStringA sWordA;
1478 if (m_spellcodepage)
1480 char * buf = sWordA.GetBuffer(sWord.GetLength() * 4 + 1);
1481 int lengthIncTerminator = WideCharToMultiByte(m_spellcodepage, 0, sWord, -1, buf, sWord.GetLength() * 4, nullptr, nullptr);
1482 if (lengthIncTerminator == 0)
1483 return ""; // converting to the codepage failed
1484 sWordA.ReleaseBuffer(lengthIncTerminator - 1);
1486 else
1487 sWordA = CStringA(sWord);
1489 sWordA.Trim("\'\".,");
1491 return sWordA;
1494 CString CSciEdit::GetWordFromSpellChecker(const CStringA& sWordA)
1496 CString sWord;
1497 if (m_spellcodepage)
1499 wchar_t * buf = sWord.GetBuffer(sWordA.GetLength() * 2);
1500 int lengthIncTerminator = MultiByteToWideChar(m_spellcodepage, 0, sWordA, -1, buf, sWordA.GetLength() * 2);
1501 if (lengthIncTerminator == 0)
1502 return L"";
1503 sWord.ReleaseBuffer(lengthIncTerminator - 1);
1505 else
1506 sWord = CString(sWordA);
1508 sWord.Trim(L"\'\".,");
1510 return sWord;
1513 bool CSciEdit::IsUTF8(LPVOID pBuffer, size_t cb)
1515 if (cb < 2)
1516 return true;
1517 UINT16 * pVal = (UINT16 *)pBuffer;
1518 UINT8 * pVal2 = (UINT8 *)(pVal+1);
1519 // scan the whole buffer for a 0x0000 sequence
1520 // if found, we assume a binary file
1521 for (size_t i=0; i<(cb-2); i=i+2)
1523 if (0x0000 == *pVal++)
1524 return false;
1526 pVal = (UINT16 *)pBuffer;
1527 if (*pVal == 0xFEFF)
1528 return false;
1529 if (cb < 3)
1530 return false;
1531 if (*pVal == 0xBBEF)
1533 if (*pVal2 == 0xBF)
1534 return true;
1536 // check for illegal UTF8 chars
1537 pVal2 = (UINT8 *)pBuffer;
1538 for (size_t i=0; i<cb; ++i)
1540 if ((*pVal2 == 0xC0)||(*pVal2 == 0xC1)||(*pVal2 >= 0xF5))
1541 return false;
1542 pVal2++;
1544 pVal2 = (UINT8 *)pBuffer;
1545 bool bUTF8 = false;
1546 for (size_t i=0; i<(cb-3); ++i)
1548 if ((*pVal2 & 0xE0)==0xC0)
1550 pVal2++;i++;
1551 if ((*pVal2 & 0xC0)!=0x80)
1552 return false;
1553 bUTF8 = true;
1555 if ((*pVal2 & 0xF0)==0xE0)
1557 pVal2++;i++;
1558 if ((*pVal2 & 0xC0)!=0x80)
1559 return false;
1560 pVal2++;i++;
1561 if ((*pVal2 & 0xC0)!=0x80)
1562 return false;
1563 bUTF8 = true;
1565 if ((*pVal2 & 0xF8)==0xF0)
1567 pVal2++;i++;
1568 if ((*pVal2 & 0xC0)!=0x80)
1569 return false;
1570 pVal2++;i++;
1571 if ((*pVal2 & 0xC0)!=0x80)
1572 return false;
1573 pVal2++;i++;
1574 if ((*pVal2 & 0xC0)!=0x80)
1575 return false;
1576 bUTF8 = true;
1578 pVal2++;
1580 if (bUTF8)
1581 return true;
1582 return false;
1585 void CSciEdit::SetAStyle(int style, COLORREF fore, COLORREF back, int size, const char *face)
1587 Call(SCI_STYLESETFORE, style, fore);
1588 Call(SCI_STYLESETBACK, style, back);
1589 if (size >= 1)
1590 Call(SCI_STYLESETSIZE, style, size);
1591 if (face)
1592 Call(SCI_STYLESETFONT, style, reinterpret_cast<LPARAM>(face));
1595 void CSciEdit::SetUDiffStyle()
1597 m_bDoStyle = false;
1598 SetAStyle(STYLE_DEFAULT, ::GetSysColor(COLOR_WINDOWTEXT), ::GetSysColor(COLOR_WINDOW),
1599 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffFontSize", 10),
1600 CUnicodeUtils::StdGetUTF8(CRegStdString(L"Software\\TortoiseGit\\UDiffFontName", L"Consolas")).c_str());
1601 Call(SCI_SETTABWIDTH, CRegStdDWORD(L"Software\\TortoiseGit\\UDiffTabSize", 4));
1603 Call(SCI_SETREADONLY, TRUE);
1604 //LRESULT pix = Call(SCI_TEXTWIDTH, STYLE_LINENUMBER, (LPARAM)"_99999");
1605 //Call(SCI_SETMARGINWIDTHN, 0, pix);
1606 //Call(SCI_SETMARGINWIDTHN, 1);
1607 //Call(SCI_SETMARGINWIDTHN, 2);
1608 //Set the default windows colors for edit controls
1609 SetColors(false);
1611 //SendEditor(SCI_SETREADONLY, FALSE);
1612 Call(SCI_CLEARALL);
1613 Call(EM_EMPTYUNDOBUFFER);
1614 Call(SCI_SETSAVEPOINT);
1615 Call(SCI_CANCEL);
1616 Call(SCI_SETUNDOCOLLECTION, 0);
1618 Call(SCI_SETUNDOCOLLECTION, 1);
1619 Call(SCI_SETWRAPMODE,SC_WRAP_NONE);
1621 //::SetFocus(m_hWndEdit);
1622 Call(EM_EMPTYUNDOBUFFER);
1623 Call(SCI_SETSAVEPOINT);
1624 Call(SCI_GOTOPOS, 0);
1626 Call(SCI_CLEARDOCUMENTSTYLE, 0, 0);
1627 Call(SCI_SETSTYLEBITS, 5, 0);
1629 HIGHCONTRAST highContrast = { 0 };
1630 highContrast.cbSize = sizeof(HIGHCONTRAST);
1631 if (SystemParametersInfo(SPI_GETHIGHCONTRAST, 0, &highContrast, 0) == TRUE && (highContrast.dwFlags & HCF_HIGHCONTRASTON))
1633 Call(SCI_SETLEXER, SCLEX_NULL);
1634 return;
1637 //SetAStyle(SCE_DIFF_DEFAULT, RGB(0, 0, 0));
1638 SetAStyle(SCE_DIFF_COMMAND,
1639 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeCommandColor", UDIFF_COLORFORECOMMAND),
1640 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackCommandColor", UDIFF_COLORBACKCOMMAND));
1641 SetAStyle(SCE_DIFF_POSITION,
1642 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForePositionColor", UDIFF_COLORFOREPOSITION),
1643 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackPositionColor", UDIFF_COLORBACKPOSITION));
1644 SetAStyle(SCE_DIFF_HEADER,
1645 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeHeaderColor", UDIFF_COLORFOREHEADER),
1646 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackHeaderColor", UDIFF_COLORBACKHEADER));
1647 SetAStyle(SCE_DIFF_COMMENT,
1648 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeCommentColor", UDIFF_COLORFORECOMMENT),
1649 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackCommentColor", UDIFF_COLORBACKCOMMENT));
1650 Call(SCI_STYLESETBOLD, SCE_DIFF_COMMENT, TRUE);
1651 SetAStyle(SCE_DIFF_ADDED,
1652 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeAddedColor", UDIFF_COLORFOREADDED),
1653 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackAddedColor", UDIFF_COLORBACKADDED));
1654 SetAStyle(SCE_DIFF_DELETED,
1655 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffForeRemovedColor", UDIFF_COLORFOREREMOVED),
1656 CRegStdDWORD(L"Software\\TortoiseGit\\UDiffBackRemovedColor", UDIFF_COLORBACKREMOVED));
1658 Call(SCI_SETLEXER, SCLEX_DIFF);
1659 Call(SCI_SETKEYWORDS, 0, (LPARAM)"revision");
1660 Call(SCI_COLOURISE, 0, -1);
1663 int CSciEdit::LoadFromFile(CString &filename)
1665 CAutoFILE fp = _wfsopen(filename, L"rb", _SH_DENYWR);
1666 if (!fp)
1667 return -1;
1669 char data[4096] = { 0 };
1670 size_t lenFile = fread(data, 1, sizeof(data), fp);
1671 bool bUTF8 = IsUTF8(data, lenFile);
1672 while (lenFile > 0)
1674 Call(SCI_ADDTEXT, lenFile, reinterpret_cast<LPARAM>(static_cast<char *>(data)));
1675 lenFile = fread(data, 1, sizeof(data), fp);
1677 Call(SCI_SETCODEPAGE, bUTF8 ? SC_CP_UTF8 : GetACP());
1678 return 0;
1681 void CSciEdit::RestyleBugIDs()
1683 int endstylepos = (int)Call(SCI_GETLENGTH);
1684 // clear all styles
1685 Call(SCI_STARTSTYLING, 0, STYLE_MASK);
1686 Call(SCI_SETSTYLING, endstylepos, STYLE_DEFAULT);
1687 // style the bug IDs
1688 MarkEnteredBugID(0, endstylepos);