Merge "Goto source when user double clicks on stack trace."
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java
new file mode 100644
index 0000000..6e814b0
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2011 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.android.ddmuilib.logcat;
+
+/**
+ * Classes interested in listening to user selection of logcat
+ * messages should implement this interface.
+ */
+public interface ILogCatMessageSelectionListener {
+    void messageDoubleClicked(LogCatMessage m);
+}
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java
index 9a598c0..83a0e2f 100644
--- a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java
@@ -548,6 +548,8 @@
         mViewer.setLabelProvider(mLogCatMessageLabelProvider);
         mViewer.setContentProvider(new LogCatMessageContentProvider());
         mViewer.setInput(mReceiver.getMessages());
+
+        initDoubleClickListener();
     }
 
     private String getColPreferenceKey(String field) {
@@ -660,6 +662,9 @@
     private void refreshFiltersTable() {
         Display.getDefault().asyncExec(new Runnable() {
             public void run() {
+                if (mFiltersTableViewer.getTable().isDisposed()) {
+                    return;
+                }
                 mFiltersTableViewer.refresh();
             }
         });
@@ -704,4 +709,28 @@
 
         return sb.getSelection() + sb.getThumb() == sb.getMaximum();
     }
+
+    private List<ILogCatMessageSelectionListener> mMessageSelectionListeners;
+
+    private void initDoubleClickListener() {
+        mMessageSelectionListeners = new ArrayList<ILogCatMessageSelectionListener>(1);
+
+        mViewer.getTable().addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetDefaultSelected(SelectionEvent arg0) {
+                List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
+                if (selectedMessages.size() == 0) {
+                    return;
+                }
+
+                for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) {
+                    l.messageDoubleClicked(selectedMessages.get(0));
+                }
+            }
+        });
+    }
+
+    public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) {
+        mMessageSelectionListeners.add(l);
+    }
 }
diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatStackTraceParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatStackTraceParser.java
new file mode 100644
index 0000000..3da9fd0
--- /dev/null
+++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatStackTraceParser.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2011 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.android.ddmuilib.logcat;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class that can determine if a string matches the exception
+ * stack trace pattern, and if so, can provide the java source file
+ * and line where the exception occured.
+ */
+public final class LogCatStackTraceParser {
+    /** Regex to match a stack trace line. E.g.:
+     *          at com.foo.Class.method(FileName.extension:10)
+     *  extension is typically java, but can be anything (java/groovy/scala/..).
+     */
+    private static final String EXCEPTION_LINE_REGEX =
+            "\\s*at\\ (.*)\\((.*)\\..*\\:(\\d+)\\)"; //$NON-NLS-1$
+
+    private static final Pattern EXCEPTION_LINE_PATTERN =
+            Pattern.compile(EXCEPTION_LINE_REGEX);
+
+    /**
+     * Identify if a input line matches the expected pattern
+     * for a stack trace from an exception.
+     */
+    public boolean isValidExceptionTrace(String line) {
+        return EXCEPTION_LINE_PATTERN.matcher(line).find();
+    }
+
+    /**
+     * Get fully qualified method name that threw the exception.
+     * @param line line from the stack trace, must have been validated with
+     * {@link LogCatStackTraceParser#isValidExceptionTrace(String)} before calling this method.
+     * @return fully qualified method name
+     */
+    public String getMethodName(String line) {
+        Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+        m.find();
+        return m.group(1);
+    }
+
+    /**
+     * Get source file name where exception was generated. Input line must be first validated with
+     * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}.
+     */
+    public String getFileName(String line) {
+        Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+        m.find();
+        return m.group(2);
+    }
+
+    /**
+     * Get line number where exception was generated. Input line must be first validated with
+     * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}.
+     */
+    public int getLineNumber(String line) {
+        Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+        m.find();
+        try {
+            return Integer.parseInt(m.group(3));
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+}
diff --git a/ddms/libs/ddmuilib/tests/src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java b/ddms/libs/ddmuilib/tests/src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java
new file mode 100644
index 0000000..7d9869a
--- /dev/null
+++ b/ddms/libs/ddmuilib/tests/src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2011 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.android.ddmuilib.logcat;
+
+import junit.framework.TestCase;
+
+public class LogCatStackTraceParserTest extends TestCase {
+    private LogCatStackTraceParser mTranslator;
+
+    private static final String SAMPLE_METHOD = "com.foo.Class.method"; //$NON-NLS-1$
+    private static final String SAMPLE_FNAME = "FileName";              //$NON-NLS-1$
+    private static final int SAMPLE_LINENUM = 20;
+    private static final String SAMPLE_TRACE =
+            String.format("  at %s(%s.groovy:%d)",                      //$NON-NLS-1$
+                    SAMPLE_METHOD, SAMPLE_FNAME, SAMPLE_LINENUM);
+
+    @Override
+    protected void setUp() throws Exception {
+        mTranslator = new LogCatStackTraceParser();
+    }
+
+    public void testIsValidExceptionTrace() {
+        assertTrue(mTranslator.isValidExceptionTrace(SAMPLE_TRACE));
+        assertFalse(mTranslator.isValidExceptionTrace(
+                "java.lang.RuntimeException: message"));  //$NON-NLS-1$
+        assertFalse(mTranslator.isValidExceptionTrace(
+                "at com.foo.test(Ins.java:unknown)"));    //$NON-NLS-1$
+    }
+
+    public void testGetMethodName() {
+        assertEquals(SAMPLE_METHOD, mTranslator.getMethodName(SAMPLE_TRACE));
+    }
+
+    public void testGetFileName() {
+        assertEquals(SAMPLE_FNAME, mTranslator.getFileName(SAMPLE_TRACE));
+    }
+
+    public void testGetLineNumber() {
+        assertEquals(SAMPLE_LINENUM, mTranslator.getLineNumber(SAMPLE_TRACE));
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java
index 9000cad..1f758bb 100644
--- a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java
@@ -15,19 +15,50 @@
  */
 package com.android.ide.eclipse.ddms.views;
 
+import com.android.ddmuilib.logcat.ILogCatMessageSelectionListener;
+import com.android.ddmuilib.logcat.LogCatMessage;
 import com.android.ddmuilib.logcat.LogCatPanel;
 import com.android.ddmuilib.logcat.LogCatReceiver;
+import com.android.ddmuilib.logcat.LogCatStackTraceParser;
 import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.preferences.PreferenceInitializer;
 
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.search.IJavaSearchConstants;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchMatch;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.core.search.SearchPattern;
+import org.eclipse.jdt.core.search.SearchRequestor;
+import org.eclipse.jface.preference.IPreferenceStore;
 import org.eclipse.swt.layout.FillLayout;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IPerspectiveRegistry;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.WorkbenchException;
+import org.eclipse.ui.ide.IDE;
+
+import java.util.HashMap;
+import java.util.Map;
 
 public class LogCatView extends SelectionDependentViewPart {
     /** LogCatView ID as defined in plugin.xml. */
     public static final String ID = "com.android.ide.eclipse.ddms.views.LogCatView"; //$NON-NLS-1$
 
+    /** Constant indicating that double clicking on a stack trace should
+     * open the method declaration. */
     public static final String CHOICE_METHOD_DECLARATION =
             DdmsPlugin.PLUGIN_ID + ".logcat.MethodDeclaration"; //$NON-NLS-1$
+
+    /** Constant indicating that double clicking on a stack trace should
+     * open the line at which error occurred. */
     public static final String CHOICE_ERROR_LINE =
             DdmsPlugin.PLUGIN_ID + ".logcat.ErrorLine"; //$NON-NLS-1$
 
@@ -39,6 +70,7 @@
             "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$
 
     private LogCatPanel mLogCatPanel;
+    private LogCatStackTraceParser mStackTraceParser = new LogCatStackTraceParser();
 
     @Override
     public void createPartControl(Composite parent) {
@@ -48,9 +80,146 @@
                 DdmsPlugin.getDefault().getPreferenceStore());
         mLogCatPanel.createPanel(parent);
         setSelectionDependentPanel(mLogCatPanel);
+
+        mLogCatPanel.addLogCatMessageSelectionListener(new ILogCatMessageSelectionListener() {
+            public void messageDoubleClicked(LogCatMessage m) {
+                onDoubleClick(m);
+            }
+        });
     }
 
     @Override
     public void setFocus() {
     }
+
+    /**
+     * This class defines what to do with the search match returned by a
+     * double-click or by the Go to Problem action.
+     */
+    private class LogCatViewSearchRequestor extends SearchRequestor {
+        private boolean mFoundFirstMatch = false;
+        private String mChoice;
+        private int mLineNumber;
+
+        public LogCatViewSearchRequestor(String choice, int lineNumber) {
+            super();
+            mChoice = choice;
+            mLineNumber = lineNumber;
+        }
+
+        IMarker createMarkerFromSearchMatch(IFile file, SearchMatch match) {
+            IMarker marker = null;
+            try {
+                if (CHOICE_METHOD_DECLARATION.equals(mChoice)) {
+                    Map<String, Object> attrs = new HashMap<String, Object>();
+                    attrs.put(IMarker.CHAR_START, Integer.valueOf(match.getOffset()));
+                    attrs.put(IMarker.CHAR_END, Integer.valueOf(match.getOffset()
+                            + match.getLength()));
+                    marker = file.createMarker(IMarker.TEXT);
+                    marker.setAttributes(attrs);
+                } else if (CHOICE_ERROR_LINE.equals(mChoice)) {
+                    marker = file.createMarker(IMarker.TEXT);
+                    marker.setAttribute(IMarker.LINE_NUMBER, mLineNumber);
+                }
+            } catch (CoreException e) {
+                Status s = new Status(Status.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e);
+                DdmsPlugin.getDefault().getLog().log(s);
+            }
+            return marker;
+        }
+
+        @Override
+        public void acceptSearchMatch(SearchMatch match) throws CoreException {
+            if (match.getResource() instanceof IFile && !mFoundFirstMatch) {
+                mFoundFirstMatch = true;
+                IFile matchedFile = (IFile) match.getResource();
+                IMarker marker = createMarkerFromSearchMatch(matchedFile, match);
+                // There should only be one exact match,
+                // so we go immediately to that one.
+                if (marker != null) {
+                    switchPerspective();
+                    showMarker(marker);
+                }
+            }
+        }
+    }
+
+    /**
+     * Switch to perspective specified by user when opening a source file.
+     * User preferences control whether the perspective should be switched,
+     * and if so, what the target perspective is.
+     */
+    private void switchPerspective() {
+        IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore();
+        if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) {
+            IWorkbench workbench = PlatformUI.getWorkbench();
+            IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
+            IPerspectiveRegistry perspectiveRegistry = workbench.getPerspectiveRegistry();
+            String perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID);
+            if (perspectiveId != null
+                    && perspectiveId.length() > 0
+                    && perspectiveRegistry.findPerspectiveWithId(perspectiveId) != null) {
+                try {
+                    workbench.showPerspective(perspectiveId, window);
+                } catch (WorkbenchException e) {
+                    Status s = new Status(Status.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e);
+                    DdmsPlugin.getDefault().getLog().log(s);
+                }
+            }
+        }
+    }
+
+    private void showMarker(IMarker marker) {
+        try {
+            IWorkbenchPage page = getViewSite().getWorkbenchWindow()
+                    .getActivePage();
+            if (page != null) {
+                IDE.openEditor(page, marker);
+                marker.delete();
+            }
+        } catch (CoreException e) {
+            Status s = new Status(Status.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e);
+            DdmsPlugin.getDefault().getLog().log(s);
+        }
+    }
+
+    private void onDoubleClick(LogCatMessage m) {
+        String msg = m.getMessage();
+        if (!mStackTraceParser.isValidExceptionTrace(msg)) {
+            return;
+        }
+
+        String methodName = mStackTraceParser.getMethodName(msg);
+        String fileName = mStackTraceParser.getFileName(msg);
+        int lineNumber = mStackTraceParser.getLineNumber(msg);
+
+        IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore();
+        String jumpToLocation = store.getString(PreferenceInitializer.ATTR_LOGCAT_GOTO_PROBLEM);
+
+        String stringPattern = methodName;
+        LogCatViewSearchRequestor requestor =
+                new LogCatViewSearchRequestor(CHOICE_METHOD_DECLARATION, 0);
+        int searchFor = IJavaSearchConstants.METHOD;
+        if (jumpToLocation.equals(CHOICE_ERROR_LINE)) {
+            searchFor = IJavaSearchConstants.CLASS;
+            stringPattern = fileName;
+            requestor = new LogCatViewSearchRequestor(CHOICE_ERROR_LINE, lineNumber);
+        }
+
+        SearchEngine se = new SearchEngine();
+        SearchPattern searchPattern = SearchPattern.createPattern(stringPattern,
+                searchFor,
+                IJavaSearchConstants.DECLARATIONS,
+                SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE);
+        try {
+            se.search(searchPattern,
+                    new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()},
+                    SearchEngine.createWorkspaceScope(),
+                    requestor,
+                    new NullProgressMonitor());
+        } catch (CoreException e) {
+            Status s = new Status(Status.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e);
+            DdmsPlugin.getDefault().getLog().log(s);
+        }
+    }
 }