[util] IDEA-247475 API for HTML concatenations

GitOrigin-RevId: 634bc8da11238b0ce7b89de66af837efe5ccef0f
This commit is contained in:
Tagir Valeev
2020-08-05 14:15:22 +07:00
committed by intellij-monorepo-bot
parent f842ce8802
commit bd2f9d5c3c
28 changed files with 674 additions and 114 deletions

View File

@@ -34,6 +34,7 @@ import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFileManager;
@@ -519,7 +520,8 @@ public final class CompileDriver {
ToolWindowManager.getInstance(myProject).notifyByBalloon(toolWindowId, messageType, statusMessage);
}
final String wrappedMessage = _status != ExitStatus.UP_TO_DATE? "<a href='#'>" + statusMessage + "</a>" : statusMessage;
final String wrappedMessage = _status != ExitStatus.UP_TO_DATE?
HtmlChunk.link("#", statusMessage).toString() : statusMessage;
final Notification notification = CompilerManager.NOTIFICATION_GROUP.createNotification(
"", wrappedMessage,
messageType.toNotificationType(),

View File

@@ -19,6 +19,8 @@ import com.intellij.openapi.roots.ui.configuration.projectRoot.LibraryConfigurab
import com.intellij.openapi.roots.ui.configuration.projectRoot.StructureConfigurableContext;
import com.intellij.openapi.ui.NamedConfigurable;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.PathUtil;
import com.intellij.xml.util.XmlStringUtil;
@@ -76,16 +78,16 @@ public class LibraryProjectStructureElement extends ProjectStructureElement {
}
private static String createInvalidRootsDescription(List<String> invalidClasses, String rootName, String libraryName) {
StringBuilder buffer = new StringBuilder();
HtmlBuilder buffer = new HtmlBuilder();
final String name = StringUtil.escapeXmlEntities(libraryName);
buffer.append("Library ");
buffer.append("<a href='http://library/").append(name).append("'>").append(name).append("</a>");
buffer.appendLink("http://library/"+name, name);
buffer.append(" has broken " + rootName + " " + StringUtil.pluralize("path", invalidClasses.size()) + ":");
for (String url : invalidClasses) {
buffer.append("<br>&nbsp;&nbsp;");
buffer.br().nbsp(2);
buffer.append(PathUtil.toPresentableUrl(url));
}
return XmlStringUtil.wrapInHtml(buffer);
return buffer.wrapWith("html").toString();
}
@NotNull
@@ -135,8 +137,7 @@ public class LibraryProjectStructureElement extends ProjectStructureElement {
@Override
public ProjectStructureProblemDescription createUnusedElementWarning() {
final List<ConfigurationErrorQuickFix> fixes = Arrays.asList(new AddLibraryToDependenciesFix(), new RemoveLibraryFix(), new RemoveAllUnusedLibrariesFix());
final String name = StringUtil.escapeXmlEntities(myLibrary.getName());
String libraryName = "<a href='http://library/" + name + "'>" + name + "</a>";
String libraryName = HtmlChunk.link("http://library/" + myLibrary.getName(), myLibrary.getName()).toString();
return new ProjectStructureProblemDescription(XmlStringUtil.wrapInHtml("Library " + libraryName + " is not used"), null, createPlace(),
ProjectStructureProblemType.unused("unused-library"), ProjectStructureProblemDescription.ProblemLevel.PROJECT,
fixes, false);

View File

@@ -26,4 +26,4 @@
</ul>
</div>
</div>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href='placeholder'>`genericMethod(Class&lt;?&gt;)` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href="placeholder">`genericMethod(Class&lt;?&gt;)` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>

View File

@@ -19,4 +19,4 @@
<div class="block"><a href="psi_element://com.jetbrains.LinkBetweenMethods#m2()"><code>m2()</code></a></div>
</li>
</ul>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href='placeholder'>`m1()` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href="placeholder">`m1()` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>

View File

@@ -24,4 +24,4 @@
</ul>
</div>
</div>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href='placeholder'>`SimpleInterface` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href="placeholder">`SimpleInterface` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>

View File

@@ -32,4 +32,4 @@ extends java.lang.Object</pre>
</li>
</ul>
</div>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href='placeholder'>`ClassWithRefLink` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href="placeholder">`ClassWithRefLink` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>

View File

@@ -26,4 +26,4 @@
</ul>
</div>
</div>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href='placeholder'>`param()` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>
</div><div class='bottom'><icon src='AllIcons.Nodes.PpLibFolder'>&nbsp;myLib</div><div class='bottom'><a href="placeholder">`param()` on localhost<icon src='AllIcons.Ide.External_link_arrow'></a>

View File

@@ -2,6 +2,7 @@
package com.intellij.util;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
@@ -43,7 +44,7 @@ public final class PathUtil {
}
@NotNull
public static String toPresentableUrl(@NotNull String url) {
public static @NlsSafe String toPresentableUrl(@NotNull String url) {
return getLocalPath(VirtualFileManager.extractPath(url));
}

View File

@@ -15,6 +15,7 @@ import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.ui.CheckedTreeNode;
import com.intellij.util.Function;
@@ -581,7 +582,7 @@ public final class PushController implements Disposable {
for (int i = 0; i < commits.size(); ++i) {
if (i >= commitsNum) {
@NonNls
final VcsLinkedTextComponent moreCommitsLink = new VcsLinkedTextComponent("<a href='loadMore'>...</a>", new VcsLinkListener() {
final VcsLinkedTextComponent moreCommitsLink = new VcsLinkedTextComponent(HtmlChunk.link("loadMore", "...").toString(), new VcsLinkListener() {
@Override
public void hyperlinkActivated(@NotNull DefaultMutableTreeNode sourceNode, @NotNull MouseEvent event) {
TreeNode parent = sourceNode.getParent();

View File

@@ -35,6 +35,8 @@ import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
@@ -254,16 +256,16 @@ public class FileInEditorProcessor {
@Override
@NotNull
public String getMessage() {
StringBuilder builder = new StringBuilder("<html>");
HtmlBuilder builder = new HtmlBuilder();
LayoutCodeInfoCollector notifications = myProcessor.getInfoCollector();
LOG.assertTrue(notifications != null);
if (notifications.isEmpty() && !myNoChangesDetected) {
if (myProcessChangesTextOnly) {
builder.append("No lines changed: changes since last revision are already properly formatted").append("<br>");
builder.append("No lines changed: changes since last revision are already properly formatted").br();
}
else {
builder.append("No lines changed: content is already properly formatted").append("<br>");
builder.append("No lines changed: content is already properly formatted").br();
}
}
else {
@@ -277,26 +279,25 @@ public class FileInEditorProcessor {
builder.append(" in changes since last revision");
}
builder.append("<br>");
builder.br();
}
else if (myNoChangesDetected) {
builder.append("No lines changed: no changes since last revision").append("<br>");
builder.append("No lines changed: no changes since last revision").br();
}
String optimizeImportsNotification = notifications.getOptimizeImportsNotification();
if (optimizeImportsNotification != null) {
builder.append(StringUtil.capitalize(optimizeImportsNotification)).append("<br>");
builder.append(StringUtil.capitalize(optimizeImportsNotification)).br();
}
}
String shortcutText = KeymapUtil.getFirstKeyboardShortcutText(ActionManager.getInstance().getAction("ShowReformatFileDialog"));
String color = ColorUtil.toHex(JBColor.gray);
String color = ColorUtil.toHtmlColor(JBColor.gray);
builder.append("<span style='color:#").append(color).append("'>")
.append("<a href=''>Show</a> reformat dialog: ").append(shortcutText).append("</span>")
.append("</html>");
builder.append(HtmlChunk.span("color:"+color)
.child(HtmlChunk.raw("<a href=''>Show</a> reformat dialog: ")).addText(shortcutText));
return builder.toString();
return builder.wrapWith("html").toString();
}
@Override

View File

@@ -44,6 +44,7 @@ import com.intellij.openapi.util.DimensionService;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
@@ -968,9 +969,6 @@ public class DocumentationComponent extends JPanel implements Disposable, DataPr
}
String title = manager.getTitle(element);
if (title != null) {
title = StringUtil.escapeXmlEntities(title);
}
if (externalUrl == null) {
List<String> urls = provider.getUrlFor(element, originalElement);
if (urls != null) {
@@ -997,27 +995,25 @@ public class DocumentationComponent extends JPanel implements Disposable, DataPr
if (link != null) return link;
}
return "<a href='external_doc'>External documentation" +
(title == null ? "" : (" for `" + title + "`")) +
"<icon src='AllIcons.Ide.External_link_arrow'></a></div>";
String linkText = "External documentation" + (title == null ? "" : " for `" + title + "`");
return HtmlChunk.link("external_doc", linkText)
.child(HtmlChunk.tag("icon").attr("src", "AllIcons.Ide.External_link_arrow")).toString();
}
private static String getLink(String title, String url) {
StringBuilder result = new StringBuilder();
String hostname = getHostname(url);
if (hostname == null) {
return null;
}
result.append("<a href='").append(url).append("'>");
String linkText;
if (title == null) {
result.append("Documentation");
linkText = "Documentation on " + hostname;
}
else {
result.append("`").append(title).append("`");
linkText = "`" + title + "` on " + hostname;
}
result.append(" on ").append(hostname).append("</a>");
return result.toString();
return HtmlChunk.link(url, linkText).toString();
}
static boolean shouldShowExternalDocumentationLink(DocumentationProvider provider,

View File

@@ -6,6 +6,7 @@ import com.intellij.codeInsight.highlighting.ReadWriteAccessDetector;
import com.intellij.openapi.fileEditor.FileEditorLocation;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.refactoring.RefactoringBundle;
@@ -109,14 +110,14 @@ public class ConflictsDialog extends DialogWrapper{
panel.add(new JLabel(RefactoringBundle.message("the.following.problems.were.found")), BorderLayout.NORTH);
@NonNls StringBuilder buf = new StringBuilder();
HtmlBuilder buf = new HtmlBuilder();
for (int i = 0; i < Math.min(myConflictDescriptions.length, MAX_CONFLICTS_SHOWN); i++) {
buf.append(myConflictDescriptions[i]).append("<br><br>");
buf.append(myConflictDescriptions[i]).br().br();
}
if (myConflictDescriptions.length > MAX_CONFLICTS_SHOWN) {
buf.append("<a href='" + EXPAND_LINK + "'>Show more...</a>");
buf.appendLink(EXPAND_LINK, "Show more...");
}
JEditorPane messagePane = new JEditorPane();

View File

@@ -3,11 +3,7 @@ package com.intellij.openapi.keymap.impl.ui;
import com.intellij.diagnostic.VMOptions;
import com.intellij.icons.AllIcons;
import com.intellij.ide.CommonActionsManager;
import com.intellij.ide.DataManager;
import com.intellij.ide.DefaultTreeExpander;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.TreeExpander;
import com.intellij.ide.*;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.QuickList;
@@ -29,6 +25,7 @@ import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeFrame;
@@ -145,7 +142,7 @@ public class KeymapPanel extends JPanel implements SearchableConfigurable, Confi
if (allConflicts.isEmpty())
return;
String htmlBody = "";
HtmlBuilder htmlBody = new HtmlBuilder();
final Map<String, Runnable> href2linkAction = new HashMap<>();
int count = 0;
boolean empty = true;
@@ -159,8 +156,8 @@ public class KeymapPanel extends JPanel implements SearchableConfigurable, Confi
final AnAction act = ActionManager.getInstance().getAction(actId);
final String actText = act == null ? actId : act.getTemplateText();
if (!empty)
htmlBody += ", ";
htmlBody += "<a href='" + actId + "'>" + actText + "</a>";
htmlBody.append(", ");
htmlBody.appendLink(actId, actText);
empty = false;
++count;
@@ -169,10 +166,10 @@ public class KeymapPanel extends JPanel implements SearchableConfigurable, Confi
}
if (count > 2 && allConflicts.size() > count) {
final @NotNull String text = String.format("%d more", allConflicts.size() - count);
htmlBody += " and <a href='" + text + "'>" + text + "</a>";
String actionId = "show.more";
htmlBody.append(" and ").appendLink(actionId, String.format("%d more", allConflicts.size() - count));
href2linkAction.put(text, ()->{
href2linkAction.put(actionId, ()->{
myShowOnlyConflicts = true;
myActionsTree.setBaseFilter(systemShortcuts.createKeymapConflictsActionFilter());
myActionsTree.filter(null, myQuickLists);
@@ -180,9 +177,10 @@ public class KeymapPanel extends JPanel implements SearchableConfigurable, Confi
});
}
htmlBody += " shortcuts conflict with the macOS system shortcuts.<br>Assign custom shortcuts or change the macOS system settings.</p></html>";
htmlBody.append(" shortcuts conflict with the macOS system shortcuts.")
.br().append("Assign custom shortcuts or change the macOS system settings.");
JBLabel jbLabel = new JBLabel(createWarningHtmlText(htmlBody)) {
JBLabel jbLabel = new JBLabel(createWarningHtmlText(htmlBody.toString())) {
@NotNull
@Override
protected HyperlinkListener createHyperlinkListener() {

View File

@@ -31,9 +31,12 @@ import com.intellij.openapi.util.AtomicNotNullLazyValue;
import com.intellij.openapi.util.BuildNumber;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.LineSeparator;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.DateFormatUtil;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
@@ -289,9 +292,10 @@ final class UpdateCheckerComponent {
}
String title = IdeBundle.message("update.installed.notification.title");
String message = "<html>" + StringUtil.join(descriptors, descriptor -> {
return "<a href='" + descriptor.getPluginId().getIdString() + "'>" + descriptor.getName() + "</a>";
}, ", ") + "</html>";
String message = new HtmlBuilder()
.appendWithSeparators(HtmlChunk.text(", "), ContainerUtil.map(
descriptors, descriptor -> HtmlChunk.link(descriptor.getPluginId().getIdString(), descriptor.getName())))
.wrapWith("html").toString();
UpdateChecker.getNotificationGroup().createNotification(title, message, NotificationType.INFORMATION, (notification, event) -> {
String id = event.getDescription();

View File

@@ -26,6 +26,8 @@ import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Factory;
import com.intellij.openapi.util.Segment;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindowId;
@@ -97,16 +99,17 @@ final class SearchForUsagesRunnable implements Runnable {
@NotNull
private static String createOptionsHtml(@NonNls UsageTarget @NotNull [] searchFor) {
KeyboardShortcut shortcut = UsageViewImpl.getShowUsagesWithSettingsShortcut(searchFor);
String shortcutText = "";
HtmlBuilder builder = new HtmlBuilder()
.appendLink(FIND_OPTIONS_HREF_TARGET, "Find Options...");
if (shortcut != null) {
shortcutText = "&nbsp;(" + KeymapUtil.getShortcutText(shortcut) + ")";
builder.nbsp(1).append("(" + KeymapUtil.getShortcutText(shortcut) + ")");
}
return "<a href='" + FIND_OPTIONS_HREF_TARGET + "'>Find Options...</a>" + shortcutText;
return builder.toString();
}
@NotNull
private static String createSearchInProjectHtml() {
return "<a href='" + SEARCH_IN_PROJECT_HREF_TARGET + "'>Search in Project</a>";
return HtmlChunk.link(SEARCH_IN_PROJECT_HREF_TARGET, "Search in Project").toString();
}
private void notifyByFindBalloon(@Nullable final HyperlinkListener listener,

View File

@@ -0,0 +1,141 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.util.text;
import com.intellij.openapi.util.NlsSafe;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A simple builder to create HTML fragments. It encapsulates a series of {@link HtmlChunk} objects.
*/
public final class HtmlBuilder {
private final List<HtmlChunk> myChunks = new ArrayList<>();
/**
* Appends a new chunk to this builder
*
* @param chunk chunk to append
* @return this builder
*/
@Contract("_ -> this")
public HtmlBuilder append(@NotNull HtmlChunk chunk) {
myChunks.add(chunk);
return this;
}
/**
* Appends a text chunk to this builder
*
* @param text text to append (must not be escaped by caller)
* @return this builder
*/
@Contract("_ -> this")
public HtmlBuilder append(@NotNull @Nls String text) {
myChunks.add(HtmlChunk.text(text));
return this;
}
/**
* Appends a raw html text to this builder. Should be sued with care.
* The purpose of this method is to be able to externalize the text with embedded link. E.g.:
* {@code "Click <a href=\"...\">here</a> for details"}.
*
* @param rawHtml raw HTML content. It's the responsibility of the caller to balance tags and escape HTML entities.
* @return this builder
*/
@Contract("_ -> this")
public HtmlBuilder appendRaw(@NotNull @Nls String rawHtml) {
myChunks.add(HtmlChunk.raw(rawHtml));
return this;
}
/**
* Appends a link element to this builder
*
* @param target link target (href)
* @param text link text
* @return this builder
*/
@Contract("_, _ -> this")
public HtmlBuilder appendLink(@NotNull @NonNls String target, @NotNull @Nls String text) {
myChunks.add(HtmlChunk.link(target, text));
return this;
}
/**
* Appends a collection of chunks interleaving them with a supplied separator chunk
*
* @param separator a separator chunk
* @param children chunks to append
* @return this builder
*/
@Contract("_, _ -> this")
public HtmlBuilder appendWithSeparators(@NotNull HtmlChunk separator, @NotNull Iterable<HtmlChunk> children) {
boolean first = true;
for (HtmlChunk child : children) {
if (!first) {
append(separator);
}
first = false;
append(child);
}
return this;
}
/**
* Appends a series of non-breaking spaces ({@code &nbsp;} entities).
*
* @param count number of non-breaking spaces to append
* @return this builder
*/
@Contract("_ -> this")
public HtmlBuilder nbsp(int count) {
myChunks.add(HtmlChunk.nbsp(count));
return this;
}
/**
* Appends a line-break ({@code <br/>}).
*
* @return this builder
*/
@Contract(" -> this")
public HtmlBuilder br() {
myChunks.add(HtmlChunk.br());
return this;
}
/**
* Wraps this builder content with a specified tag
*
* @param tag name of the tag to wrap with
* @return a new Element object that contains elements from this builder
*/
public HtmlChunk.Element wrapWith(@NotNull @NonNls String tag) {
return HtmlChunk.tag(tag).children(myChunks.toArray(new HtmlChunk[0]));
}
/**
* @return true if no elements were added to this builder
*/
public boolean isEmpty() {
return myChunks.isEmpty();
}
/**
* @return a rendered HTML representation of all the chunks in this builder.
*/
@Override
public @NlsSafe String toString() {
StringBuilder sb = new StringBuilder();
for (HtmlChunk chunk : myChunks) {
chunk.appendTo(sb);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,291 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.util.text;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.util.containers.UnmodifiableHashMap;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* An immutable representation of HTML node. Could be used as a DSL to quickly generate HTML strings.
*
* @see HtmlBuilder
*/
public abstract class HtmlChunk {
private static class Text extends HtmlChunk {
private final String myContent;
private Text(String content) {
myContent = content;
}
@Override
public void appendTo(@NotNull StringBuilder builder) {
builder.append(StringUtil.escapeXmlEntities(myContent));
}
}
private static class Raw extends HtmlChunk {
private final String myContent;
private Raw(String content) {
myContent = content;
}
@Override
public void appendTo(@NotNull StringBuilder builder) {
builder.append(myContent);
}
}
private static class Nbsp extends HtmlChunk {
private final int myCount;
private Nbsp(int count) {
myCount = count;
}
@Override
public void appendTo(@NotNull StringBuilder builder) {
builder.append(StringUtil.repeat("&nbsp;", myCount));
}
}
public static class Element extends HtmlChunk {
private static final Element BODY = tag("body");
private static final Element HTML = tag("html");
private static final Element BR = tag("br");
private static final Element DIV = tag("div");
private static final Element SPAN = tag("span");
private final String myTagName;
private final UnmodifiableHashMap<String, String> myAttributes;
private final List<HtmlChunk> myChildren;
private Element(String name,
UnmodifiableHashMap<String, String> attributes,
List<HtmlChunk> children) {
myTagName = name;
myAttributes = attributes;
myChildren = children;
}
@Override
public void appendTo(@NotNull StringBuilder builder) {
builder.append('<').append(myTagName);
myAttributes.forEach((attrKey, attrValue) -> {
builder.append(' ').append(attrKey).append("=\"").append(StringUtil.escapeXmlEntities(attrValue)).append('"');
});
if (myChildren.isEmpty()) {
builder.append("/>");
} else {
builder.append(">");
for (HtmlChunk child : myChildren) {
child.appendTo(builder);
}
builder.append("</").append(myTagName).append(">");
}
}
/**
* @param name attribute name
* @param value attribute value
* @return a new element that is like this element but has the specified attribute added or replaced
*/
public Element attr(@NonNls String name, String value) {
return new Element(myTagName, myAttributes.with(name, value), myChildren);
}
/**
* @param style CSS style specification
* @return a new element that is like this element but has the specified style added or replaced
*/
public Element style(@NonNls String style) {
return attr("style", style);
}
/**
* @param text text to add to the list of children (should not be escaped)
* @return a new element that is like this element but has an extra text child
*/
public Element addText(@NotNull @Nls String text) {
return child(text(text));
}
/**
* @param chunks chunks to add to the list of children
* @return a new element that is like this element but has extra children
*/
public Element children(@NotNull HtmlChunk @NotNull ... chunks) {
if (myChildren.isEmpty()) {
return new Element(myTagName, myAttributes, Arrays.asList(chunks));
}
List<HtmlChunk> newChildren = new ArrayList<>(myChildren.size() + chunks.length);
newChildren.addAll(myChildren);
Collections.addAll(myChildren, chunks);
return new Element(myTagName, myAttributes, newChildren);
}
/**
* @param chunk a chunk to add to the list of children
* @return a new element that is like this element but has an extra child
*/
public @NotNull Element child(@NotNull HtmlChunk chunk) {
if (myChildren.isEmpty()) {
return new Element(myTagName, myAttributes, Collections.singletonList(chunk));
}
List<HtmlChunk> newChildren = new ArrayList<>(myChildren.size() + 1);
newChildren.addAll(myChildren);
newChildren.add(chunk);
return new Element(myTagName, myAttributes, newChildren);
}
}
/**
* @param tagName name of the tag to wrap with
* @return an element that wraps this element
*/
public @NotNull Element wrapWith(@NotNull @NonNls String tagName) {
return new Element(tagName, UnmodifiableHashMap.empty(), Collections.singletonList(this));
}
/**
* @return a B element that wraps this element
*/
public @NotNull Element bold() {
return wrapWith("b");
}
/**
* @return an I element that wraps this element
*/
public @NotNull Element italic() {
return wrapWith("i");
}
/**
* @param tagName name of the tag
* @return an empty tag
*/
public static @NotNull Element tag(@NotNull @NonNls String tagName) {
return new Element(tagName, UnmodifiableHashMap.empty(), Collections.emptyList());
}
/**
* @return a &lt;div&gt; element
*/
public static @NotNull Element div() {
return Element.DIV;
}
/**
* @return a &lt;div&gt; element with a specified style.
*/
public static @NotNull Element div(@NotNull @NonNls String style) {
return Element.DIV.style(style);
}
/**
* @return a &lt;span&gt; element.
*/
public static @NotNull Element span() {
return Element.SPAN;
}
/**
* @return a &lt;span&gt; element with a specified style.
*/
public static @NotNull Element span(@NonNls @NotNull String style) {
return Element.SPAN.style(style);
}
/**
* @return a &lt;br&gt; element.
*/
public static @NotNull Element br() {
return Element.BR;
}
/**
* @return a &lt;body&gt; element.
*/
public static @NotNull Element body() {
return Element.BODY;
}
/**
* @return a &lt;html&gt; element.
*/
public static @NotNull Element html() {
return Element.HTML;
}
/**
* Creates a HTML text node that represents a given number of non-breaking spaces
*
* @param count number of non-breaking spaces
* @return HtmlChunk that represents a sequence of non-breaking spaces
*/
public static @NotNull HtmlChunk nbsp(int count) {
if (count <= 0) {
throw new IllegalArgumentException();
}
return new Nbsp(count);
}
/**
* Creates a HTML text node
*
* @param text text to display (no escaping should be done by caller).
* @return HtmlChunk that represents a HTML text node.
*/
public static @NotNull HtmlChunk text(@NotNull @Nls String text) {
return new Text(text);
}
/**
* Creates a chunk that represents a piece of raw HTML. Should be used with care!
* The purpose of this method is to be able to externalize the text with embedded link. E.g.:
* {@code "Click <a href=\"...\">here</a> for details"}.
*
* @param rawHtml raw HTML content. It's the responsibility of the caller to balance tags and escape HTML entities.
* @return the HtmlChunk that represents the supplied content.
*/
public static @NotNull HtmlChunk raw(@NotNull @Nls String rawHtml) {
return new Raw(rawHtml);
}
/**
* Creates an element that represents a simple HTML link.
*
* @param target link target (HREF)
* @param text link text
* @return the Element that represents a link
*/
public static @NotNull Element link(@NotNull @NonNls String target, @NotNull @Nls String text) {
return new Element("a", UnmodifiableHashMap.<String, String>empty().with("href", target), Collections.singletonList(text(text)));
}
/**
* Appends the rendered HTML representation of this chunk to the supplied builder
*
* @param builder builder to append to.
*/
abstract public void appendTo(@NotNull StringBuilder builder);
/**
* @return the rendered HTML representation of this chunk.
*/
@Override
public @NlsSafe @NotNull String toString() {
StringBuilder builder = new StringBuilder();
appendTo(builder);
return builder.toString();
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.text;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.util.containers.ContainerUtil;
import org.junit.Test;
import java.util.Collections;
import static org.junit.Assert.*;
public class HtmlBuilderTest {
@Test
public void create() {
assertEquals("", new HtmlBuilder().toString());
}
@Test
public void isEmpty() {
assertTrue(new HtmlBuilder().isEmpty());
assertFalse(new HtmlBuilder().append("foo").isEmpty());
}
@Test
public void append() {
assertEquals("hello world!", new HtmlBuilder().append("hello ").append("world!").toString());
assertEquals("&lt;click here&gt;", new HtmlBuilder().append("<click here>").toString());
assertEquals("<br/>&lt;click here&gt;", new HtmlBuilder().append(HtmlChunk.br()).append("<click here>").toString());
}
@Test
public void appendLink() {
assertEquals("<a href=\"url\">click</a> for more info", new HtmlBuilder().appendLink("url", "click").append(" for more info").toString());
}
@Test
public void appendWithSeparators() {
assertEquals("", new HtmlBuilder().appendWithSeparators(HtmlChunk.br(), Collections.emptyList()).toString());
String[] data = {"foo", "bar", "baz"};
String html = new HtmlBuilder().appendWithSeparators(HtmlChunk.br(), ContainerUtil.map(data, d -> HtmlChunk.link(d, d))).toString();
assertEquals("<a href=\"foo\">foo</a><br/><a href=\"bar\">bar</a><br/><a href=\"baz\">baz</a>", html);
}
@Test
public void wrapWith() {
assertEquals("<html>Click <a href=\"foo\">here</a></html>",
new HtmlBuilder().append("Click ").appendLink("foo", "here").wrapWith("html").toString());
}
}

View File

@@ -0,0 +1,65 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.text;
import com.intellij.openapi.util.text.HtmlChunk;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class HtmlChunkTest {
@Test
public void text() {
assertEquals("foo", HtmlChunk.text("foo").toString());
assertEquals("&lt;a href=&quot;hello&quot;&gt;", HtmlChunk.text("<a href=\"hello\">").toString());
}
@Test
public void raw() {
assertEquals("foo", HtmlChunk.raw("foo").toString());
assertEquals("<a href=\"hello\">", HtmlChunk.raw("<a href=\"hello\">").toString());
}
@Test
public void nbsp() {
assertEquals("&nbsp;", HtmlChunk.nbsp(1).toString());
assertEquals("&nbsp;&nbsp;&nbsp;", HtmlChunk.nbsp(3).toString());
}
@Test
public void link() {
assertEquals("<a href=\"target\">&lt;Click here&gt;</a>", HtmlChunk.link("target", "<Click here>").toString());
}
@Test
public void tag() {
assertEquals("<b/>", HtmlChunk.tag("b").toString());
assertEquals("<br/>", HtmlChunk.br().toString());
}
@Test
public void div() {
assertEquals("<div/>", HtmlChunk.div().toString());
assertEquals("<div style=\"color: blue\"/>", HtmlChunk.div("color: blue").toString());
}
@Test
public void attr() {
assertEquals("<p align=\"left\"/>", HtmlChunk.tag("p").attr("align", "left").toString());
assertEquals("<p align=\"right\"/>", HtmlChunk.tag("p").attr("align", "left").attr("align", "right").toString());
}
@Test
public void children() {
assertEquals("<p align=\"left\"><br/></p>", HtmlChunk.tag("p").attr("align", "left").child(HtmlChunk.br()).toString());
assertEquals("<p>&lt;hello&gt;</p>", HtmlChunk.tag("p").child(HtmlChunk.text("<hello>")).toString());
assertEquals("<p>&lt;hello&gt;</p>", HtmlChunk.tag("p").addText("<hello>").toString());
assertEquals("<p><a href=\"ref\">&lt;hello&gt;</a></p>", HtmlChunk.tag("p").child(HtmlChunk.link("ref", "<hello>")).toString());
}
@Test
public void wrapWith() {
assertEquals("<p>hello</p>", HtmlChunk.text("hello").wrapWith("p").toString());
assertEquals("<b>hello</b>", HtmlChunk.text("hello").bold().toString());
assertEquals("<i>hello</i>", HtmlChunk.text("hello").italic().toString());
}
}

View File

@@ -8,6 +8,7 @@ import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.changes.Change;
@@ -135,20 +136,18 @@ class GitCheckoutOperation extends GitBranchOperation {
}
else {
Collection<GitRepository> successfulRepositories = getSuccessfulRepositories();
HtmlBuilder builder = new HtmlBuilder();
String mentionSuccess = GitBundle.message("checkout.operation.in", getSuccessMessage(),
successfulRepositories.size(),
joinShortNames(successfulRepositories, REPOSITORIES_LIMIT));
String mentionSkipped = wereSkipped()
? UIUtil.BR + revisionNotFound
: "";
builder.appendRaw(mentionSuccess);
if (wereSkipped()) {
builder.br().append(revisionNotFound);
}
builder.br().appendLink(ROLLBACK_HREF_ATTRIBUTE, GitBundle.message("checkout.operation.rollback"));
VcsNotifier.getInstance(myProject).notifySuccess("",
mentionSuccess +
mentionSkipped +
UIUtil.BR +
"<a href='" + ROLLBACK_HREF_ATTRIBUTE + "'>" + //NON-NLS
GitBundle.message("checkout.operation.rollback")
+ "</a>", //NON-NLS
builder.toString(),
new RollbackOperationNotificationListener());
}
notifyBranchHasChanged(myStartPointReference);

View File

@@ -8,11 +8,11 @@ import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.Hash;
import git4idea.GitUtil;
import git4idea.commands.*;
@@ -147,7 +147,7 @@ class GitMergeOperation extends GitBranchOperation {
}
@Override
protected void notifySuccess(@NotNull String message) {
protected void notifySuccess(@NotNull @Nls String message) {
switch (myDeleteOnMerge) {
case DELETE:
super.notifySuccess(message);
@@ -155,7 +155,7 @@ class GitMergeOperation extends GitBranchOperation {
break;
case PROPOSE:
String deleteBranch = GitBundle.message("merge.operation.delete.branch", myBranchToMerge);
String description = message + UIUtil.BR + "<a href='" + DELETE_HREF_ATTRIBUTE + "'>" + deleteBranch + "</a>"; //NON-NLS
String description = new HtmlBuilder().appendRaw(message).br().appendLink(DELETE_HREF_ATTRIBUTE, deleteBranch).toString();
VcsNotifier.getInstance(myProject).notifySuccess("", description, new DeleteMergedLocalBranchNotificationListener());
break;
case NOTHING:
@@ -327,11 +327,13 @@ class GitMergeOperation extends GitBranchOperation {
@NotNull
@Override
protected String getRollbackProposal() {
return GitBundle.message("merge.operation.however.merge.has.succeeded.for.the.following.repositories", getSuccessfulRepositories().size()) +
UIUtil.BR +
successfulRepositoriesJoined() +
UIUtil.BR +
GitBundle.message("merge.operation.you.may.rollback.not.to.let.branches.diverge");
return new HtmlBuilder()
.append(
GitBundle.message("merge.operation.however.merge.has.succeeded.for.the.following.repositories", getSuccessfulRepositories().size()))
.br()
.appendRaw(successfulRepositoriesJoined())
.br()
.append(GitBundle.message("merge.operation.you.may.rollback.not.to.let.branches.diverge")).toString();
}
@NotNull

View File

@@ -12,6 +12,8 @@ import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.ChangesUtil;
import com.intellij.openapi.vcs.changes.CommitContext;
@@ -294,22 +296,24 @@ public class GitCheckinHandlerFactory extends VcsCheckinHandlerFactory {
final String title;
final String message;
final CharSequence rootPath = detachedRoot.myRoot.getPresentableUrl();
final String rootPath = detachedRoot.myRoot.getPresentableUrl();
final String messageCommonStart = "The Git repository at the following path";
if (detachedRoot.myRebase) {
title = "Unfinished Rebase Process";
message = messageCommonStart + " has an <b>unfinished rebase</b> process: <br/>" +
"<b>" + rootPath + "</b><br>" +
"You probably want to <b>continue rebase</b> instead of committing. <br/>" +
"Committing during rebase may lead to the commit loss. <br/>" +
readMore("https://www.kernel.org/pub/software/scm/git/docs/git-rebase.html", "Read more about Git rebase");
message = new HtmlBuilder()
.appendRaw(messageCommonStart + " has an <b>unfinished rebase</b> process:").br()
.append(HtmlChunk.text(rootPath).bold()).br()
.appendRaw("You probably want to <b>continue rebase</b> instead of committing.").br()
.append("Committing during rebase may lead to the commit loss.").br()
.appendLink("https://www.kernel.org/pub/software/scm/git/docs/git-rebase.html", "Read more about Git rebase").toString();
} else {
title = "Commit in Detached HEAD";
message = messageCommonStart + " is in the <b>detached HEAD</b> state: <br/>" +
"<b>" + rootPath + "</b><br>" +
"You can look around, make experimental changes and commit them, but be sure to checkout a branch not to lose your work. <br/>" +
"Otherwise you risk losing your changes. <br/>" +
readMore("http://gitolite.com/detached-head.html", "Read more about detached HEAD");
message = new HtmlBuilder()
.appendRaw(messageCommonStart + " is in the <b>detached HEAD</b> state:").br()
.append(HtmlChunk.text(rootPath).bold()).br()
.append("You can look around, make experimental changes and commit them, but be sure to checkout a branch not to lose your work.").br()
.append("Otherwise you risk losing your changes.").br()
.appendLink("http://gitolite.com/detached-head.html", "Read more about detached HEAD").toString();
}
DialogWrapper.DoNotAskOption dontAskAgain = new DialogWrapper.DoNotAskOption.Adapter() {
@@ -337,11 +341,6 @@ public class GitCheckinHandlerFactory extends VcsCheckinHandlerFactory {
return executor == null || executor instanceof GitCommitAndPushExecutor;
}
@NotNull
private static String readMore(@NotNull String link, @NotNull String message) {
return String.format("<a href='%s'>%s</a>.", link, message);
}
/**
* Scans the Git roots, selected for commit, for the root which is on a detached HEAD.
* Returns null, if all repositories are on the branch.

View File

@@ -227,7 +227,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
assertDetachedState(second, "feature")
assertSuccessfulNotification("Checked out ${bcode("feature")} in community and contrib<br/>" +
"Revision not found in ${project.stateStore.projectBasePath.fileName}<br><a href='rollback'>Rollback</a>")
"Revision not found in ${project.stateStore.projectBasePath.fileName}<br><a href=\"rollback\">Rollback</a>")
}
fun `test checkout with untracked files overwritten by checkout in first repo should show notification`() {
@@ -343,7 +343,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
fun `test agree to smart merge should smart merge`() {
val localChanges = `agree to smart operation`("merge",
"Merged <b><code>feature</code></b> to <b><code>master</code></b><br/><a href='delete'>Delete feature</a>")
"Merged <b><code>feature</code></b> to <b><code>master</code></b><br/><a href=\"delete\">Delete feature</a>")
cd(last)
val actual = cat(localChanges.first())
@@ -653,7 +653,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
mergeBranch("master2", TestUiHandler())
assertSuccessfulNotification("Merged ${bcode("master2")} to ${bcode("master")}<br/>" +
"<a href='delete'>Delete master2</a>")
"<a href=\"delete\">Delete master2</a>")
assertFile(last, "branch_file.txt", "branch content")
assertFile(first, "branch_file.txt", "branch content")
assertFile(second, "branch_file.txt", "branch content")
@@ -709,7 +709,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
mergeBranch("master2", TestUiHandler())
assertNotNull("Success message wasn't shown", vcsNotifier.lastNotification)
assertEquals("Success message is incorrect", "Already up-to-date<br/><a href='delete'>Delete master2</a>",
assertEquals("Success message is incorrect", "Already up-to-date<br/><a href=\"delete\">Delete master2</a>",
vcsNotifier.lastNotification.content)
}
@@ -722,7 +722,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
assertNotNull("Success message wasn't shown", vcsNotifier.lastNotification)
assertEquals("Success message is incorrect",
"Merged " + bcode("master2") + " to " + bcode("master") + "<br/><a href='delete'>Delete master2</a>",
"Merged " + bcode("master2") + " to " + bcode("master") + "<br/><a href=\"delete\">Delete master2</a>",
vcsNotifier.lastNotification.content)
assertFile(first, "branch_file.txt", "branch content")
}

View File

@@ -13,6 +13,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.MessageDialogBuilder;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.vcs.VcsNotifier;
import git4idea.i18n.GitBundle;
import org.jetbrains.annotations.NotNull;
@@ -98,9 +99,8 @@ public final class GithubNotifications {
@NotificationContent @NotNull String message,
@NotNull String url) {
LOG.info(title + "; " + message + "; " + url);
//noinspection HardCodedStringLiteral
VcsNotifier.getInstance(project)
.notifyImportantInfo(title, "<a href='" + url + "'>" + message + "</a>", NotificationListener.URL_OPENING_LISTENER);
.notifyImportantInfo(title, HtmlChunk.link(url, message).toString(), NotificationListener.URL_OPENING_LISTENER);
}
public static void showWarningURL(@NotNull Project project,

View File

@@ -25,7 +25,7 @@ import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupActivity;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.util.concurrency.AppExecutorUtil;
import org.jetbrains.annotations.NotNull;
@@ -51,16 +51,16 @@ public class MvcProjectWithoutLibraryNotificator implements StartupActivity.Dumb
final String name = framework.getFrameworkName();
final Map<String, Runnable> actions = framework.createConfigureActions(module);
final StringBuilder content = new StringBuilder()
.append("<html><body>")
.append("Module ").append('\'').append(module.getName()).append('\'')
.append(" has no ").append(name).append(" SDK.");
if (!actions.isEmpty()) content.append("<br/>");
content.append(StringUtil.join(actions.keySet(), actionName -> String.format("<a href='%s'>%s</a>", actionName, actionName), " "));
content.append("</body></html>");
HtmlBuilder builder = new HtmlBuilder();
builder.append("Module '" + module.getName() + "' has no " + name + " SDK.");
if (!actions.isEmpty()) builder.br();
for (String actionName : actions.keySet()) {
builder.appendLink(actionName, actionName).append(" ");
}
String message = builder.wrapWith("body").wrapWith("html").toString();
new Notification(
name + ".Configure", name + " SDK not found", content.toString(), NotificationType.INFORMATION,
name + ".Configure", name + " SDK not found", message, NotificationType.INFORMATION,
new NotificationListener.Adapter() {
@Override
protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent e) {

View File

@@ -10,6 +10,7 @@ import com.intellij.dvcs.push.ui.VcsEditableTextComponent;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
@@ -37,7 +38,7 @@ public class HgPushTargetPanel extends PushTargetPanel<HgTarget> {
myBranchName = source.getBranch();
final List<String> targetVariants = HgUtil.getTargetNames(repository);
String defaultText = defaultTarget != null ? defaultTarget.getPresentation() : "";
myTargetRenderedComponent = new VcsEditableTextComponent("<a href=''>" + defaultText + "</a>", null);
myTargetRenderedComponent = new VcsEditableTextComponent(HtmlChunk.link("", defaultText).toString(), null);
myDestTargetPanel = new PushTargetTextField(repository.getProject(), targetVariants, defaultText);
add(myDestTargetPanel, BorderLayout.CENTER);
}

View File

@@ -15,7 +15,10 @@ import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.impl.FoldingPopupManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.HintHint;
import com.intellij.ui.LightweightHint;
import com.intellij.util.Alarm;
@@ -106,9 +109,9 @@ public final class EditPropertyValueTooltipManager implements EditorMouseListene
if (action == null) return null;
String text = action.getTemplateText();
if (text == null) return null;
StringBuilder b = new StringBuilder().append("<a href='").append(href).append("'>").append(text).append("</a> <span style='color:#");
UIUtil.appendColor(UIUtil.getContextHelpForeground(), b);
return b.append("'>").append(KeymapUtil.getFirstKeyboardShortcutText(action)).append("</span>").toString();
HtmlChunk.Element shortcut = HtmlChunk.span("color:" + ColorUtil.toHtmlColor(UIUtil.getContextHelpForeground()))
.addText(KeymapUtil.getFirstKeyboardShortcutText(action));
return new HtmlBuilder().appendLink(href, text).append(" ").append(shortcut).toString();
}
public static LightweightHint showTooltip(@NotNull Editor editor, @NotNull JComponent component, boolean tenacious) {

View File

@@ -13,6 +13,7 @@ import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.AtomicNullableLazyValue;
import com.intellij.openapi.util.NullableLazyValue;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
@@ -111,7 +112,7 @@ public class ShDocumentationProvider implements DocumentationProvider {
while (m.find()) {
if (m.groupCount() > 0) {
String url = m.group(0);
m.appendReplacement(sb, "<a href='" + url + "'>" + url + "</a>");
m.appendReplacement(sb, HtmlChunk.link(url, url).toString());
}
}
m.appendTail(sb);