From 38d7ba6eff6757b83b54b0485b3452a41de25470 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Alex=20Villac=C3=ADs=20Lasso?= Date: Wed, 18 Jun 2008 10:54:17 -0500 Subject: [PATCH] richedit: WM_SETTEXT must immediately autodetect URLs, without waiting for a WM_CHAR. --- dlls/riched20/editor.c | 178 +++++++++++++++++++++++++- dlls/riched20/editor.h | 3 + dlls/riched20/tests/editor.c | 239 +++++++++++++++++++++++++++++++++-- 3 files changed, 410 insertions(+), 10 deletions(-) diff --git a/dlls/riched20/editor.c b/dlls/riched20/editor.c index f72f24371c..edd6be6da8 100644 --- a/dlls/riched20/editor.c +++ b/dlls/riched20/editor.c @@ -2454,6 +2454,22 @@ static LRESULT RichEditWndProc_common(HWND hWnd, UINT msg, WPARAM wParam, } else TRACE("WM_SETTEXT - NULL\n"); + if (editor->AutoURLDetect_bEnable) + { + int cMin = 0, cMax = -1; + while (ME_FindNextURLCandidate(editor, cMin, cMax, &cMin, &cMax)) + { + if (ME_IsCandidateAnURL(editor, cMin, cMax)) { + CHARFORMAT2W link; + link.cbSize = sizeof(link); + link.dwMask = CFM_LINK; + link.dwEffects = CFE_LINK; + ME_SetCharFormat(editor, cMin, cMax - cMin, &link); + } + cMin = cMax; + cMax = -1; + } + } ME_SetSelection(editor, 0, 0); editor->nModifyStep = 0; ME_CommitUndo(editor); @@ -3449,9 +3465,11 @@ int ME_GetTextW(ME_TextEditor *editor, WCHAR *buffer, int nStart, int nChars, in CopyMemory(buffer, item->member.run.strText->szData + nStart, sizeof(WCHAR)*nLen); nChars -= nLen; nWritten += nLen; - if (!nChars) - return nWritten; buffer += nLen; + if (!nChars) { + *buffer = 0; + return nWritten; + } nStart = 0; item = ME_FindItemFwd(item, diRun); } @@ -3713,3 +3731,159 @@ int ME_AutoURLDetect(ME_TextEditor *editor, WCHAR curChar) } return 0; } + + +static BOOL isurlspecial(WCHAR c) +{ + static const WCHAR special_chars[] = {'.','/','%','@','*','|','\\','+','#',0}; + return strchrW( special_chars, c ) != NULL; +} + +/** + * This proc takes a selection, and scans it forward in order to select the span + * of a possible URL candidate. A possible URL candidate must start with isalnum + * or one of the following special characters: *|/\+%#@ and must consist entirely + * of the characters allowed to start the URL, plus : (colon) which may occur + * at most once, and not at either end. + * + * sel_max == -1 indicates scan to end of text. + */ +BOOL ME_FindNextURLCandidate(ME_TextEditor *editor, int sel_min, int sel_max, + int * candidate_min, int * candidate_max) +{ + ME_DisplayItem * item; + ME_DisplayItem * para; + int nStart; + BOOL foundColon = FALSE; + WCHAR lastAcceptedChar = '\0'; + + TRACE("sel_min = %d sel_max = %d\n", sel_min, sel_max); + + *candidate_min = *candidate_max = -1; + item = ME_FindItemAtOffset(editor, diRun, sel_min, &nStart); + if (!item) return FALSE; + TRACE("nStart = %d\n", nStart); + para = ME_GetParagraph(item); + if (sel_max == -1) sel_max = ME_GetTextLength(editor); + while (item && para->member.para.nCharOfs + item->member.run.nCharOfs + nStart < sel_max) + { + ME_DisplayItem * next_item; + + if (!(item->member.run.nFlags & MERF_ENDPARA)) { + /* Find start of candidate */ + if (*candidate_min == -1) { + while (nStart < ME_StrLen(item->member.run.strText) && + !(isalnumW(item->member.run.strText->szData[nStart]) || + isurlspecial(item->member.run.strText->szData[nStart]))) { + nStart++; + } + if (nStart < ME_StrLen(item->member.run.strText) && + (isalnumW(item->member.run.strText->szData[nStart]) || + isurlspecial(item->member.run.strText->szData[nStart]))) { + *candidate_min = para->member.para.nCharOfs + item->member.run.nCharOfs + nStart; + lastAcceptedChar = item->member.run.strText->szData[nStart]; + nStart++; + } + } + + /* Find end of candidate */ + if (*candidate_min >= 0) { + while (nStart < ME_StrLen(item->member.run.strText) && + (isalnumW(item->member.run.strText->szData[nStart]) || + isurlspecial(item->member.run.strText->szData[nStart]) || + (!foundColon && item->member.run.strText->szData[nStart] == ':') )) { + if (item->member.run.strText->szData[nStart] == ':') foundColon = TRUE; + lastAcceptedChar = item->member.run.strText->szData[nStart]; + nStart++; + } + if (nStart < ME_StrLen(item->member.run.strText) && + !(isalnumW(item->member.run.strText->szData[nStart]) || + isurlspecial(item->member.run.strText->szData[nStart]) )) { + *candidate_max = para->member.para.nCharOfs + item->member.run.nCharOfs + nStart; + nStart++; + if (lastAcceptedChar == ':') (*candidate_max)--; + return TRUE; + } + } + } else { + /* End of paragraph: skip it if before candidate span, or terminates + current active span */ + if (*candidate_min >= 0) { + *candidate_max = para->member.para.nCharOfs + item->member.run.nCharOfs; + if (lastAcceptedChar == ':') (*candidate_max)--; + return TRUE; + } + } + + /* Reaching this point means no span was found, so get next span */ + next_item = ME_FindItemFwd(item, diRun); + if (!next_item) { + if (*candidate_min >= 0) { + /* There are no further runs, so take end of text as end of candidate */ + *candidate_max = para->member.para.nCharOfs + item->member.run.nCharOfs + nStart; + if (lastAcceptedChar == ':') (*candidate_max)--; + return TRUE; + } + } + item = next_item; + para = ME_GetParagraph(item); + nStart = 0; + } + + if (item) { + if (*candidate_min >= 0) { + /* There are no further runs, so take end of text as end of candidate */ + *candidate_max = para->member.para.nCharOfs + item->member.run.nCharOfs + nStart; + if (lastAcceptedChar == ':') (*candidate_max)--; + return TRUE; + } + } + return FALSE; +} + +/** + * This proc evaluates the selection and returns TRUE if it can be considered an URL + */ +BOOL ME_IsCandidateAnURL(ME_TextEditor *editor, int sel_min, int sel_max) +{ + struct prefix_s { + const char *text; + int length; + } prefixes[12] = { + /* Code below depends on these being in decreasing length order! */ + {"prospero:", 10}, + {"telnet:", 8}, + {"gopher:", 8}, + {"mailto:", 8}, + {"https:", 7}, + {"file:", 6}, + {"news:", 6}, + {"wais:", 6}, + {"nntp:", 6}, + {"http:", 5}, + {"www.", 5}, + {"ftp:", 5}, + }; + LPWSTR bufferW = NULL; + WCHAR bufW[32]; + int i; + + if (sel_max == -1) sel_max = ME_GetTextLength(editor); + assert(sel_min <= sel_max); + for (i = 0; i < sizeof(prefixes) / sizeof(struct prefix_s); i++) + { + if (sel_max - sel_min < prefixes[i].length) continue; + if (bufferW == NULL) { + bufferW = (LPWSTR)heap_alloc((sel_max - sel_min + 1) * sizeof(WCHAR)); + } + ME_GetTextW(editor, bufferW, sel_min, min(sel_max - sel_min, strlen(prefixes[i].text)), 0); + MultiByteToWideChar(CP_ACP, 0, prefixes[i].text, -1, bufW, 32); + if (!lstrcmpW(bufW, bufferW)) + { + heap_free(bufferW); + return TRUE; + } + } + if (bufferW != NULL) heap_free(bufferW); + return FALSE; +} diff --git a/dlls/riched20/editor.h b/dlls/riched20/editor.h index aa28060f64..8bc819353f 100644 --- a/dlls/riched20/editor.h +++ b/dlls/riched20/editor.h @@ -279,6 +279,9 @@ void ME_StreamInFill(ME_InStream *stream); int ME_AutoURLDetect(ME_TextEditor *editor, WCHAR curChar); extern int me_debug; extern void DoWrap(ME_TextEditor *editor); +extern BOOL ME_FindNextURLCandidate(ME_TextEditor *editor, int sel_min, int sel_max, + int * candidate_min, int * candidate_max); +extern BOOL ME_IsCandidateAnURL(ME_TextEditor *editor, int sel_min, int sel_max); /* undo.c */ ME_UndoItem *ME_AddUndoItem(ME_TextEditor *editor, ME_DIType type, const ME_DisplayItem *pdi); diff --git a/dlls/riched20/tests/editor.c b/dlls/riched20/tests/editor.c index a1d11276cf..69951b8dc7 100644 --- a/dlls/riched20/tests/editor.c +++ b/dlls/riched20/tests/editor.c @@ -929,14 +929,20 @@ static void test_EM_SETOPTIONS(void) DestroyWindow(hwndRichEdit); } -static void check_CFE_LINK_rcvd(HWND hwnd, int is_url, const char * url) +static int check_CFE_LINK_selection(HWND hwnd, int sel_start, int sel_end) { CHARFORMAT2W text_format; - int link_present = 0; text_format.cbSize = sizeof(text_format); - SendMessage(hwnd, EM_SETSEL, 0, 1); + SendMessage(hwnd, EM_SETSEL, sel_start, sel_end); SendMessage(hwnd, EM_GETCHARFORMAT, SCF_SELECTION, (LPARAM) &text_format); - link_present = text_format.dwEffects & CFE_LINK; + return (text_format.dwEffects & CFE_LINK) ? 1 : 0; +} + +static void check_CFE_LINK_rcvd(HWND hwnd, int is_url, const char * url) +{ + int link_present = 0; + + link_present = check_CFE_LINK_selection(hwnd, 0, 1); if (is_url) { /* control text is url; should get CFE_LINK */ ok(0 != link_present, "URL Case: CFE_LINK not set for [%s].\n", url); @@ -971,10 +977,70 @@ static void test_EM_AUTOURLDETECT(void) {"wais:waisserver", 1} }; - int i; + int i, j; int urlRet=-1; HWND hwndRichEdit, parent; + /* All of the following should cause the URL to be detected */ + const char * templates_delim[] = { + "This is some text with X on it", + "This is some text with (X) on it", + "This is some text with X\r on it", + "This is some text with ---X--- on it", + "This is some text with \"X\" on it", + "This is some text with 'X' on it", + "This is some text with 'X' on it", + "This is some text with :X: on it", + + "This text ends with X", + + "This is some text with X) on it", + "This is some text with X--- on it", + "This is some text with X\" on it", + "This is some text with X' on it", + "This is some text with X: on it", + + "This is some text with (X on it", + "This is some text with \rX on it", + "This is some text with ---X on it", + "This is some text with \"X on it", + "This is some text with 'X on it", + "This is some text with :X on it", + }; + /* None of these should cause the URL to be detected */ + const char * templates_non_delim[] = { + "This is some text with |X| on it", + "This is some text with *X* on it", + "This is some text with /X/ on it", + "This is some text with +X+ on it", + "This is some text with %X% on it", + "This is some text with #X# on it", + "This is some text with @X@ on it", + "This is some text with \\X\\ on it", + "This is some text with |X on it", + "This is some text with *X on it", + "This is some text with /X on it", + "This is some text with +X on it", + "This is some text with %X on it", + "This is some text with #X on it", + "This is some text with @X on it", + "This is some text with \\X on it", + }; + /* All of these cause the URL detection to be extended by one more byte, + thus demonstrating that the tested character is considered as part + of the URL. */ + const char * templates_xten_delim[] = { + "This is some text with X| on it", + "This is some text with X* on it", + "This is some text with X/ on it", + "This is some text with X+ on it", + "This is some text with X% on it", + "This is some text with X# on it", + "This is some text with X@ on it", + "This is some text with X\\ on it", + }; + char buffer[1024]; + parent = new_static_wnd(NULL); hwndRichEdit = new_richedit(parent); /* Try and pass EM_AUTOURLDETECT some test wParam values */ @@ -989,16 +1055,173 @@ static void test_EM_AUTOURLDETECT(void) ok(urlRet==E_INVALIDARG, "Bad wParam2: urlRet is: %d\n", urlRet); /* for each url, check the text to see if CFE_LINK effect is present */ for (i = 0; i < sizeof(urls)/sizeof(struct urls_s); i++) { + SendMessage(hwndRichEdit, EM_AUTOURLDETECT, FALSE, 0); SendMessage(hwndRichEdit, WM_SETTEXT, 0, (LPARAM) urls[i].text); - SendMessage(hwndRichEdit, WM_CHAR, 0, 0); check_CFE_LINK_rcvd(hwndRichEdit, 0, urls[i].text); + + /* Link detection should happen immediately upon WM_SETTEXT */ SendMessage(hwndRichEdit, EM_AUTOURLDETECT, TRUE, 0); SendMessage(hwndRichEdit, WM_SETTEXT, 0, (LPARAM) urls[i].text); - SendMessage(hwndRichEdit, WM_CHAR, 0, 0); check_CFE_LINK_rcvd(hwndRichEdit, urls[i].is_url, urls[i].text); } DestroyWindow(hwndRichEdit); + + /* Test detection of URLs within normal text - WM_SETTEXT case. */ + for (i = 0; i < sizeof(urls)/sizeof(struct urls_s); i++) { + hwndRichEdit = new_richedit(parent); + + for (j = 0; j < sizeof(templates_delim) / sizeof(const char *); j++) { + char * at_pos; + int at_offset; + int end_offset; + + at_pos = strchr(templates_delim[j], 'X'); + at_offset = at_pos - templates_delim[j]; + strncpy(buffer, templates_delim[j], at_offset); + buffer[at_offset] = '\0'; + strcat(buffer, urls[i].text); + strcat(buffer, templates_delim[j] + at_offset + 1); + end_offset = at_offset + strlen(urls[i].text); + + SendMessage(hwndRichEdit, EM_AUTOURLDETECT, TRUE, 0); + SendMessage(hwndRichEdit, WM_SETTEXT, 0, (LPARAM) buffer); + + /* This assumes no templates start with the URL itself, and that they + have at least two characters before the URL text */ + ok(!check_CFE_LINK_selection(hwndRichEdit, 0, 1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", 0, 1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset -2, at_offset -1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset -2, at_offset -1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset -1, at_offset), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset -1, at_offset, buffer); + + if (urls[i].is_url) + { + ok(check_CFE_LINK_selection(hwndRichEdit, at_offset, at_offset +1), + "CFE_LINK not set in (%d-%d), text: %s\n", at_offset, at_offset +1, buffer); + ok(check_CFE_LINK_selection(hwndRichEdit, end_offset -1, end_offset), + "CFE_LINK not set in (%d-%d), text: %s\n", end_offset -1, end_offset, buffer); + } + else + { + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset, at_offset +1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset, at_offset + 1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset -1, end_offset), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset -1, end_offset, buffer); + } + if (buffer[end_offset] != '\0') + { + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset, end_offset +1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset, end_offset + 1, buffer); + if (buffer[end_offset +1] != '\0') + { + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset +1, end_offset +2), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset +1, end_offset +2, buffer); + } + } + } + + for (j = 0; j < sizeof(templates_non_delim) / sizeof(const char *); j++) { + char * at_pos; + int at_offset; + int end_offset; + + at_pos = strchr(templates_non_delim[j], 'X'); + at_offset = at_pos - templates_non_delim[j]; + strncpy(buffer, templates_non_delim[j], at_offset); + buffer[at_offset] = '\0'; + strcat(buffer, urls[i].text); + strcat(buffer, templates_non_delim[j] + at_offset + 1); + end_offset = at_offset + strlen(urls[i].text); + + SendMessage(hwndRichEdit, EM_AUTOURLDETECT, TRUE, 0); + SendMessage(hwndRichEdit, WM_SETTEXT, 0, (LPARAM) buffer); + + /* This assumes no templates start with the URL itself, and that they + have at least two characters before the URL text */ + ok(!check_CFE_LINK_selection(hwndRichEdit, 0, 1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", 0, 1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset -2, at_offset -1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset -2, at_offset -1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset -1, at_offset), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset -1, at_offset, buffer); + + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset, at_offset +1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset, at_offset + 1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset -1, end_offset), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset -1, end_offset, buffer); + if (buffer[end_offset] != '\0') + { + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset, end_offset +1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset, end_offset + 1, buffer); + if (buffer[end_offset +1] != '\0') + { + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset +1, end_offset +2), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset +1, end_offset +2, buffer); + } + } + } + + for (j = 0; j < sizeof(templates_xten_delim) / sizeof(const char *); j++) { + char * at_pos; + int at_offset; + int end_offset; + + at_pos = strchr(templates_xten_delim[j], 'X'); + at_offset = at_pos - templates_xten_delim[j]; + strncpy(buffer, templates_xten_delim[j], at_offset); + buffer[at_offset] = '\0'; + strcat(buffer, urls[i].text); + strcat(buffer, templates_xten_delim[j] + at_offset + 1); + end_offset = at_offset + strlen(urls[i].text); + + SendMessage(hwndRichEdit, EM_AUTOURLDETECT, TRUE, 0); + SendMessage(hwndRichEdit, WM_SETTEXT, 0, (LPARAM) buffer); + + /* This assumes no templates start with the URL itself, and that they + have at least two characters before the URL text */ + ok(!check_CFE_LINK_selection(hwndRichEdit, 0, 1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", 0, 1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset -2, at_offset -1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset -2, at_offset -1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset -1, at_offset), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset -1, at_offset, buffer); + + if (urls[i].is_url) + { + ok(check_CFE_LINK_selection(hwndRichEdit, at_offset, at_offset +1), + "CFE_LINK not set in (%d-%d), text: %s\n", at_offset, at_offset +1, buffer); + ok(check_CFE_LINK_selection(hwndRichEdit, end_offset -1, end_offset), + "CFE_LINK not set in (%d-%d), text: %s\n", end_offset -1, end_offset, buffer); + ok(check_CFE_LINK_selection(hwndRichEdit, end_offset, end_offset +1), + "CFE_LINK not set in (%d-%d), text: %s\n", end_offset, end_offset +1, buffer); + } + else + { + ok(!check_CFE_LINK_selection(hwndRichEdit, at_offset, at_offset +1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", at_offset, at_offset + 1, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset -1, end_offset), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset -1, end_offset, buffer); + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset, end_offset +1), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset, end_offset +1, buffer); + } + if (buffer[end_offset +1] != '\0') + { + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset +1, end_offset +2), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset +1, end_offset + 2, buffer); + if (buffer[end_offset +2] != '\0') + { + ok(!check_CFE_LINK_selection(hwndRichEdit, end_offset +2, end_offset +3), + "CFE_LINK incorrectly set in (%d-%d), text: %s\n", end_offset +2, end_offset +3, buffer); + } + } + } + + DestroyWindow(hwndRichEdit); + hwndRichEdit = NULL; + } + DestroyWindow(parent); } @@ -3259,7 +3482,6 @@ START_TEST( editor ) test_WM_GETTEXT(); test_EM_GETTEXTRANGE(); test_EM_GETSELTEXT(); - test_EM_AUTOURLDETECT(); test_EM_SETUNDOLIMIT(); test_ES_PASSWORD(); test_EM_SETTEXTEX(); @@ -3270,6 +3492,7 @@ START_TEST( editor ) test_EM_GETMODIFY(); test_EM_EXSETSEL(); test_WM_PASTE(); + test_EM_AUTOURLDETECT(); test_EM_STREAMIN(); test_EM_STREAMOUT(); test_EM_StreamIn_Undo(); -- 2.32.0.93.g670b81a890