richedit: WM_SETTEXT must immediately autodetect URLs, without waiting for a WM_CHAR.
authorAlex Villacís Lasso <a_villacis@palosanto.com>
Wed, 18 Jun 2008 15:54:17 +0000 (10:54 -0500)
committerAlexandre Julliard <julliard@winehq.org>
Thu, 19 Jun 2008 09:59:04 +0000 (11:59 +0200)
dlls/riched20/editor.c
dlls/riched20/editor.h
dlls/riched20/tests/editor.c

index f72f24371cd4e565afc9998461e642cc6fc485c1..edd6be6da8873cf95bb0e82fca15c38b482f2622 100644 (file)
@@ -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;
+}
index aa28060f6434a7a5da5c32f4ad33370ad04c3db6..8bc819353ffbb90591e1f88690e27ada4ad5ac80 100644 (file)
@@ -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);
index a1d11276cfcb323f56f1155531d2f09440a1d5ab..69951b8dc75051d7d1276f13008270095c27dd1d 100644 (file)
@@ -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();