diff options
5 files changed, 357 insertions, 0 deletions
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 @@ public final class LogCatPanel extends SelectionDependentPanel mViewer.setLabelProvider(mLogCatMessageLabelProvider); mViewer.setContentProvider(new LogCatMessageContentProvider()); mViewer.setInput(mReceiver.getMessages()); + + initDoubleClickListener(); } private String getColPreferenceKey(String field) { @@ -660,6 +662,9 @@ public final class LogCatPanel extends SelectionDependentPanel private void refreshFiltersTable() { Display.getDefault().asyncExec(new Runnable() { public void run() { + if (mFiltersTableViewer.getTable().isDisposed()) { + return; + } mFiltersTableViewer.refresh(); } }); @@ -704,4 +709,28 @@ public final class LogCatPanel extends SelectionDependentPanel 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 @@ public class LogCatView extends SelectionDependentViewPart { "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 @@ public class LogCatView extends SelectionDependentViewPart { 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); + } + } } |