blob: c8f0c67ec02fd6aabe1ff1b8b888bb25750c9523 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.devrel.cluestick.studioclient;
import com.appspot.cluestick_server.search.model.CodeResult;
import com.appspot.cluestick_server.search.model.Result;
import com.intellij.icons.AllIcons;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
/**
* Tree structure containing search results for display in a JTree. Results are displayed as a
* collection of repos. This tree is not expanded by default.
*/
public final class SearchResultsTree extends Tree {
private static final SimpleTextAttributes SUFFIX_ATTRIBUTES = new SimpleTextAttributes(
SimpleTextAttributes.STYLE_ITALIC, UIUtil.getInactiveTextColor());
private final LayoutNode symbolHeading;
private final LayoutNode resultHeading;
public SearchResultsTree(Symbol symbol, List<Result> results) {
super(new DefaultMutableTreeNode());
DefaultMutableTreeNode top = (DefaultMutableTreeNode) getModel().getRoot();
setRootVisible(false);
setCellRenderer(new CellRenderer());
symbolHeading = new LayoutNode();
symbolHeading.text = "Symbol";
symbolHeading.isHeading = true;
top.add(symbolHeading);
LayoutNode symbolNode = new LayoutNode();
if (symbol.packageName != null && !symbol.packageName.isEmpty()) {
symbolNode.prefix = symbol.packageName + ".";
}
symbolNode.icon = AllIcons.Nodes.EjbFinderMethod;
symbolNode.text = symbol.symbolName;
symbolHeading.add(symbolNode);
resultHeading = new LayoutNode();
resultHeading.text = "Found results";
resultHeading.count = results.size();
resultHeading.isHeading = true;
top.add(resultHeading);
addResults(resultHeading, results);
}
/**
* Determines whether the {@link Result} has code.
* @param result The result to check.
* @return Whether the result safely has code.
*/
public static boolean hasCode(Result result) {
return result.getCode() != null && result.getCode().getLines() != null;
}
private void addResults(DefaultMutableTreeNode parent, List<Result> results) {
Map<String, RepoNode> repoNodes = new LinkedHashMap<String, RepoNode>(); // linked provides insert ordering
for (Result result : results) {
if (!hasCode(result)) {
parent.add(new ResultNode(result));
continue;
}
// If this is a code node, then group by repo/branch pair. Retrieve or create the RepoNode
// for this pair, add and update its total count.
CodeResult code = result.getCode();
String key = String.format("%s:%s", code.getRepo(), code.getBranch());
RepoNode repoNode = repoNodes.get(key);
if (repoNode == null) {
repoNode = new RepoNode(code.getRepo(), code.getBranch());
repoNodes.put(key, repoNode);
}
repoNode.add(new ResultNode(result));
}
// Now, add the repo nodes in order of first seen.
for (RepoNode repoNode : repoNodes.values()) {
parent.add(repoNode);
}
}
/**
* RepoNode represents a whole repository (aka, repo/branch).
*/
private static class RepoNode extends DefaultMutableTreeNode implements SearchResultsNode {
private final String repo;
private final String branch;
private final ResultUtils.GitHubInfo info;
RepoNode(String repo, String branch) {
this.repo = repo;
this.branch = branch;
info = ResultUtils.parseRepoName(repo);
}
public Icon getIcon() {
if (info != null) {
return IconFetcher.GitHub;
}
return null;
}
public String getSuffix() {
if (this.branch == null || this.branch.isEmpty() || this.branch.equals("master")) {
return null;
}
return this.branch;
}
public String getPrefix() {
if (info != null) {
return info.user + "/";
}
return null;
}
@Override
public String getURL() {
if (info != null) {
// TODO(thorogood): Deal with non-master branch.
return String.format("https://github.com/%s/%s", info.user, info.repo);
}
return null;
}
@Override
public Result getSearchResult() {
return null;
}
@Override
public String toString() {
if (info != null) {
return info.repo;
}
return this.repo;
}
}
/**
* LayoutNode is a generic node for use inside SearchResultsTree.
*/
private static class LayoutNode extends DefaultMutableTreeNode {
String prefix;
String suffix;
String text;
Icon icon;
boolean isHeading;
int count = -1;
@NotNull
public String getText() {
if (text == null) {
return "?";
}
return text;
}
}
/**
* Node representing a {@link Result}.
*/
public static class ResultNode extends DefaultMutableTreeNode implements SearchResultsNode {
public ResultNode(Result result) {
super(result);
}
@Override
public String getURL() {
return getSearchResult().getUrl();
}
@Override
public Result getSearchResult() {
return (Result) getUserObject();
}
}
private static class CellRenderer extends ColoredTreeCellRenderer {
@Override
public void customizeCellRenderer(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf,
int row, boolean hasFocus) {
if (value instanceof LayoutNode) {
LayoutNode node = (LayoutNode) value;
setIcon(node.icon);
if (node.prefix != null) {
append(node.prefix, SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
}
SimpleTextAttributes defaultAttributes = SimpleTextAttributes.REGULAR_ATTRIBUTES;
if (node.isHeading) {
defaultAttributes = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
}
append(node.getText(), defaultAttributes, true);
if (node.suffix != null) {
append(" " + node.suffix, SUFFIX_ATTRIBUTES, false);
}
if (node.count >= 0) {
String format = " (%d result" + (node.count != 1 ? "s" : "") + ")";
append(String.format(format, node.count), SUFFIX_ATTRIBUTES, false);
}
return;
}
if (value instanceof RepoNode) {
RepoNode node = (RepoNode) value;
setIcon(node.getIcon());
String prefix = node.getPrefix();
if (prefix != null) {
append(prefix, SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
}
append(node.toString(), SimpleTextAttributes.REGULAR_ATTRIBUTES, true);
String suffix = node.getSuffix();
if (suffix != null) {
append(" " + suffix, SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
}
return;
}
if (value instanceof ResultNode) {
ResultNode node = (ResultNode) value;
Result result = node.getSearchResult();
if (!hasCode(result)) {
setIcon(AllIcons.Actions.CreateFromUsage); // "light bulb" icon
append(result.getText());
return;
}
setIcon(AllIcons.Actions.EditSource);
String baseName = ResultUtils.getBaseName(result.getCode().getPath());
append(baseName, SimpleTextAttributes.REGULAR_ATTRIBUTES, true);
int count = result.getCode().getLines().size();
String format = " (%d result" + (count != 1 ? "s" : "") + ")";
append(String.format(format, count), SUFFIX_ATTRIBUTES, false);
return;
}
// This should never happen, but just in case IntelliJ gives us random nodes, then be sure
// to always render something.
append(value.toString());
}
}
}