[openapi] IDEA-248714 - html link in Project Structure -> Problems

Both ProjectStructureProblemDescription#getDescription and ConfigurationError#getDescription used to return a string with caused problems with escaped html characters that can be converted multiple times and hence appear not as html tags on panes but as strings with extra symbols. In order to overcome these problems this patch changed the return type of the methods to HtmlChunk to provide the type safety when working with html tags.

Signed-off-by: Nikita Eshkeev <nikita.eshkeev@jetbrains.com>

GitOrigin-RevId: 277c8b44606159db7dc9e5101f3638eeb3bab541
This commit is contained in:
Nikita Eshkeev
2020-09-01 22:53:57 +03:00
committed by intellij-monorepo-bot
parent d91811c567
commit 34eaac02c3
11 changed files with 164 additions and 67 deletions

View File

@@ -268,3 +268,4 @@ popup.title.debug.recent.tests=Debug Recent Tests
popup.title.run.recent.tests=Run Recent Tests
shift.key=Shift
popup.advertisement.debug.with.shift.navigate.with.f4=Debug with {0}, navigate with F4
module.sources.set.display.name={0, choice, 0#|1#Tests of }''{1}'' module

View File

@@ -15,7 +15,9 @@
*/
package com.intellij.compiler;
import com.intellij.openapi.compiler.JavaCompilerBundle;
import com.intellij.openapi.module.Module;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
@@ -63,8 +65,9 @@ public class ModuleSourceSet {
}
@NotNull
public String getDisplayName() {
return (myType == Type.PRODUCTION ? "" : "Tests of ") + "'" + myModule.getName() + "' module";
public @Nls(capitalization = Nls.Capitalization.Sentence) String getDisplayName() {
final int choice = myType == Type.PRODUCTION ? 0 : 1;
return JavaCompilerBundle.message("module.sources.set.display.name", choice, myModule.getName());
}
@NotNull

View File

@@ -78,7 +78,7 @@ jdk.combo.box.none.item=<None>
error.resolve.generic=Resolve Error
action.name.extract.artifact=Extract Artifact...
prompt.overwrite.project.file=The {1} file \n''{0}''\nalready exists.\nWould you like to overwrite it?
module.circular.dependency.warning.description=<html><b>There are circular dependencies between:</b> {0}</html>
module.circular.dependency.warning.description=There are circular dependencies between:
action.text.0.disabled.if.elements.are.sorted={0} (Disabled if Elements Are Sorted)
error.project.undefined=No external project config file is defined
dialog.title.extract.artifact=Extract Artifact

View File

@@ -16,24 +16,42 @@
package com.intellij.openapi.roots.ui.configuration;
import com.intellij.openapi.util.NlsContexts.DetailedDescription;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.ui.awt.RelativePoint;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.Comparator;
public abstract class ConfigurationError implements Comparable<ConfigurationError> {
private final String myPlainTextTitle;
private final @DetailedDescription String myDescription;
private final HtmlChunk myDescription;
private boolean myIgnored;
protected ConfigurationError(final String plainTextTitle, final @NotNull HtmlChunk description) {
this(plainTextTitle, description, false);
}
protected ConfigurationError(final String plainTextTitle, final @NotNull HtmlChunk description, final boolean ignored) {
myPlainTextTitle = plainTextTitle;
myDescription = description;
myIgnored = ignored;
}
/**
* @deprecated Use the constructors with {@link HtmlChunk} for description
*/
@Deprecated
protected ConfigurationError(final String plainTextTitle, final @DetailedDescription String description) {
this(plainTextTitle, description, false);
}
/**
* @deprecated Use the constructors with {@link HtmlChunk} for description
*/
@Deprecated
protected ConfigurationError(final String plainTextTitle, final @DetailedDescription String description, final boolean ignored) {
myPlainTextTitle = plainTextTitle;
myDescription = description;
myIgnored = ignored;
this(plainTextTitle, HtmlChunk.raw(description), ignored);
}
@NotNull
@@ -42,7 +60,7 @@ public abstract class ConfigurationError implements Comparable<ConfigurationErro
}
@NotNull
public @DetailedDescription String getDescription() {
public HtmlChunk getDescription() {
return myDescription;
}
@@ -82,7 +100,7 @@ public abstract class ConfigurationError implements Comparable<ConfigurationErro
final int titleResult = getPlainTextTitle().compareTo(o.getPlainTextTitle());
if (titleResult != 0) return titleResult;
final int descriptionResult = getDescription().compareTo(o.getDescription());
final int descriptionResult = Comparator.comparing(e -> getDescription().toString()).compare(this, o);
if (descriptionResult != 0) return descriptionResult;
return 0;

View File

@@ -22,7 +22,7 @@ import com.intellij.util.ui.StartupUiUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import com.intellij.xml.util.XmlStringUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -36,6 +36,7 @@ import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
/**
* @author Konstantin Bulenkov
@@ -165,38 +166,63 @@ public class ErrorPaneConfigurable extends JPanel implements Configurable, Dispo
@Contract(pure = true)
private static HtmlChunk @NotNull[] getErrorDescriptions(final ConfigurationError @NotNull[] errors) {
final int limit = Math.min(errors.length, 100);
final HtmlChunk[] liTags = new HtmlChunk[limit];
for (int i = 0; i < limit; i++) {
final ConfigurationError error = errors[i];
String description;
if (error instanceof ProjectConfigurationProblem) {
//todo[nik] pass ProjectStructureProblemDescription directly and get rid of ConfigurationError at all
ProjectStructureProblemDescription problemDescription = ((ProjectConfigurationProblem)error).getProblemDescription();
description = problemDescription.getDescription();
if (description == null) {
description = problemDescription.getMessage(false);
if (problemDescription.canShowPlace()) {
ProjectStructureElement place = problemDescription.getPlace().getContainingElement();
final String link = HtmlChunk.link("http://navigate/" + i, place.getPresentableName()).toString();
description = XmlStringUtil.convertToHtmlContent(description);
description = place.getTypeName() + " " + link + ": " + StringUtil.decapitalize(description);
}
}
}
else {
description = error.getDescription();
}
if (XmlStringUtil.isWrappedInHtml(description)) {
description = XmlStringUtil.stripHtml(description);
}
if (error.canBeFixed()) {
final String text = "[" + JavaUiBundle.message("fix.link.text") + "]";
description += " " + HtmlChunk.link("http://fix/" + i, text);
}
final HtmlChunk li = HtmlChunk.raw("<li>" + description + "</li>");
liTags[i] = li;
final Integer[] indices = IntStream.range(0, limit).boxed().toArray(Integer[]::new);
return StreamEx.zip(indices, errors, ConfigurationErrorWithIndex::new)
.map(ErrorPaneConfigurable::getErrorDescriptionTag)
.toArray(HtmlChunk[]::new);
}
private static final class ConfigurationErrorWithIndex {
private final int myIdx;
private final @NotNull ConfigurationError myError;
private ConfigurationErrorWithIndex(final int idx, @NotNull final ConfigurationError error) {
myIdx = idx;
myError = error;
}
return liTags;
}
@Contract(pure = true)
@NotNull
private static HtmlChunk getErrorDescriptionTag(@NotNull final ConfigurationErrorWithIndex errorIndex) {
final int index = errorIndex.myIdx;
final ConfigurationError error = errorIndex.myError ;
final HtmlChunk description = getErrorDescription(index, error);
if (!error.canBeFixed()) return description.wrapWith("li");
final String text = "[" + JavaUiBundle.message("fix.link.text") + "]";
return new HtmlBuilder().append(description)
.append(HtmlChunk.link("http://fix/" + index, text))
.wrapWith("li");
}
@Contract(pure = true)
@NotNull
private static HtmlChunk getErrorDescription(final int index, @NotNull final ConfigurationError error) {
//todo[nik] pass ProjectStructureProblemDescription directly and get rid of ConfigurationError at all
if (!(error instanceof ProjectConfigurationProblem)) return error.getDescription();
final ProjectStructureProblemDescription problemDescription = ((ProjectConfigurationProblem)error).getProblemDescription();
if (problemDescription.getDescription() != null) return problemDescription.getDescription();
if (!problemDescription.canShowPlace()) return HtmlChunk.text(problemDescription.getMessage());
final String message = StringUtil.decapitalize(problemDescription.getMessage());
final ProjectStructureElement place = problemDescription.getPlace().getContainingElement();
final HtmlChunk link = HtmlChunk.link("http://navigate/" + index, place.getPresentableName());
return new HtmlBuilder().append(place.getTypeName())
.append(" ")
.append(link)
.append(": ")
.append(message)
.toFragment();
}
@Nls

View File

@@ -30,11 +30,12 @@ import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel;
import com.intellij.openapi.roots.ui.configuration.projectRoot.StructureConfigurableContext;
import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.*;
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.Chunk;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@@ -77,11 +78,11 @@ public class GeneralProjectSettingsElement extends ProjectStructureElement {
List<Chunk<ModuleSourceSet>> sourceSetCycles = ModuleCompilerUtil.computeSourceSetCycles(myContext.getModulesConfigurator());
List<String> cycles = new ArrayList<>();
List<@Nls String> cycles = new ArrayList<>();
for (Chunk<ModuleSourceSet> chunk : sourceSetCycles) {
final Set<ModuleSourceSet> sourceSets = chunk.getNodes();
List<String> names = new ArrayList<>();
List<@Nls String> names = new ArrayList<>();
for (ModuleSourceSet sourceSet : sourceSets) {
String name = sourceSet.getDisplayName();
names.add(names.isEmpty() ? name : StringUtil.decapitalize(name));
@@ -92,15 +93,24 @@ public class GeneralProjectSettingsElement extends ProjectStructureElement {
final PlaceInProjectStructureBase place =
new PlaceInProjectStructureBase(project, ProjectStructureConfigurable.getInstance(project).createModulesPlace(), this);
final String message;
final String description;
final HtmlChunk description;
if (cycles.size() > 1) {
message = JavaUiBundle.message("circular.dependencies.message");
@NonNls final String br = "<br>&nbsp;&nbsp;&nbsp;&nbsp;";
StringBuilder cyclesString = new StringBuilder();
for (int i = 0; i < cycles.size(); i++) {
cyclesString.append(br).append(i + 1).append(". ").append(cycles.get(i));
}
description = JavaUiBundle.message("module.circular.dependency.warning.description", cyclesString);
final String header = JavaUiBundle.message("module.circular.dependency.warning.description");
final HtmlChunk[] liTags = cycles.stream()
.map(c -> HtmlChunk.tag("li").addText(c))
.toArray(HtmlChunk[]::new);
final HtmlChunk.Element ol = HtmlChunk.tag("ol").style("padding-left: 30pt;")
.children(liTags);
description = new HtmlBuilder()
.append(HtmlChunk.tag("b").addText(header))
.append(ol)
.toFragment()
;
}
else {
message = JavaUiBundle.message("module.circular.dependency.warning.short", StringUtil.decapitalize(cycles.get(0)));

View File

@@ -18,6 +18,7 @@ package com.intellij.openapi.roots.ui.configuration.artifacts;
import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.ConfigurationErrorQuickFix;
import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.ProjectStructureProblemDescription;
import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.ProjectStructureProblemType;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.packaging.elements.PackagingElement;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -31,7 +32,7 @@ public class ArtifactProblemDescription extends ProjectStructureProblemDescripti
public ArtifactProblemDescription(@NotNull @Nls(capitalization = Nls.Capitalization.Sentence) String message, @NotNull ProjectStructureProblemType problemType,
@Nullable List<PackagingElement<?>> pathToPlace, @NotNull PlaceInArtifact place,
final List<ConfigurationErrorQuickFix> quickFixList) {
super(message, null, place, problemType, quickFixList);
super(message, (HtmlChunk) null, place, problemType, quickFixList);
myPathToPlace = pathToPlace;
}

View File

@@ -70,7 +70,7 @@ public class ArtifactValidationManagerImpl implements Disposable {
final List<ProjectStructureProblemDescription> problemDescriptions = holder.getProblemDescriptions();
if (problemDescriptions != null) {
for (ProjectStructureProblemDescription description : problemDescriptions) {
final String message = description.getMessage(false);
final String message = description.getMessage();
List<? extends ConfigurationErrorQuickFix> quickFixes = Collections.emptyList();
if (description instanceof ArtifactProblemDescription) {
final ArtifactProblemDescription artifactProblem = (ArtifactProblemDescription)description;

View File

@@ -19,7 +19,6 @@ 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.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
@@ -65,7 +64,7 @@ public class LibraryProjectStructureElement extends ProjectStructureElement {
@NotNull OrderRootType type, String rootName, final ProjectStructureProblemType problemType) {
final List<String> invalidUrls = library.getInvalidRootUrls(type);
if (!invalidUrls.isEmpty()) {
final String description = createInvalidRootsDescription(invalidUrls, rootName, library.getName());
final HtmlChunk description = createInvalidRootsDescription(invalidUrls, rootName, library.getName());
final PlaceInProjectStructure place = createPlace();
final String message = JavaUiBundle.message("project.roots.error.message.invalid.roots", rootName, invalidUrls.size());
ProjectStructureProblemDescription.ProblemLevel level = library.getTable().getTableLevel().equals(LibraryTablesRegistrar.PROJECT_LEVEL)
@@ -77,7 +76,7 @@ public class LibraryProjectStructureElement extends ProjectStructureElement {
}
}
private static @NlsContexts.DetailedDescription String createInvalidRootsDescription(List<String> invalidClasses, String rootName, @NlsSafe String libraryName) {
private static HtmlChunk createInvalidRootsDescription(List<String> invalidClasses, String rootName, @NlsSafe String libraryName) {
HtmlBuilder buffer = new HtmlBuilder();
final String name = StringUtil.escapeXmlEntities(libraryName);
final HtmlChunk.Element link = HtmlChunk.link("http://library/" + name, name);
@@ -92,7 +91,7 @@ public class LibraryProjectStructureElement extends ProjectStructureElement {
buffer.br().nbsp(2);
buffer.append(PathUtil.toPresentableUrl(url));
}
return buffer.wrapWith("html").toString();
return buffer.toFragment();
}
@NotNull
@@ -147,7 +146,7 @@ public class LibraryProjectStructureElement extends ProjectStructureElement {
@Nls final String result = JavaUiBundle.message("library.0.is.not.used", libraryName);
return new ProjectStructureProblemDescription(XmlStringUtil.wrapInHtml(result),
null,
(HtmlChunk) null,
createPlace(),
ProjectStructureProblemType.unused("unused-library"),
ProjectStructureProblemDescription.ProblemLevel.PROJECT,

View File

@@ -17,7 +17,7 @@ import com.intellij.openapi.roots.ui.configuration.ConfigurationError;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.awt.RelativePoint;
import org.jetbrains.annotations.NotNull;
@@ -44,9 +44,12 @@ public class ProjectConfigurationProblem extends ConfigurationError {
}
}
private static @NlsContexts.DetailedDescription String computeDescription(ProjectStructureProblemDescription description) {
final String descriptionString = description.getDescription();
return descriptionString != null ? descriptionString : description.getMessage(true);
private static HtmlChunk computeDescription(ProjectStructureProblemDescription description) {
if (description.getDescription() == null) {
return HtmlChunk.text(description.getMessage(true));
}
return description.getDescription();
}
@NotNull

View File

@@ -17,6 +17,7 @@ package com.intellij.openapi.roots.ui.configuration.projectRoot.daemon;
import com.intellij.openapi.util.NlsContexts.DetailedDescription;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -28,7 +29,7 @@ public class ProjectStructureProblemDescription {
public enum ProblemLevel {PROJECT, GLOBAL}
@NotNull
private final @Nls(capitalization = Nls.Capitalization.Sentence) String myMessage;
private final @DetailedDescription String myDescription;
private final HtmlChunk myDescription;
private final PlaceInProjectStructure myPlace;
private final List<? extends ConfigurationErrorQuickFix> myFixes;
private final ProjectStructureProblemType myProblemType;
@@ -36,7 +37,7 @@ public class ProjectStructureProblemDescription {
private final boolean myCanShowPlace;
public ProjectStructureProblemDescription(@NotNull @Nls(capitalization = Nls.Capitalization.Sentence) String message,
@Nullable @DetailedDescription String description,
@Nullable HtmlChunk description,
@NotNull PlaceInProjectStructure place,
@NotNull ProjectStructureProblemType problemType,
@NotNull List<? extends ConfigurationErrorQuickFix> fixes) {
@@ -44,7 +45,7 @@ public class ProjectStructureProblemDescription {
}
public ProjectStructureProblemDescription(@NotNull @Nls(capitalization = Nls.Capitalization.Sentence) String message,
@Nullable @DetailedDescription String description,
@Nullable HtmlChunk description,
@NotNull PlaceInProjectStructure place,
@NotNull ProjectStructureProblemType problemType,
@NotNull ProblemLevel level,
@@ -58,10 +59,46 @@ public class ProjectStructureProblemDescription {
myCanShowPlace = canShowPlace;
}
/**
* @deprecated use the constructor with {@link HtmlChunk} for description.
*/
@Deprecated
public ProjectStructureProblemDescription(@NotNull @Nls(capitalization = Nls.Capitalization.Sentence) String message,
@Nullable @DetailedDescription String description,
@NotNull PlaceInProjectStructure place,
@NotNull ProjectStructureProblemType problemType,
@NotNull List<? extends ConfigurationErrorQuickFix> fixes) {
this(message, description, place, problemType, ProblemLevel.PROJECT, fixes, true);
}
/**
* @deprecated use the constructor with {@link HtmlChunk} for description.
*/
@Deprecated
public ProjectStructureProblemDescription(@NotNull @Nls(capitalization = Nls.Capitalization.Sentence) String message,
@Nullable @DetailedDescription String description,
@NotNull PlaceInProjectStructure place,
@NotNull ProjectStructureProblemType problemType,
@NotNull ProblemLevel level,
@NotNull List<? extends ConfigurationErrorQuickFix> fixes, final boolean canShowPlace) {
this(message,
description != null ? HtmlChunk.raw(description) : null,
place,
problemType,
level,
fixes,
canShowPlace);
}
public ProblemLevel getProblemLevel() {
return myProblemLevel;
}
@NotNull
public @Nls(capitalization = Nls.Capitalization.Sentence) String getMessage() {
return myMessage;
}
@NotNull
public @Nls(capitalization = Nls.Capitalization.Sentence) String getMessage(final boolean includePlace) {
if (!includePlace || !myCanShowPlace) return myMessage;
@@ -74,8 +111,7 @@ public class ProjectStructureProblemDescription {
return myCanShowPlace;
}
@Nullable
public @DetailedDescription String getDescription() {
public HtmlChunk getDescription() {
return myDescription;
}