format multiline notifications in event log (IDEA-80305)
authorpeter <peter@jetbrains.com>
Wed, 1 Feb 2012 17:26:05 +0000 (18:26 +0100)
committerpeter <peter@jetbrains.com>
Wed, 1 Feb 2012 19:56:01 +0000 (20:56 +0100)
platform/platform-impl/src/com/intellij/notification/EventLog.java
platform/platform-tests/testSrc/com/intellij/notification/EventLogTest.groovy

index 2d1e660c2c6d189cf4587d5c2bf8bc4b7de7631b..e6bff9c8c44d21040406f95abb9c162f5a83bd1a 100644 (file)
@@ -24,9 +24,12 @@ import com.intellij.notification.impl.NotificationsManagerImpl;
 import com.intellij.openapi.actionSystem.*;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.components.AbstractProjectComponent;
+import com.intellij.openapi.editor.Document;
 import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.RangeMarker;
 import com.intellij.openapi.editor.actions.ScrollToTheEndToolbarAction;
 import com.intellij.openapi.editor.actions.ToggleUseSoftWrapsToolbarAction;
+import com.intellij.openapi.editor.impl.DocumentImpl;
 import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces;
 import com.intellij.openapi.options.ShowSettingsUtil;
 import com.intellij.openapi.project.DumbAware;
@@ -43,6 +46,8 @@ import com.intellij.openapi.wm.ToolWindowManager;
 import com.intellij.ui.awt.RelativePoint;
 import com.intellij.ui.content.Content;
 import com.intellij.ui.content.ContentFactory;
+import com.intellij.util.containers.hash.LinkedHashMap;
+import com.intellij.util.text.CharArrayUtil;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -52,8 +57,9 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.TreeSet;
+import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -116,67 +122,134 @@ public class EventLog implements Notifications {
   }
 
   public static LogEntry formatForLog(@NotNull final Notification notification) {
-    String content = notification.getContent();
-    String mainText = notification.getTitle();
-    boolean showMore = false;
-    if (StringUtil.isNotEmpty(content)) {
-      if (content.startsWith("<p>")) {
-        content = content.substring("<p>".length());
-      }
+    DocumentImpl document = new DocumentImpl(true);
+    AtomicBoolean showMore = new AtomicBoolean(false);
+    Map<RangeMarker, HyperlinkInfo> links = new LinkedHashMap<RangeMarker, HyperlinkInfo>();
+    List<RangeMarker> lineSeparators = new ArrayList<RangeMarker>();
+
+    boolean hasHtml = parseHtmlContent(notification, document, showMore, links, lineSeparators);
+    removeJavaNewLines(document, lineSeparators, hasHtml);
+    insertNewLineSubstitutors(document, showMore, lineSeparators);
+
+    String title = notification.getTitle();
+    if (StringUtil.isNotEmpty(title)) {
+      document.insertString(0, document.getTextLength() > 0 ? title + ": " : title);
+    }
 
-      if (content.startsWith("<") && !content.startsWith("<a ")) {
-        showMore = true;
-      }
-      if (StringUtil.isNotEmpty(mainText)) {
-        mainText += ": ";
+    String status = document.getText();
+
+    ArrayList<Pair<TextRange, HyperlinkInfo>> list = new ArrayList<Pair<TextRange, HyperlinkInfo>>();
+    for (RangeMarker marker : links.keySet()) {
+      if (!marker.isValid()) {
+        showMore.set(true);
+        continue;
       }
-      mainText += content;
+      list.add(Pair.create(new TextRange(marker.getStartOffset(), marker.getEndOffset()), links.get(marker)));
     }
 
-    mainText = StringUtil.replace(mainText, "&nbsp;", " ");
-    int nlIndex = eolIndex(mainText);
-    if (nlIndex >= 0) {
-      mainText = mainText.substring(0, nlIndex);
-      showMore = true;
+    if (showMore.get()) {
+      String sb = "show balloon";
+      appendText(document, " (" + sb + ")");
+      list.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(document.getTextLength() - 1 - sb.length(), sb.length()),
+                                                  new ShowBalloon(notification)));
     }
 
-    List<Pair<TextRange, HyperlinkInfo>> links = new ArrayList<Pair<TextRange, HyperlinkInfo>>();
+    return new LogEntry(document.getText(), status, list);
+  }
 
-    String message = "";
+  private static boolean parseHtmlContent(Notification notification,
+                                          Document document,
+                                          AtomicBoolean showMore,
+                                          Map<RangeMarker, HyperlinkInfo> links, List<RangeMarker> lineSeparators) {
+    String content = notification.getContent();
+    content = StringUtil.replace(StringUtil.convertLineSeparators(content), "&nbsp;", " ");
+    boolean hasHtml = false;
     while (true) {
-      Matcher tagMatcher = TAG_PATTERN.matcher(mainText);
+      Matcher tagMatcher = TAG_PATTERN.matcher(content);
       if (!tagMatcher.find()) {
-        message += mainText;
+        appendText(document, content);
         break;
       }
-      message += mainText.substring(0, tagMatcher.start());
-      Matcher aMatcher = A_PATTERN.matcher(tagMatcher.group());
+      
+      String tagStart = tagMatcher.group();
+      appendText(document, content.substring(0, tagMatcher.start()));
+      Matcher aMatcher = A_PATTERN.matcher(tagStart);
       if (aMatcher.matches()) {
         final String href = aMatcher.group(2);
-        int linkEnd = mainText.indexOf(A_CLOSING, tagMatcher.end());
+        int linkEnd = content.indexOf(A_CLOSING, tagMatcher.end());
         if (linkEnd > 0) {
-          String linkText = mainText.substring(tagMatcher.end(), linkEnd).replaceAll(TAG_PATTERN.pattern(), "");
-
-          links.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(message.length(), linkText.length()), new NotificationHyperlinkInfo(notification, href)));
-
-          message += linkText;
-          mainText = mainText.substring(linkEnd + A_CLOSING.length());
-          continue;
+          String linkText = content.substring(tagMatcher.end(), linkEnd).replaceAll(TAG_PATTERN.pattern(), "");
+          appendText(document, linkText);
+          links.put(document.createRangeMarker(new TextRange(document.getTextLength() - linkText.length(), document.getTextLength())),
+                    new NotificationHyperlinkInfo(notification, href));
+          content = content.substring(linkEnd + A_CLOSING.length());
+        }
+      }
+      else {
+        hasHtml = true;
+        if ("<br>".equals(tagStart) ||
+            "</br>".equals(tagStart) ||
+            "</p>".equals(tagStart) ||
+            "<p>".equals(tagStart) ||
+            "<p/>".equals(tagStart)) {
+          lineSeparators.add(document.createRangeMarker(TextRange.from(document.getTextLength(), 0)));
         }
+        else if (!"<html>".equals(tagStart) && !"</html>".equals(tagStart) && !"<body>".equals(tagStart) && !"</body>".equals(tagStart)) {
+          showMore.set(true);
+        }
+        content = content.substring(tagMatcher.end());
       }
-      mainText = mainText.substring(tagMatcher.end());
     }
+    return hasHtml;
+  }
 
-    message = StringUtil.unescapeXml(StringUtil.convertLineSeparators(message));
+  private static void insertNewLineSubstitutors(Document document, AtomicBoolean showMore, List<RangeMarker> lineSeparators) {
+    for (int j = lineSeparators.size() - 1; j >= 0; j--) {
+      RangeMarker marker = lineSeparators.get(j);
+      if (!marker.isValid()) {
+        showMore.set(true);
+        continue;
+      }
+      
+      int offset = marker.getStartOffset();
+      if (offset == 0 || offset == document.getTextLength()) {
+        continue;
+      }
+      boolean spaceBefore = offset > 0 && Character.isWhitespace(document.getCharsSequence().charAt(offset - 1));
+      if (offset < document.getTextLength()) {
+        boolean spaceAfter = Character.isWhitespace(document.getCharsSequence().charAt(offset));
+        int next = CharArrayUtil.shiftForward(document.getCharsSequence(), offset, " \t");
+        if (next < document.getTextLength() && Character.isUpperCase(document.getCharsSequence().charAt(next))) {
+          document.insertString(offset, (spaceBefore ? "" : " ") + "//" + (spaceAfter ? "" : " "));
+          continue;
+        }
+        if (spaceAfter) {
+          continue;
+        }
+      }
+      if (spaceBefore) {
+        continue;
+      }
 
-    String status = message;
+      document.insertString(offset, " ");
+    }
+  }
 
-    if (showMore) {
-      message += " more ";
-      links.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(message.length() - 5, 4), new ShowBalloon(notification)));
+  private static void removeJavaNewLines(Document document, List<RangeMarker> lineSeparators, boolean hasHtml) {
+    String text = document.getText();
+    int i = -1;
+    while (true) {
+      i = text.indexOf('\n', i + 1);
+      if (i < 0) break;
+      document.deleteString(i, i + 1);
+      if (!hasHtml) {
+        lineSeparators.add(document.createRangeMarker(TextRange.from(i, 0)));
+      }
     }
+  }
 
-    return new LogEntry(message, status, links);
+  private static void appendText(Document document, String text) {
+    document.insertString(document.getTextLength(), StringUtil.unescapeXml(text));
   }
 
   public static class LogEntry {
@@ -191,22 +264,6 @@ public class EventLog implements Notifications {
     }
   }
 
-  private static int eolIndex(String mainText) {
-    TreeSet<Integer> indices = new TreeSet<Integer>();
-    indices.add(mainText.indexOf("<br>", 1));
-    indices.add(mainText.indexOf("<br/>", 1));
-    indices.add(mainText.indexOf("<p/>", 1));
-    indices.add(mainText.indexOf("<p>", 1));
-    indices.add(mainText.indexOf("\n"));
-    indices.remove(-1);
-    return indices.isEmpty() ? -1 : indices.iterator().next();
-  }
-
-  public static boolean isEventLogVisible(Project project) {
-    final ToolWindow window = getEventLog(project);
-    return window != null && window.isVisible();
-  }
-
   @Nullable
   public static ToolWindow getEventLog(Project project) {
     return project == null ? null : ToolWindowManager.getInstance(project).getToolWindow(LOG_TOOL_WINDOW_ID);
index f0ac1b26be4c0ded1f3366ef9b9b32815f4b2f9b..896cf802fa0315a234d457d9e6e230dacfcbce82 100644 (file)
  */
 package com.intellij.notification;
 
-import com.intellij.testFramework.UsefulTestCase;
+
+import com.intellij.openapi.util.TextRange
+import com.intellij.testFramework.LightPlatformTestCase
+import com.intellij.testFramework.PlatformTestCase
 
 /**
  * @author peter
  */
-class EventLogTest extends UsefulTestCase {
+class EventLogTest extends LightPlatformTestCase {
+
+  EventLogTest() {
+    PlatformTestCase.initPlatformLangPrefix()
+  }
 
   public void testNbsp() {
     def entry = EventLog.formatForLog(new Notification("xxx", "Title", "Hello&nbsp;world", NotificationType.ERROR))
     assert entry.message == 'Title: Hello world'
   }
 
+  public void testParseMultilineText() {
+    def entry = EventLog.formatForLog(new Notification("xxx", "Title", "<html><body> " +
+                                                                       "<font size=\"3\">first line<br>" +
+                                                                       "second line<br>" +
+                                                                       "third<br>" +
+                                                                       "<a href=\"create\">Action</a><br>" +
+                                                                       "</body></html>", NotificationType.ERROR))
+    assert entry.message == 'Title:  first line second line third // Action (show balloon)'
+    //                       0                                       40      48          60
+    assert entry.links.collect { it.first } == [new TextRange(40, 46), new TextRange(48, 60)]
+
+  }
+
+  public void testInParagraph() {
+    def entry = EventLog.formatForLog(new Notification("xxx", "Title", "<p>message</p>", NotificationType.ERROR))
+    assert entry.message == 'Title: message'
+  }
+
+  public void testJavaSeparators() {
+    def entry = EventLog.formatForLog(new Notification("xxx", "Title", "fst\nsnd", NotificationType.ERROR))
+    assert entry.message == 'Title: fst snd'
+  }
+
 }