Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,4 @@ integration, telemetry connectivity, and Azure Toolkit integration.
that require it. Otherwise, it is not necessary to authenticate non-Azure Open-AI clients.
Please refer to
the [KeyCredential Class documentation](https://learn.microsoft.com/java/api/com.azure.core.credential.keycredential?view=azure-java-stable)
for more information.
for more information.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these strings are the exact same, including number of spaces so I'm not sure how it's in the diff

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

figured out it comes from the line break so its not problematic

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool;

import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.ui.Gray;
import com.intellij.ui.JBColor;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBPanel;
import com.intellij.openapi.editor.EditorFactory;
import org.jetbrains.annotations.NotNull;

import java.awt.Insets;

import static com.intellij.ui.ColorUtil.toHex;

/**
* This class is used to create a tooltip with a recommendation text and a link to the Azure SDK for Java documentation.
*/
class CustomTooltipOnHover implements LocalQuickFix {

private final String recommendationText;
private final String linkUrl;

/**
* Constructor for CustomTooltipOnHover.
*
* @param recommendationText - the recommendation text to be shown in the tooltip
* @param linkUrl - the URL to be opened when the user clicks on the link in the tooltip
*/
CustomTooltipOnHover(String recommendationText, String linkUrl) {
this.recommendationText = recommendationText;
this.linkUrl = linkUrl;
}

/**
* This method is used to get the name of the quick fix.
*
* @return The name of the quick fix.
*/
@Override
public @NotNull String getName() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be updated in the future to get "the respective quick fix action" lookup?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the "show details" you click on to bring up on the additional info. When I'm working on the action stretch goal I'll find an alternative so the hover popup isn't too busy

return "Show Details";
}

/**
* This method is used to get the family name of the quick fix.
* The family name is used to group similar quick fixes together in the UI.
*
* @return The family name of the quick fix.
*/
@Override
public @NotNull String getFamilyName() {
return getName();
}

/**
* This method is used to display the tooltip when the user clicks on the "Show Details" link in the tooltip.
*
* @param project The project in which the problem was found.
* @param descriptor The descriptor for the problem that was found.
*/
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement element = descriptor.getPsiElement();
showDetailsTooltip(element, project);
}

/**
* This method is used to show a tooltip with the recommendation text and a link to the Azure SDK for Java documentation.
*
* @param element - the PsiElement where the tooltip should be shown
* @param project - the Project object
*/ // separate class for UI configuration & separate styles.css file
private void showDetailsTooltip(@NotNull PsiElement element, @NotNull Project project) {

// Define dynamic text color based on theme
JBColor dynamicTextColor = new JBColor(Gray._50, Gray._176);// Black for light, white for dark theme

// Combine the recommendation text and the link on the same line
String htmlContent = "<html><body style='color: " + toHex(dynamicTextColor) + ";'>" + "<div class='tooltip' role='tooltip' aria-live='polite'>" + recommendationText + " <a href='" + linkUrl + "' class='inline-links' aria-label='Refer to Azure SDK for Java documentation for more information on this suggestion.'>" + "Refer to Azure SDK for Java documentation" + "</a> for more information on this suggestion." + "</div></body></html>";

// Create a panel for the tooltip
JBPanel<JBPanel<?>> panel = QuickFixPanelConfigurations.createPanel(htmlContent);

PsiFile psiFile = element.getContainingFile();
Editor editor = EditorFactory.getInstance().getEditors(psiFile.getViewProvider().getDocument(), project)[0];

// Get the relative position of the element in the editor
RelativePoint relativePoint = new RelativePoint(editor.getContentComponent(), editor.visualPositionToXY(editor.getCaretModel().getVisualPosition()));

// Create a balloon tooltip with custom appearance
// This is specific to Java Swing/AWT components and IntelliJ's UI toolkit. This configuration is not directly related to CSS
// thus, it is not possible to directly store this code snippet in the CSS file.
JBPopupFactory.getInstance().createBalloonBuilder(panel).setFillColor(JBColor.background()) // Dynamic background color
.setBorderColor(JBColor.border()) // Dynamic border color
.setHideOnAction(true).setHideOnClickOutside(true).setHideOnFrameResize(true).setHideOnKeyOutside(true).setBorderInsets(new Insets(0, 0, 0, 0)) // Remove padding
.createBalloon().show(relativePoint, Balloon.Position.above);
}

/**
* This method is used to show a recommendation text with a link to the Azure SDK for Java documentation.
*
* @param recommendationText - the recommendation text to be shown in the tooltip
* @param linkUrl - the URL to be opened when the user clicks on the link in the tooltip
* @return CustomTooltipOnHover object
*/
static CustomTooltipOnHover showRecommendationText(String recommendationText, String linkUrl) {
return new CustomTooltipOnHover(recommendationText, linkUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public void visitElement(@NotNull PsiElement element) {
return;
}
// give the suggestion of the discouraged method
holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessageMap().get(method.getName()));
holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessageMap().get(method.getName()), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get(method.getName()), RULE_CONFIG.getRecommendationLink().get(method.getName())));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void visitTypeElement(PsiTypeElement element) {
String elementType = element.getType().getPresentableText();

// Register a problem if the client used matches a discouraged client
holder.registerProblem(element, RULE_CONFIG.getAntiPatternMessageMap().get(elementType));
holder.registerProblem(element, RULE_CONFIG.getAntiPatternMessageMap().get(elementType), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get(elementType), RULE_CONFIG.getRecommendationLink().get(elementType)));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ private void processVariableDeclaration(PsiVariable variable) {
// Process the new expression initialization
if (!isAutoCompleteDisabled((PsiMethodCallExpression) initializer)) {
// Register a problem if the auto-complete feature is not disabled
holder.registerProblem(initializer, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"));
holder.registerProblem(initializer, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get("recommendationText"), RULE_CONFIG.getRecommendationLink().get("recommendationLink")));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ private void checkClientCreation(PsiStatement blockChild) {

// Check if the right-hand side is a method call expression
if (rhs != null && isClientCreationMethod((PsiMethodCallExpression) rhs)) {
holder.registerProblem(rhs, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"));
holder.registerProblem(rhs, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get("recommendationText"), RULE_CONFIG.getRecommendationLink().get("recommendationLink")));
}
} else if (blockChild instanceof PsiDeclarationStatement) { // This is a check for the declaration statement

Expand All @@ -152,7 +152,7 @@ private void checkClientCreation(PsiStatement blockChild) {
// Check if the initializer is a method call expression
PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression) initializer;
if (isClientCreationMethod(methodCallExpression)) {
holder.registerProblem(methodCallExpression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"));
holder.registerProblem(methodCallExpression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get("recommendationText"), RULE_CONFIG.getRecommendationLink().get("recommendationLink")));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expressio
// Using KeyCredential indicates authentication of a non-Azure OpenAI client
// If the endpoint method is used with KeyCredential for non-Azure OpenAI clients, a warning is registered
if (isUsingKeyCredential(expression)) {
holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"));
holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get("recommendationText"), RULE_CONFIG.getRecommendationLink().get("recommendationLink")));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expressio
boolean isAsyncContext = checkIfAsyncContext(methodCall);

if (isAsyncContext && isAzureClient(methodCall)) {
holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"));
holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get("recommendationText"), RULE_CONFIG.getRecommendationLink().get("recommendationLink")));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void visitElement(@NotNull PsiElement element) {
// Check if the class reference is not null, the qualifier name starts with "com.azure" and
// the class reference is in the list of clients to check
if (newExpression.getClassReference() != null && newExpression.getClassReference().getQualifiedName().startsWith(RuleConfig.AZURE_PACKAGE_NAME) && RULE_CONFIG.getServicesToCheck().contains(classReference)) {
this.holder.registerProblem(newExpression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"));
this.holder.registerProblem(newExpression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get("recommendationText"), RULE_CONFIG.getRecommendationLink().get("recommendationLink")));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ private void checkParameterNames(PsiMethodCallExpressionImpl methodCall) {
}

if (isAzureClient(methodCall)) {
holder.registerProblem(methodCall, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"));
holder.registerProblem(methodCall, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage"), CustomTooltipOnHover.showRecommendationText(RULE_CONFIG.getRecommendationText().get("recommendationText"), RULE_CONFIG.getRecommendationLink().get("recommendationLink")));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool;

import com.intellij.ide.BrowserUtil;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBPanel;
import com.intellij.util.ui.JBUI;

import javax.swing.JEditorPane;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
import java.awt.BorderLayout;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;

/**
* This class is used to create a panel with a specific HTML content.
* The HTML content is styled using the styles.css file.
* One of the uses of this class is to create a tooltip with a recommendation text and a link to the Azure SDK for Java documentation.
*/
class QuickFixPanelConfigurations {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be renamed inline with the CustomHoverTip.
I would rename to HoverTipPanel


private static final Logger LOGGER = Logger.getLogger(QuickFixPanelConfigurations.class.getName());

/**
* This method is used to create a panel with a specific HTML content.
* The HTML content is styled using the styles.css file.
*
* @param htmlContent - the HTML content to be shown in the panel
* @return A panel with the specified HTML content
*/
public static JBPanel<JBPanel<?>> createPanel(String htmlContent) {
// Create a panel for the tooltip
JBPanel<JBPanel<?>> panel = new JBPanel<>();
panel.setBorder(JBUI.Borders.empty(5)); // Add padding

// Create a JEditorPane for both the recommendation text and the link
JEditorPane editorPane = new JEditorPane();
HTMLEditorKit editorKit = new HTMLEditorKit();
editorPane.setEditorKit(editorKit);
StyleSheet styleSheet = editorKit.getStyleSheet();

// Load the CSS from the styles.css file
try (InputStream cssStream = QuickFixPanelConfigurations.class.getResourceAsStream("/META-INF/styles/styles.css")) {

// Check if the CSS file is found
if (cssStream == null) {
throw new FileNotFoundException("CSS file not found.");
}
// Read the CSS file
String css = new String(cssStream.readAllBytes());
styleSheet.addRule(css);
} catch (IOException e) {
LOGGER.warning("Failed to load CSS file: " + e);
}

// Apply a font stack that's likely to match IntelliJ's appearance
editorPane.setContentType("text/html");

// Set the HTML content to the editorPane
editorPane.setText(htmlContent);
editorPane.setEditable(false);
editorPane.setBackground(panel.getBackground());
editorPane.setForeground(JBColor.LIGHT_GRAY);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add comments to where we made acccessibility improvements.
Include in javadocs for reference


// Add hyperlink listener
editorPane.addHyperlinkListener(e -> {
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
BrowserUtil.browse(e.getURL());
}
});

// Add the linkLabel to the panel
panel.add(editorPane, BorderLayout.CENTER);

return panel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ class RuleConfig {
private final Map<String, String> antiPatternMessageMap;
private final List<String> listedItemsToCheck;

private final Map<String, String> recommendationText;
private final Map<String, String> recommendationLink;

static final String AZURE_PACKAGE_NAME = "com.azure";

static final RuleConfig EMPTY_RULE = new RuleConfig(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyList());
static final RuleConfig EMPTY_RULE = new RuleConfig(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyMap());

/**
* Constructor for RuleConfig.
Expand All @@ -27,13 +30,17 @@ class RuleConfig {
* @param servicesToCheck List of services to check.
* @param antiPatternMessageMap Map of antipattern messages to display.
* @param listedItemsToCheck List of items to check for.
* @param recommendationText Map of the Recommendation text for the anti-pattern.
* @param recommendationLink Map of the Recommendation link for the anti-pattern.
*/
RuleConfig(List<String> methodsToCheck, List<String> clientsToCheck, List<String> servicesToCheck, Map<String, String> antiPatternMessageMap, List<String> listedItemsToCheck) {
RuleConfig(List<String> methodsToCheck, List<String> clientsToCheck, List<String> servicesToCheck, Map<String, String> antiPatternMessageMap, List<String> listedItemsToCheck, Map<String, String> recommendationText, Map<String, String> recommendationLink) {
this.methodsToCheck = methodsToCheck;
this.clientsToCheck = clientsToCheck;
this.servicesToCheck = servicesToCheck;
this.antiPatternMessageMap = antiPatternMessageMap;
this.listedItemsToCheck = listedItemsToCheck;
this.recommendationText = recommendationText;
this.recommendationLink = recommendationLink;
}

/**
Expand Down Expand Up @@ -95,4 +102,22 @@ Map<String, String> getAntiPatternMessageMap() {
List<String> getListedItemsToCheck() {
return listedItemsToCheck;
}

/**
* This method returns the recommendation text
*
* @return Recommendation text
*/
public Map<String, String> getRecommendationText() {
return recommendationText;
}

/**
* This method returns the recommendation link
*
* @return Recommendation link
*/
public Map<String, String> getRecommendationLink() {
return recommendationLink;
}
}
Loading