Skip to content

Conversation

@vogella
Copy link
Member

@vogella vogella commented Dec 8, 2025

No description provided.

@gemini-code-assist
Copy link

Summary of Changes

Hello @vogella, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the cyclic dependency detection feature by introducing a visual representation of cycles. Instead of merely listing detected cycles in a pop-up dialog, the tool now leverages the Eclipse Console view to display these cycles as clear, vertical ASCII art flow diagrams. This enhancement aims to provide developers with a more intuitive and actionable understanding of complex dependency structures, with the console automatically appearing to present these detailed insights.

Highlights

  • Enhanced Cycle Visualization: Cyclic dependencies are now visualized using ASCII art directly within the Eclipse Console, providing a clearer and more detailed representation than the previous simple dialog.
  • Dedicated Console Output: A new 'Cyclic Dependency Analysis' console has been introduced to display comprehensive cycle information, including the ASCII art diagrams, replacing the limited pop-up dialog output.
  • Console View Integration: The Eclipse Console view is now automatically brought to the front when cyclic dependencies are detected, ensuring immediate visibility of the detailed analysis.
  • New Dependencies Added: The org.eclipse.ui.console and org.eclipse.ui bundles have been added as required dependencies to enable the new console-based visualization features.
  • Update Site Inclusion: The com.vogella.ide.debugtools bundle has been added to the updatesite/category.xml, making it available for installation via the update site.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new visualization for cyclic dependencies, displaying them in the Eclipse console with ASCII art. The core changes are within DetectCyclicDependenciesHandler.java to implement this new output format. My review focuses on a logical issue in the ASCII art generation that causes redundant output, a potential NullPointerException when trying to display the console, and several areas where code readability and maintainability have been reduced. I've provided suggestions to fix the bugs and improve the code's clarity and robustness.

Comment on lines 141 to 144
IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
String id = IConsoleConstants.ID_CONSOLE_VIEW;
IConsoleView view = (IConsoleView) page.showView(id);
view.display(myConsole);

Choose a reason for hiding this comment

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

high

There is a potential NullPointerException here. PlatformUI.getWorkbench().getActiveWorkbenchWindow() can return null if no workbench window is active, and subsequently getActivePage() can also be called on null or return null. This would crash the handler inside the try-catch block. It's important to add null checks to handle this case gracefully.

            org.eclipse.ui.IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
            if (window == null) {
                Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active window."));
                return;
            }
            IWorkbenchPage page = window.getActivePage();
            if (page == null) {
                Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active page."));
                return;
            }
            String id = IConsoleConstants.ID_CONSOLE_VIEW;
            IConsoleView view = (IConsoleView) page.showView(id);
            view.display(myConsole);

Comment on lines 94 to 115
for (int i = 0; i < cycle.size() - 1; i++) {
String current = cycle.get(i);
String next = cycle.get(i + 1);
String type = cycleInfo.getEdgeType(current, next);

sb.append(horizontalBorder).append("\n");
sb.append(String.format(" | %-" + (boxWidth - 4) + "s |\n", current));
sb.append(horizontalBorder).append("\n");

sb.append(" |\n");
sb.append(" | [").append(type).append("]\n");
sb.append(" v\n");
}

String lastNode = cycle.get(cycle.size() - 1);
sb.append(horizontalBorder).append("\n");
sb.append(String.format(" | %-" + (boxWidth - 4) + "s |\n", lastNode));
sb.append(horizontalBorder).append("\n");

sb.append(" ^ (Loops back to start)\n");
sb.append(" |______________________|\n");

Choose a reason for hiding this comment

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

medium

The current implementation for generating the ASCII art visualization has a flaw. For a cycle of N nodes, it prints N+1 boxes, with the first node being printed twice (once at the beginning and once at the end). This makes the visualization redundant and slightly confusing. The logic should be adjusted to print each unique node in the cycle only once, with the final arrow indicating the loop back to the start.

Suggested change
for (int i = 0; i < cycle.size() - 1; i++) {
String current = cycle.get(i);
String next = cycle.get(i + 1);
String type = cycleInfo.getEdgeType(current, next);
sb.append(horizontalBorder).append("\n");
sb.append(String.format(" | %-" + (boxWidth - 4) + "s |\n", current));
sb.append(horizontalBorder).append("\n");
sb.append(" |\n");
sb.append(" | [").append(type).append("]\n");
sb.append(" v\n");
}
String lastNode = cycle.get(cycle.size() - 1);
sb.append(horizontalBorder).append("\n");
sb.append(String.format(" | %-" + (boxWidth - 4) + "s |\n", lastNode));
sb.append(horizontalBorder).append("\n");
sb.append(" ^ (Loops back to start)\n");
sb.append(" |______________________|\n");
for (int i = 0; i < cycle.size() - 1; i++) {
String current = cycle.get(i);
String next = cycle.get(i + 1);
String type = cycleInfo.getEdgeType(current, next);
sb.append(horizontalBorder).append("\n");
sb.append(String.format(" | %-" + (boxWidth - 4) + "s |\n", current));
sb.append(horizontalBorder).append("\n");
if (i < cycle.size() - 2) {
sb.append(" |\n");
sb.append(" | [").append(type).append("]\n");
sb.append(" v\n");
} else {
sb.append(" |\n");
sb.append(" | [").append(type).append("]\n");
sb.append(" ^ (Loops back to start)\n");
sb.append(" |______________________|\n");
}
}

Comment on lines 126 to 128
for (int i = 0; i < existing.length; i++)
if (name.equals(existing[i].getName()))
return (MessageConsole) existing[i];

Choose a reason for hiding this comment

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

medium

The traditional for loop with an index can be replaced with an enhanced for-each loop. This is more idiomatic in modern Java and improves readability by removing the boilerplate of index management.

Suggested change
for (int i = 0; i < existing.length; i++)
if (name.equals(existing[i].getName()))
return (MessageConsole) existing[i];
for (IConsole console : existing) {
if (name.equals(console.getName())) {
return (MessageConsole) console;
}
}

Comment on lines 298 to 316
private CycleInfo extractCycle(String current, String cycleStart, DependencyEdge finalEdge) {
LinkedList<String> path = new LinkedList<>();
path.addFirst(current); // Start with the node where recursion found cycle

// Reconstruct path from 'current' back to 'cycleStart'
path.addFirst(current);
String node = current;
while (!node.equals(cycleStart)) {
node = parent.get(node);
path.addFirst(node);
}
// Now 'path' is [cycleStart, ..., current]

// Create cycle list for CycleInfo: [cycleStart, ..., current, cycleStart]
List<String> cycleList = new ArrayList<>(path);
cycleList.add(cycleStart); // Close the cycle

cycleList.add(cycleStart);
CycleInfo cycleInfo = new CycleInfo(cycleList);

// Populate edge types for the cycle
// Edges from cycleStart to current
for (int i = 0; i < path.size() - 1; i++) {
String from = path.get(i);
String to = path.get(i + 1);
// The edge that leads to 'to' from 'from'
DependencyEdge edge = parentEdge.get(to);
cycleInfo.addEdge(from, to, edge.type);
}

// The final edge from 'current' back to 'cycleStart'
cycleInfo.addEdge(current, cycleStart, finalEdge.type);

return cycleInfo;

Choose a reason for hiding this comment

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

medium

This method was refactored to be more compact, but in the process, several helpful comments explaining the logic of cycle path reconstruction were removed. While the code is shorter, it's now less self-documenting, making it harder for other developers to understand the algorithm at a glance. Please consider re-adding some of the key comments to improve maintainability. For example, explaining how the path is reconstructed backwards from the current node to the cycleStart node.

Comment on lines 328 to 340
private List<String> normalizeCycle(List<String> cycle) {
if (cycle.size() <= 1) return new ArrayList<>(cycle);

// Remove the duplicate last element for comparison
List<String> temp = new ArrayList<>(cycle.subList(0, cycle.size() - 1));

// Find the minimum element
int minIndex = 0;
for (int i = 1; i < temp.size(); i++) {
if (temp.get(i).compareTo(temp.get(minIndex)) < 0) {
minIndex = i;
}
if (temp.get(i).compareTo(temp.get(minIndex)) < 0) minIndex = i;
}

// Rotate to start with minimum element
List<String> normalized = new ArrayList<>();
for (int i = 0; i < temp.size(); i++) {
normalized.add(temp.get((minIndex + i) % temp.size()));
}
normalized.add(normalized.get(0)); // Add back the duplicate to complete the cycle

normalized.add(normalized.get(0));
return normalized;

Choose a reason for hiding this comment

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

medium

Similar to extractCycle, the explanatory comments in this method have been removed. The logic for normalizing a cycle (finding the lexicographically smallest representation by rotation to avoid duplicate cycle reporting) is not trivial. The removed comments were valuable for understanding the algorithm. Please consider restoring them to aid future maintenance.

- Add null checks for active workbench window/page
- Optimize ASCII art generation to remove redundant node
- Use enhanced for-loop in findConsole
@vogella
Copy link
Member Author

vogella commented Dec 8, 2025

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a great new feature for visualizing cyclic dependencies using the Eclipse console with ASCII art, which is a significant improvement over the previous dialog-based output. The changes also include refactoring of the cycle detection logic. My review focuses on improving the robustness and appearance of the new visualization, ensuring the UI interaction is thread-safe, and suggesting the restoration of some comments in the refactored code to maintain its clarity.

List<String> cycle = cycleInfo.cycle;

int maxLen = 0;
for (String node : cycle) maxLen = Math.max(maxLen, node.length());

Choose a reason for hiding this comment

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

medium

This loop to find the maximum length can be expressed more concisely and efficiently using a Java Stream. This would also naturally handle the case of an empty list, although that's unlikely here.

Suggested change
for (String node : cycle) maxLen = Math.max(maxLen, node.length());
int maxLen = cycle.stream().mapToInt(String::length).max().orElse(0);

sb.append(" |\n");
sb.append(" | [").append(type).append("]\n");
sb.append(" ^ (Loops back to start)\n");
sb.append(" |______________________|\n");

Choose a reason for hiding this comment

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

medium

The ASCII art for the loop-back connection uses a hardcoded string (|______________________|). This will not scale visually if the plug-in names are very long, causing the box width to increase significantly. To make the visualization more robust and look better with varying name lengths, consider making this part of the drawing dynamic based on boxWidth.

Comment on lines +138 to +158
private void showConsoleView(IConsole myConsole) {
try {
org.eclipse.ui.IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (window == null) {
Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active window."));
return;
}
IWorkbenchPage page = window.getActivePage();
if (page == null) {
Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active page."));
return;
}
String id = IConsoleConstants.ID_CONSOLE_VIEW;
IConsoleView view = (IConsoleView) page.showView(id);
view.display(myConsole);
} catch (PartInitException e) {
// Log error if view cannot be opened, but don't fail the whole operation
Platform.getLog(getClass()).log(new Status(Status.WARNING,
"com.vogella.ide.debugtools", "Could not open console view", e));
}
}

Choose a reason for hiding this comment

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

medium

This method interacts with the UI and must be called on the UI thread. While the current usage from the @Execute handler is safe, it's a good practice to make UI-interacting methods robust by wrapping their logic in PlatformUI.getWorkbench().getDisplay().asyncExec(). This prevents potential SWTExceptions if the method is ever called from a background thread in the future.

    private void showConsoleView(IConsole myConsole) {
        PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
            try {
                org.eclipse.ui.IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
                if (window == null) {
                    Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active window."));
                    return;
                }
                IWorkbenchPage page = window.getActivePage();
                if (page == null) {
                    Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active page."));
                    return;
                }
                String id = IConsoleConstants.ID_CONSOLE_VIEW;
                IConsoleView view = (IConsoleView) page.showView(id);
                view.display(myConsole);
            } catch (PartInitException e) {
                // Log error if view cannot be opened, but don't fail the whole operation
                Platform.getLog(getClass()).log(new Status(Status.WARNING, 
                    "com.vogella.ide.debugtools", "Could not open console view", e));
            }
        });
    }

Comment on lines 296 to 349
CycleInfo cycle = extractCycle(node, dep, edge);
if (!isDuplicateCycle(cycle)) {
cycles.add(cycle);
}
}
}
}

recursionStack.remove(node);
}

private CycleInfo extractCycle(String current, String cycleStart, DependencyEdge finalEdge) {
LinkedList<String> path = new LinkedList<>();
path.addFirst(current); // Start with the node where recursion found cycle

// Reconstruct path from 'current' back to 'cycleStart'
path.addFirst(current);
String node = current;
while (!node.equals(cycleStart)) {
node = parent.get(node);
path.addFirst(node);
}
// Now 'path' is [cycleStart, ..., current]

// Create cycle list for CycleInfo: [cycleStart, ..., current, cycleStart]
List<String> cycleList = new ArrayList<>(path);
cycleList.add(cycleStart); // Close the cycle

cycleList.add(cycleStart);
CycleInfo cycleInfo = new CycleInfo(cycleList);

// Populate edge types for the cycle
// Edges from cycleStart to current
for (int i = 0; i < path.size() - 1; i++) {
String from = path.get(i);
String to = path.get(i + 1);
// The edge that leads to 'to' from 'from'
DependencyEdge edge = parentEdge.get(to);
cycleInfo.addEdge(from, to, edge.type);
}

// The final edge from 'current' back to 'cycleStart'
cycleInfo.addEdge(current, cycleStart, finalEdge.type);

return cycleInfo;
}

private boolean isDuplicateCycle(CycleInfo newCycleInfo) {
List<String> normalized = normalizeCycle(newCycleInfo.cycle);

for (CycleInfo existingCycleInfo : cycles) {
List<String> normalizedExisting = normalizeCycle(existingCycleInfo.cycle);
if (normalized.equals(normalizedExisting)) {
return true;
}
if (normalized.equals(normalizedExisting)) return true;
}
return false;
}

private List<String> normalizeCycle(List<String> cycle) {
if (cycle.size() <= 1) return new ArrayList<>(cycle);

// Remove the duplicate last element for comparison
List<String> temp = new ArrayList<>(cycle.subList(0, cycle.size() - 1));

// Find the minimum element
int minIndex = 0;
for (int i = 1; i < temp.size(); i++) {
if (temp.get(i).compareTo(temp.get(minIndex)) < 0) {
minIndex = i;
}
if (temp.get(i).compareTo(temp.get(minIndex)) < 0) minIndex = i;
}

// Rotate to start with minimum element
List<String> normalized = new ArrayList<>();
for (int i = 0; i < temp.size(); i++) {
normalized.add(temp.get((minIndex + i) % temp.size()));
}
normalized.add(normalized.get(0)); // Add back the duplicate to complete the cycle

normalized.add(normalized.get(0));
return normalized;
}

Choose a reason for hiding this comment

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

medium

In several methods within CyclicDependencyDetector, such as extractCycle and normalizeCycle, helpful explanatory comments have been removed. This code performs complex graph traversal and data manipulation, and the comments were important for understanding the algorithms used for cycle detection, extraction, and normalization. Their removal reduces the maintainability of the code. Please consider restoring them to help other developers (and your future self) understand the logic more easily.

@vogella vogella merged commit 48ecb11 into main Dec 8, 2025
1 check passed
@vogella vogella deleted the cycle-visualization branch December 8, 2025 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants