/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.plugin.ij.debugger;
import com.intellij.debugger.DebuggerBundle;
import com.intellij.debugger.DebuggerInvocationUtil;
import com.intellij.debugger.SourcePosition;
import com.intellij.debugger.engine.DebugProcessEvents;
import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.engine.evaluation.EvaluateException;
import com.intellij.debugger.engine.events.DebuggerContextCommandImpl;
import com.intellij.debugger.impl.*;
import com.intellij.debugger.jdi.StackFrameProxyImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.intellij.debugger.ui.breakpoints.*;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.EventDispatcher;
import com.intellij.util.StringBuilderSpinAllocator;
import com.intellij.xdebugger.impl.actions.ViewBreakpointsAction;
import com.intellij.xdebugger.ui.DebuggerColors;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.LocatableEvent;
import com.sun.jdi.event.MethodEntryEvent;
import gw.plugin.ij.editors.IEmbedsGosuEditors;
import gw.plugin.ij.filetypes.GosuCodeFileType;
import gw.plugin.ij.filetypes.IGosuFileTypeProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.lang.reflect.Field;
import java.util.*;
public class GosuPositionHighlighter {
private static final Key<Boolean> HIGHLIGHTER_USERDATA_KEY = new Key<>("HIGHLIGHTER_USERDATA_KEY");
private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.ui.PositionHighlighter");
private final Project myProject;
private DebuggerContextImpl myContext = DebuggerContextImpl.EMPTY_CONTEXT;
@Nullable
private SelectionDescription mySelectionDescription = null;
@Nullable
private ExecutionPointDescription myExecutionPointDescription = null;
public GosuPositionHighlighter(Project project, @NotNull DebuggerStateManager stateManager) {
myProject = project;
try {
Field fieldDispacther = DebuggerStateManager.class.getDeclaredField("myEventDispatcher");
fieldDispacther.setAccessible(true);
EventDispatcher eventDispatcher = (EventDispatcher) fieldDispacther.get(stateManager);
EventListener toRemove = null;
for(Object l : eventDispatcher.getListeners()) {
if(l.toString().startsWith("com.intellij.debugger.ui.PositionHighlighter")) {
toRemove = (EventListener) l;
break;
}
}
if(toRemove != null) {
eventDispatcher.removeListener(toRemove);
}
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
stateManager.addListener(new DebuggerContextListener() {
public void changeEvent(DebuggerContextImpl newContext, int event) {
myContext = newContext;
if (event != DebuggerSession.EVENT_REFRESH_VIEWS_ONLY && event != DebuggerSession.EVENT_THREADS_REFRESH) {
refresh();
}
}
});
}
private void showLocationInEditor() {
myContext.getDebugProcess().getManagerThread().schedule(new ShowLocationCommand(myContext));
}
private void refresh() {
clearSelections();
final DebuggerSession session = myContext.getDebuggerSession();
if (session != null) {
switch (session.getState()) {
case DebuggerSession.STATE_PAUSED:
if (myContext.getFrameProxy() != null) {
showLocationInEditor();
return;
}
break;
}
}
}
protected static class ExecutionPointDescription extends SelectionDescription {
@Nullable
private RangeHighlighter myHighlighter;
private final int myLineIndex;
protected ExecutionPointDescription(Editor editor, int lineIndex) {
super(editor);
myLineIndex = lineIndex;
}
public void select() {
if (myIsActive) return;
myIsActive = true;
EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
myHighlighter = myEditor.getMarkupModel().addLineHighlighter(
myLineIndex,
DebuggerColors.EXECUTION_LINE_HIGHLIGHTERLAYER,
scheme.getAttributes(DebuggerColors.EXECUTIONPOINT_ATTRIBUTES)
);
myHighlighter.setErrorStripeTooltip(DebuggerBundle.message("position.highlighter.stripe.tooltip"));
myHighlighter.putUserData(HIGHLIGHTER_USERDATA_KEY, Boolean.TRUE);
}
public void remove() {
if (!myIsActive) return;
myIsActive = false;
if (myHighlighter != null) {
myHighlighter.dispose();
myHighlighter = null;
}
}
@Nullable
public RangeHighlighter getHighlighter() {
return myHighlighter;
}
}
protected abstract static class SelectionDescription {
protected final Editor myEditor;
protected boolean myIsActive;
public SelectionDescription(Editor editor) {
myEditor = editor;
}
public abstract void select();
public abstract void remove();
@NotNull
public static ExecutionPointDescription createExecutionPoint(final Editor editor,
final int lineIndex) {
return new ExecutionPointDescription(editor, lineIndex);
}
@NotNull
public static SelectionDescription createSelection(@NotNull final Editor editor, final int lineIndex) {
return new SelectionDescription(editor) {
public void select() {
if (myIsActive) return;
myIsActive = true;
DocumentEx doc = (DocumentEx) editor.getDocument();
editor.getSelectionModel().setSelection(
doc.getLineStartOffset(lineIndex),
doc.getLineEndOffset(lineIndex) + doc.getLineSeparatorLength(lineIndex)
);
}
public void remove() {
if (!myIsActive) return;
myIsActive = false;
myEditor.getSelectionModel().removeSelection();
}
};
}
}
private void showSelection(@NotNull SourcePosition position) {
Editor editor = getEditor(position);
if (editor == null) {
return;
}
if (mySelectionDescription != null) {
mySelectionDescription.remove();
}
mySelectionDescription = SelectionDescription.createSelection(editor, position.getLine());
mySelectionDescription.select();
}
private void showExecutionPoint(@NotNull final SourcePosition position, @NotNull List<Pair<Breakpoint, Event>> events) {
if (myExecutionPointDescription != null) {
myExecutionPointDescription.remove();
}
int lineIndex = position.getLine();
Editor editor = getEditor(position);
if (editor == null) {
return;
}
myExecutionPointDescription = SelectionDescription.createExecutionPoint(editor, lineIndex);
myExecutionPointDescription.select();
RangeHighlighter highlighter = myExecutionPointDescription.getHighlighter();
if (highlighter != null) {
final List<Pair<Breakpoint, Event>> eventsOutOfLine = new ArrayList<>();
for (final Pair<Breakpoint, Event> eventDescriptor : events) {
final Breakpoint breakpoint = eventDescriptor.getFirst();
// filter breakpoints that do not match the event
if (breakpoint instanceof MethodBreakpoint) {
try {
if (!((MethodBreakpoint) breakpoint).matchesEvent((LocatableEvent) eventDescriptor.getSecond(), myContext.getDebugProcess())) {
continue;
}
} catch (EvaluateException ignored) {
}
} else if (breakpoint instanceof WildcardMethodBreakpoint) {
if (!((WildcardMethodBreakpoint) breakpoint).matchesEvent((LocatableEvent) eventDescriptor.getSecond())) {
continue;
}
}
if (breakpoint instanceof BreakpointWithHighlighter) {
if (((BreakpointWithHighlighter) breakpoint).isVisible() && breakpoint.isValid()) {
breakpoint.reload();
final SourcePosition sourcePosition = ((BreakpointWithHighlighter) breakpoint).getSourcePosition();
if (sourcePosition == null || sourcePosition.getLine() != lineIndex) {
eventsOutOfLine.add(eventDescriptor);
}
}
} else {
eventsOutOfLine.add(eventDescriptor);
}
}
if (!eventsOutOfLine.isEmpty()) {
highlighter.setGutterIconRenderer(new MyGutterIconRenderer(eventsOutOfLine));
}
}
}
@Nullable
private Editor getEditor(@NotNull SourcePosition position) {
final PsiFile psiFile = position.getFile();
Document doc = PsiDocumentManager.getInstance(myProject).getDocument(psiFile);
if (!psiFile.isValid()) {
return null;
}
final int lineIndex = position.getLine();
if (lineIndex < 0 || lineIndex > doc.getLineCount()) {
//LOG.assertTrue(false, "Incorrect lineIndex " + lineIndex + " in file " + psiFile.getName());
return null;
}
return openEditor(position, false);
}
@Nullable
public Editor openEditor(@NotNull SourcePosition position, final boolean requestFocus) {
final PsiFile psiFile = position.getFile();
final Project project = psiFile.getProject();
if (project.isDisposed()) {
return null;
}
final VirtualFile virtualFile = psiFile.getVirtualFile();
if (virtualFile == null || !virtualFile.isValid()) {
return null;
}
final int offset = position.getOffset();
if (offset < 0) {
return null;
}
OpenFileDescriptor descriptor = null;
IGosuFileTypeProvider fileTypeProvider = GosuCodeFileType.getFileTypeProvider(virtualFile);
if (fileTypeProvider != null) {
descriptor = fileTypeProvider.getOpenFileDescriptor(project, virtualFile, offset);
}
return openTextEditor(descriptor != null ? descriptor : new OpenFileDescriptor(project, virtualFile, offset), requestFocus);
}
@Nullable
public Editor openTextEditor(@NotNull final OpenFileDescriptor descriptor, final boolean focusEditor) {
FileEditorManager manager = FileEditorManager.getInstance(descriptor.getProject());
final Collection<FileEditor> fileEditors = manager.openEditor(descriptor, focusEditor);
for (FileEditor fileEditor : fileEditors) {
if (fileEditor instanceof IEmbedsGosuEditors) {
return ((IEmbedsGosuEditors) fileEditor).getGosuEditorAt(descriptor);
}
}
return manager.openTextEditor(descriptor,focusEditor);
}
private void clearSelections() {
if (mySelectionDescription != null || myExecutionPointDescription != null) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
public void run() {
if (mySelectionDescription != null) {
mySelectionDescription.remove();
mySelectionDescription = null;
}
if (myExecutionPointDescription != null) {
myExecutionPointDescription.remove();
myExecutionPointDescription = null;
}
}
});
}
}
public void updateContextPointDescription() {
if (myContext.getDebuggerSession() == null) return;
showLocationInEditor();
}
private class ShowLocationCommand extends DebuggerContextCommandImpl {
private final DebuggerContextImpl myContext;
public ShowLocationCommand(DebuggerContextImpl context) {
super(context);
myContext = context;
}
public void threadAction() {
final SourcePosition contextPosition = myContext.getSourcePosition();
if (contextPosition == null) {
return;
}
boolean isExecutionPoint = false;
try {
StackFrameProxyImpl frameProxy = myContext.getFrameProxy();
final ThreadReferenceProxyImpl thread = getSuspendContext().getThread();
isExecutionPoint = thread != null && frameProxy.equals(thread.frame(0));
} catch (Throwable th) {
LOG.debug(th);
}
final List<Pair<Breakpoint, Event>> events = DebuggerUtilsEx.getEventDescriptors(getSuspendContext());
final SourcePosition position = ApplicationManager.getApplication().runReadAction(new Computable<SourcePosition>() {
public SourcePosition compute() {
Document document = PsiDocumentManager.getInstance(myProject).getDocument(contextPosition.getFile());
if (document != null) {
if (contextPosition.getLine() < 0 || contextPosition.getLine() >= document.getLineCount()) {
return SourcePosition.createFromLine(contextPosition.getFile(), 0);
}
}
return contextPosition;
}
});
if (isExecutionPoint) {
DebuggerInvocationUtil.swingInvokeLater(myProject, new Runnable() {
public void run() {
final SourcePosition highlightPosition = getHighlightPosition(events, position);
showExecutionPoint(highlightPosition, events);
}
});
} else {
DebuggerInvocationUtil.swingInvokeLater(myProject, new Runnable() {
public void run() {
showSelection(position);
}
});
}
}
@NotNull
private SourcePosition getHighlightPosition(@NotNull final List<Pair<Breakpoint, Event>> events, @NotNull SourcePosition position) {
for (final Pair<Breakpoint, Event> eventDescriptor : events) {
final Breakpoint breakpoint = eventDescriptor.getFirst();
if (breakpoint instanceof LineBreakpoint) {
breakpoint.reload();
final SourcePosition breakPosition = ((BreakpointWithHighlighter) breakpoint).getSourcePosition();
if (breakPosition != null && breakPosition.getLine() != position.getLine()) {
position = SourcePosition.createFromLine(position.getFile(), breakPosition.getLine());
}
} else if (breakpoint instanceof MethodBreakpoint) {
final MethodBreakpoint methodBreakpoint = (MethodBreakpoint) breakpoint;
methodBreakpoint.reload();
final SourcePosition breakPosition = methodBreakpoint.getSourcePosition();
final LocatableEvent event = (LocatableEvent) eventDescriptor.getSecond();
if (breakPosition != null && breakPosition.getFile().equals(position.getFile()) && breakPosition.getLine() != position.getLine() && event instanceof MethodEntryEvent) {
try {
if (methodBreakpoint.matchesEvent(event, myContext.getDebugProcess())) {
position = SourcePosition.createFromLine(position.getFile(), breakPosition.getLine());
}
} catch (EvaluateException ignored) {
}
}
}
}
return position;
}
}
private class MyGutterIconRenderer extends GutterIconRenderer {
private final List<Pair<Breakpoint, Event>> myEventsOutOfLine;
public MyGutterIconRenderer(List<Pair<Breakpoint, Event>> eventsOutOfLine) {
myEventsOutOfLine = eventsOutOfLine;
}
@NotNull
public Icon getIcon() {
return myEventsOutOfLine.get(0).getFirst().getIcon();
}
public String getTooltipText() {
DebugProcessImpl debugProcess = myContext.getDebugProcess();
if (debugProcess == null) {
return null;
}
final StringBuilder buf = StringBuilderSpinAllocator.alloc();
try {
//noinspection HardCodedStringLiteral
buf.append("<html><body>");
for (Iterator<Pair<Breakpoint, Event>> iterator = myEventsOutOfLine.iterator(); iterator.hasNext(); ) {
Pair<Breakpoint, Event> eventDescriptor = iterator.next();
buf.append(((DebugProcessEvents) debugProcess).getEventText(eventDescriptor));
if (iterator.hasNext()) {
//noinspection HardCodedStringLiteral
buf.append("<br>");
}
}
//noinspection HardCodedStringLiteral
buf.append("</body></html>");
return buf.toString();
} finally {
StringBuilderSpinAllocator.dispose(buf);
}
}
public ActionGroup getPopupMenuActions() {
DefaultActionGroup group = new DefaultActionGroup();
for (Pair<Breakpoint, Event> eventDescriptor : myEventsOutOfLine) {
Breakpoint breakpoint = eventDescriptor.getFirst();
ViewBreakpointsAction viewBreakpointsAction = new ViewBreakpointsAction(breakpoint.getDisplayName(), breakpoint);
group.add(viewBreakpointsAction);
}
return group;
}
@Override
public boolean equals(@NotNull Object obj) {
return obj instanceof MyGutterIconRenderer &&
Comparing.equal(getTooltipText(), ((MyGutterIconRenderer) obj).getTooltipText()) &&
Comparing.equal(getIcon(), ((MyGutterIconRenderer) obj).getIcon());
}
@Override
public int hashCode() {
return getIcon().hashCode();
}
}
}