blob: 703efc28f7db392a5b28998f5a844092566986ce [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.google.devrel.cluestick.searchservice.EventLog;
import com.appspot.cluestick_server.search.model.CodeResult;
import com.appspot.cluestick_server.search.model.Result;
import com.appspot.cluestick_server.search.model.ResultContext;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.TextAnnotationGutterProvider;
import com.intellij.openapi.editor.colors.EditorFontType;
import com.intellij.openapi.editor.event.SelectionEvent;
import com.intellij.openapi.editor.event.SelectionListener;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.editor.markup.EffectType;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.MarkupModel;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JComponent;
import javax.swing.JPanel;
/**
* CodeBrowser implements Browser to show Cluestick search results containing code.
*/
class CodeBrowser extends JPanel implements Browser, Disposable {
private static final Color HIGHLIGHT_BACKGROUND = UIUtil.getTreeUnfocusedSelectionBackground();
private static final TextAttributes HIGHLIGHT_ATTRIBUTES =
new TextAttributes(null, HIGHLIGHT_BACKGROUND, null, EffectType.SEARCH_MATCH, Font.BOLD);
private static final Pattern WHITESPACE_PREFIX = Pattern.compile("^\\s*");
private final EventLog eventLog;
private final EditorEx editor;
private final Document document;
private final Project project;
private final EditorHighlighterFactory highlighterFactory;
private Result result;
public CodeBrowser(@NotNull Project project, @NotNull EventLog eventLog) {
super(new BorderLayout());
this.project = project;
this.eventLog = eventLog;
highlighterFactory = EditorHighlighterFactory.getInstance();
EditorFactory factory = EditorFactory.getInstance();
document = factory.createDocument("");
editor = (EditorEx) factory.createViewer(document, project);
EditorSettings settings = editor.getSettings();
settings.setLineNumbersShown(true);
settings.setAnimatedScrolling(false);
settings.setRefrainFromScrolling(true);
EditorGutterComponentEx gutter = editor.getGutterComponentEx();
gutter.setShowDefaultGutterPopup(false);
gutter.revalidateMarkup();
editor.getSelectionModel().addSelectionListener(new SelectionListener() {
@Override
public void selectionChanged(SelectionEvent e) {
TextRange range = e.getNewRange();
if (range.isEmpty()) {
return;
}
// TODO(thorogood): event log constants
CodeBrowser.this.eventLog.logEvent("select", getResultKey(), 0);
}
});
// Add the component. Configure the preferred size to zero, as otherwise the Editor itself will
// grow rather than fitting inside its scroll pane.
JComponent component = editor.getComponent();
component.setPreferredSize(new Dimension(0, 0));
add(component, BorderLayout.CENTER);
}
@Override
public void dispose() {
EditorFactory factory = EditorFactory.getInstance();
factory.releaseEditor(editor);
}
@Override
public void showEmpty() {
result = null;
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
document.setText("");
}
});
}
@Override
public void showResult(Result result) {
if (result == null || result.getCode() == null) {
showEmpty();
return;
}
this.result = result;
CodeResult code = result.getCode();
List<CharSequence> lines = new ArrayList<CharSequence>();
int scrollTo = -1;
List<Integer> highlightLines = new ArrayList<Integer>();
List<ResultContext> allContext = code.getContext();
if (allContext != null && !allContext.isEmpty()) {
// If there's only a single result and it's at line one, it's likely the whole file.
// Otherwise, the results contain snippets that can be displayed separately.
boolean containsSnippets = !(allContext.size() == 1 && allContext.get(0).getStartAt() == 1);
for (ResultContext context : allContext) {
List<String> src = context.getSrc();
if (src == null || src.isEmpty()) {
continue;
}
src = formatCode(src);
if (!containsSnippets) {
if (scrollTo == -1 && !context.getResultsAt().isEmpty()) {
scrollTo = context.getResultsAt().get(0);
}
for (int line : context.getResultsAt()) {
highlightLines.add(line - 1);
}
lines.addAll(formatCode(src));
continue;
}
// If this is a snippet, then annotate it as such.
// TODO(thorogood): Different annotations if we ever show other languages (probably just
// Python).
StringBuilder dividerBuilder = new StringBuilder();
int startAt = context.getStartAt();
int endAt = startAt + src.size() - 1;
if (endAt == startAt) {
dividerBuilder.append("// Line ").append(startAt);
} else {
dividerBuilder.append(String.format("// Lines %d-%d", startAt, endAt));
String resultsAt = StringUtil.join(context.getResultsAt(), ", ");
if (!StringUtil.isEmpty(resultsAt)) {
dividerBuilder.append(" (").append(resultsAt).append(')');
}
}
lines.add(dividerBuilder);
for (int line : context.getResultsAt()) {
highlightLines.add(lines.size() + line - context.getStartAt());
}
lines.addAll(formatCode(src));
lines.add("");
}
}
if (scrollTo < 1) {
scrollTo = 1;
}
// LogicalPosition is zero-indexed, but our code is one-indexed.
LogicalPosition position = new LogicalPosition(scrollTo - 1, 0);
setText(lines, position, highlightLines);
String path = ResultUtils.getBaseName(code.getPath());
editor.setHighlighter(highlighterFactory.createEditorHighlighter(project, path));
}
@Override
public JPanel getPanel() {
return this;
}
/**
* @return The result key for the result currently being shown, if available.
*/
@Nullable
private String getResultKey() {
if (result != null) {
return result.getKey();
}
return null;
}
/**
* Sets the text and cursor position in the current {@link EditorEx}.
* @param lines The lines of code to render.
* @param position The position to center on once lines are set.
*/
private void setText(@NotNull List<? extends CharSequence> lines,
@NotNull final LogicalPosition position,
@NotNull final List<Integer> highlight) {
final MarkupModel markup = editor.getMarkupModel();
markup.removeAllHighlighters();
final StringBuilder text = new StringBuilder();
for (CharSequence line : lines) {
text.append(line).append('\n');
}
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
document.setText(text);
editor.getCaretModel().moveToLogicalPosition(position);
editor.getScrollingModel().scrollToCaret(ScrollType.CENTER);
int layer = HighlighterLayer.SELECTION - 1;
for (int line : highlight) {
markup.addLineHighlighter(line, layer, HIGHLIGHT_ATTRIBUTES);
}
editor.getGutterComponentEx().revalidateMarkup();
}
});
}
/**
* Formats code for display. Currently just trims left whitespace.
*
* @param input Source code to format.
* @return Formatted source code.
*/
@NotNull
public static List<String> formatCode(@NotNull List<String> input) {
boolean knownPrefix = false;
String prefix = "";
for (String line : input) {
Matcher m = WHITESPACE_PREFIX.matcher(line);
if (!m.find()) {
throw new IllegalStateException("empty regex should always match");
}
if (m.hitEnd()) {
continue; // blank line
}
String localPrefix = line.substring(0, m.end());
if (!knownPrefix) {
prefix = localPrefix;
knownPrefix = true;
continue;
}
// Find minimum common substring of prefix/localPrefix.
int j = 0;
int len = Math.min(prefix.length(), localPrefix.length());
while (j < len && prefix.charAt(j) == localPrefix.charAt(j)) {
++j;
}
prefix = prefix.substring(0, j);
}
if (prefix.isEmpty()) {
return input;
}
int plen = prefix.length();
List<String> output = new ArrayList<String>(input.size());
for (String src : input) {
if (src.length() < plen) {
output.add("");
} else {
output.add(src.substring(plen));
}
}
return output;
}
}