/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.eclipse.adt.internal.lint; import static com.android.SdkConstants.DOT_JAVA; import static com.android.SdkConstants.DOT_XML; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.preferences.LintPreferencePage; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.tools.lint.detector.api.LintUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IStatusLineManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.preference.IPreferenceNode; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.preference.PreferenceManager; import org.eclipse.jface.preference.PreferenceNode; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IMemento; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IViewSite; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.editors.text.TextFileDocumentProvider; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.texteditor.IDocumentProvider; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Eclipse View which shows lint warnings for the current project */ public class LintViewPart extends ViewPart implements SelectionListener, IJobChangeListener { /** The view id for this view part */ public static final String ID = "com.android.ide.eclipse.adt.internal.lint.LintViewPart"; //$NON-NLS-1$ private static final String QUICKFIX_DISABLED_ICON = "quickfix-disabled"; //$NON-NLS-1$ private static final String QUICKFIX_ICON = "quickfix"; //$NON-NLS-1$ private static final String REFRESH_ICON = "refresh"; //$NON-NLS-1$ private static final String EXPAND_DISABLED_ICON = "expandall-disabled"; //$NON-NLS-1$ private static final String EXPAND_ICON = "expandall"; //$NON-NLS-1$ private static final String COLUMNS_ICON = "columns"; //$NON-NLS-1$ private static final String OPTIONS_ICON = "options"; //$NON-NLS-1$ private static final String IGNORE_THIS_ICON = "ignore-this"; //$NON-NLS-1$ private static final String IGNORE_THIS_DISABLED_ICON = "ignore-this-disabled"; //$NON-NLS-1$ private static final String IGNORE_FILE_ICON = "ignore-file"; //$NON-NLS-1$ private static final String IGNORE_FILE_DISABLED_ICON = "ignore-file-disabled"; //$NON-NLS-1$ private static final String IGNORE_PRJ_ICON = "ignore-project"; //$NON-NLS-1$ private static final String IGNORE_PRJ_DISABLED_ICON = "ignore-project-disabled"; //$NON-NLS-1$ private static final String IGNORE_ALL_ICON = "ignore-all"; //$NON-NLS-1$ private static final String IGNORE_ALL_DISABLED_ICON = "ignore-all-disabled"; //$NON-NLS-1$ private IMemento mMemento; private LintList mLintView; private Text mDetailsText; private Label mErrorLabel; private SashForm mSashForm; private Action mFixAction; private Action mRemoveAction; private Action mIgnoreAction; private Action mAlwaysIgnoreAction; private Action mIgnoreFileAction; private Action mIgnoreProjectAction; private Action mRemoveAllAction; private Action mRefreshAction; private Action mExpandAll; private Action mCollapseAll; private Action mConfigureColumns; private Action mOptions; /** * Initial projects to show: this field is only briefly not null during the * construction initiated by {@link #show(List)} */ private static List<? extends IResource> sInitialResources; /** * Constructs a new {@link LintViewPart} */ public LintViewPart() { } @Override public void init(IViewSite site, IMemento memento) throws PartInitException { super.init(site, memento); mMemento = memento; } @Override public void saveState(IMemento memento) { super.saveState(memento); mLintView.saveState(memento); } @Override public void dispose() { if (mLintView != null) { mLintView.dispose(); mLintView = null; } super.dispose(); } @Override public void createPartControl(Composite parent) { GridLayout gridLayout = new GridLayout(1, false); gridLayout.verticalSpacing = 0; gridLayout.marginWidth = 0; gridLayout.marginHeight = 0; parent.setLayout(gridLayout); mErrorLabel = new Label(parent, SWT.NONE); mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); mSashForm = new SashForm(parent, SWT.NONE); mSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); mLintView = new LintList(getSite(), mSashForm, mMemento, false /*singleFile*/); mDetailsText = new Text(mSashForm, SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL | SWT.MULTI); Display display = parent.getDisplay(); mDetailsText.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); mDetailsText.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); mLintView.addSelectionListener(this); mSashForm.setWeights(new int[] {8, 2}); createActions(); initializeToolBar(); // If there are currently running jobs, listen for them such that we can update the // button state refreshStopIcon(); if (sInitialResources != null) { mLintView.setResources(sInitialResources); sInitialResources = null; } else { // No supplied context: show lint warnings for all projects IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(null); if (androidProjects.length > 0) { List<IResource> projects = new ArrayList<IResource>(); for (IJavaProject project : androidProjects) { projects.add(project.getProject()); } mLintView.setResources(projects); } } updateIssueCount(); } /** * Create the actions. */ private void createActions() { ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); IconFactory iconFactory = IconFactory.getInstance(); mFixAction = new LintViewAction("Fix", ACTION_FIX, iconFactory.getImageDescriptor(QUICKFIX_ICON), iconFactory.getImageDescriptor(QUICKFIX_DISABLED_ICON)); mIgnoreAction = new LintViewAction("Suppress this error with an annotation/attribute", ACTION_IGNORE_THIS, iconFactory.getImageDescriptor(IGNORE_THIS_ICON), iconFactory.getImageDescriptor(IGNORE_THIS_DISABLED_ICON)); mIgnoreFileAction = new LintViewAction("Ignore in this file", ACTION_IGNORE_FILE, iconFactory.getImageDescriptor(IGNORE_FILE_ICON), iconFactory.getImageDescriptor(IGNORE_FILE_DISABLED_ICON)); mIgnoreProjectAction = new LintViewAction("Ignore in this project", ACTION_IGNORE_TYPE, iconFactory.getImageDescriptor(IGNORE_PRJ_ICON), iconFactory.getImageDescriptor(IGNORE_PRJ_DISABLED_ICON)); mAlwaysIgnoreAction = new LintViewAction("Always Ignore", ACTION_IGNORE_ALL, iconFactory.getImageDescriptor(IGNORE_ALL_ICON), iconFactory.getImageDescriptor(IGNORE_ALL_DISABLED_ICON)); mRemoveAction = new LintViewAction("Remove", ACTION_REMOVE, sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVE), sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVE_DISABLED)); mRemoveAllAction = new LintViewAction("Remove All", ACTION_REMOVE_ALL, sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVEALL), sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVEALL_DISABLED)); mRefreshAction = new LintViewAction("Refresh (& Save Files)", ACTION_REFRESH, iconFactory.getImageDescriptor(REFRESH_ICON), null); mRemoveAllAction.setEnabled(true); mCollapseAll = new LintViewAction("Collapse All", ACTION_COLLAPSE, sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL), sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL_DISABLED)); mCollapseAll.setEnabled(true); mExpandAll = new LintViewAction("Expand All", ACTION_EXPAND, iconFactory.getImageDescriptor(EXPAND_ICON), iconFactory.getImageDescriptor(EXPAND_DISABLED_ICON)); mExpandAll.setEnabled(true); mConfigureColumns = new LintViewAction("Configure Columns...", ACTION_COLUMNS, iconFactory.getImageDescriptor(COLUMNS_ICON), null); mOptions = new LintViewAction("Options...", ACTION_OPTIONS, iconFactory.getImageDescriptor(OPTIONS_ICON), null); enableActions(Collections.<IMarker>emptyList(), false /*updateWidgets*/); } /** * Initialize the toolbar. */ private void initializeToolBar() { IToolBarManager toolbarManager = getViewSite().getActionBars().getToolBarManager(); toolbarManager.add(mRefreshAction); toolbarManager.add(mFixAction); toolbarManager.add(mIgnoreAction); toolbarManager.add(mIgnoreFileAction); toolbarManager.add(mIgnoreProjectAction); toolbarManager.add(mAlwaysIgnoreAction); toolbarManager.add(new Separator()); toolbarManager.add(mRemoveAction); toolbarManager.add(mRemoveAllAction); toolbarManager.add(new Separator()); toolbarManager.add(mExpandAll); toolbarManager.add(mCollapseAll); toolbarManager.add(mConfigureColumns); toolbarManager.add(mOptions); } @Override public void setFocus() { mLintView.setFocus(); } /** * Sets the resource associated with the lint view * * @param resources the associated resources */ public void setResources(List<? extends IResource> resources) { mLintView.setResources(resources); // Refresh the stop/refresh icon status refreshStopIcon(); } private void refreshStopIcon() { Job[] currentJobs = LintJob.getCurrentJobs(); if (currentJobs.length > 0) { ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); mRefreshAction.setImageDescriptor(sharedImages.getImageDescriptor( ISharedImages.IMG_ELCL_STOP)); for (Job job : currentJobs) { job.addJobChangeListener(this); } } else { mRefreshAction.setImageDescriptor( IconFactory.getInstance().getImageDescriptor(REFRESH_ICON)); } } // ---- Implements SelectionListener ---- @Override public void widgetSelected(SelectionEvent e) { List<IMarker> markers = mLintView.getSelectedMarkers(); if (markers.size() != 1) { mDetailsText.setText(""); //$NON-NLS-1$ } else { mDetailsText.setText(EclipseLintClient.describe(markers.get(0))); } IStatusLineManager status = getViewSite().getActionBars().getStatusLineManager(); status.setMessage(mDetailsText.getText()); updateIssueCount(); enableActions(markers, true /* updateWidgets */); } private void enableActions(List<IMarker> markers, boolean updateWidgets) { // Update enabled state of actions boolean hasSelection = markers.size() > 0; boolean canFix = hasSelection; for (IMarker marker : markers) { if (!LintFix.hasFix(EclipseLintClient.getId(marker))) { canFix = false; break; } // Some fixes cannot be run in bulk if (markers.size() > 1) { List<LintFix> fixes = LintFix.getFixes(EclipseLintClient.getId(marker), marker); if (fixes == null || !fixes.get(0).isBulkCapable()) { canFix = false; break; } } } boolean haveFile = false; boolean isJavaOrXml = true; for (IMarker marker : markers) { IResource resource = marker.getResource(); if (resource instanceof IFile || resource instanceof IFolder) { haveFile = true; String name = resource.getName(); if (!LintUtils.endsWith(name, DOT_XML) && !LintUtils.endsWith(name, DOT_JAVA)) { isJavaOrXml = false; } break; } } mFixAction.setEnabled(canFix); mIgnoreAction.setEnabled(hasSelection && haveFile && isJavaOrXml); mIgnoreFileAction.setEnabled(hasSelection && haveFile); mIgnoreProjectAction.setEnabled(hasSelection); mAlwaysIgnoreAction.setEnabled(hasSelection); mRemoveAction.setEnabled(hasSelection); if (updateWidgets) { getViewSite().getActionBars().getToolBarManager().update(false); } } @Override public void widgetDefaultSelected(SelectionEvent e) { Object source = e.getSource(); if (source == mLintView.getTreeViewer().getControl()) { // Jump to editor List<IMarker> selection = mLintView.getSelectedMarkers(); if (selection.size() > 0) { EclipseLintClient.showMarker(selection.get(0)); } } } // --- Implements IJobChangeListener ---- @Override public void done(IJobChangeEvent event) { mRefreshAction.setImageDescriptor( IconFactory.getInstance().getImageDescriptor(REFRESH_ICON)); if (!mLintView.isDisposed()) { mLintView.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (!mLintView.isDisposed()) { updateIssueCount(); } } }); } } private void updateIssueCount() { int errors = mLintView.getErrorCount(); int warnings = mLintView.getWarningCount(); mErrorLabel.setText(String.format("%1$d errors, %2$d warnings", errors, warnings)); } @Override public void aboutToRun(IJobChangeEvent event) { } @Override public void awake(IJobChangeEvent event) { } @Override public void running(IJobChangeEvent event) { } @Override public void scheduled(IJobChangeEvent event) { } @Override public void sleeping(IJobChangeEvent event) { } // ---- Actions ---- private static final int ACTION_REFRESH = 1; private static final int ACTION_FIX = 2; private static final int ACTION_IGNORE_THIS = 3; private static final int ACTION_IGNORE_FILE = 4; private static final int ACTION_IGNORE_TYPE = 5; private static final int ACTION_IGNORE_ALL = 6; private static final int ACTION_REMOVE = 7; private static final int ACTION_REMOVE_ALL = 8; private static final int ACTION_COLLAPSE = 9; private static final int ACTION_EXPAND = 10; private static final int ACTION_COLUMNS = 11; private static final int ACTION_OPTIONS = 12; private class LintViewAction extends Action { private final int mAction; private LintViewAction(String label, int action, ImageDescriptor imageDesc, ImageDescriptor disabledImageDesc) { super(label); mAction = action; setImageDescriptor(imageDesc); if (disabledImageDesc != null) { setDisabledImageDescriptor(disabledImageDesc); } } @Override public void run() { switch (mAction) { case ACTION_REFRESH: { IWorkbench workbench = PlatformUI.getWorkbench(); if (workbench != null) { workbench.saveAllEditors(false /*confirm*/); } Job[] jobs = LintJob.getCurrentJobs(); if (jobs.length > 0) { EclipseLintRunner.cancelCurrentJobs(false); } else { List<? extends IResource> resources = mLintView.getResources(); if (resources == null) { return; } Job job = EclipseLintRunner.startLint(resources, null, null, false /*fatalOnly*/, false /*show*/); if (job != null && workbench != null) { job.addJobChangeListener(LintViewPart.this); ISharedImages sharedImages = workbench.getSharedImages(); setImageDescriptor(sharedImages.getImageDescriptor( ISharedImages.IMG_ELCL_STOP)); } } break; } case ACTION_FIX: { List<IMarker> markers = mLintView.getSelectedMarkers(); for (IMarker marker : markers) { List<LintFix> fixes = LintFix.getFixes(EclipseLintClient.getId(marker), marker); if (fixes == null) { continue; } LintFix fix = fixes.get(0); IResource resource = marker.getResource(); if (fix.needsFocus() && resource instanceof IFile) { IRegion region = null; try { int start = marker.getAttribute(IMarker.CHAR_START, -1); int end = marker.getAttribute(IMarker.CHAR_END, -1); if (start != -1) { region = new Region(start, end - start); } AdtPlugin.openFile((IFile) resource, region); } catch (PartInitException e) { AdtPlugin.log(e, "Can't open file %1$s", resource); } } IDocumentProvider provider = new TextFileDocumentProvider(); try { provider.connect(resource); IDocument document = provider.getDocument(resource); if (document != null) { fix.apply(document); if (!fix.needsFocus()) { provider.saveDocument(new NullProgressMonitor(), resource, document, true /*overwrite*/); } } } catch (Exception e) { AdtPlugin.log(e, "Did not find associated editor to apply fix: %1$s", resource.getName()); } finally { provider.disconnect(resource); } } break; } case ACTION_REMOVE: { for (IMarker marker : mLintView.getSelectedMarkers()) { try { marker.delete(); } catch (CoreException e) { AdtPlugin.log(e, null); } } break; } case ACTION_REMOVE_ALL: { List<? extends IResource> resources = mLintView.getResources(); if (resources != null) { for (IResource resource : resources) { EclipseLintClient.clearMarkers(resource); } } break; } case ACTION_IGNORE_ALL: assert false; break; case ACTION_IGNORE_TYPE: case ACTION_IGNORE_FILE: { boolean ignoreInFile = mAction == ACTION_IGNORE_FILE; for (IMarker marker : mLintView.getSelectedMarkers()) { String id = EclipseLintClient.getId(marker); if (id != null) { IResource resource = marker.getResource(); LintFixGenerator.suppressDetector(id, true, ignoreInFile ? resource : resource.getProject(), ignoreInFile); } } break; } case ACTION_IGNORE_THIS: { for (IMarker marker : mLintView.getSelectedMarkers()) { LintFixGenerator.addSuppressAnnotation(marker); } break; } case ACTION_COLLAPSE: { mLintView.collapseAll(); break; } case ACTION_EXPAND: { mLintView.expandAll(); break; } case ACTION_COLUMNS: { mLintView.configureColumns(); break; } case ACTION_OPTIONS: { PreferenceManager manager = new PreferenceManager(); LintPreferencePage page = new LintPreferencePage(); String title = "Default/Global Settings"; page.setTitle(title); IPreferenceNode node = new PreferenceNode(title, page); manager.addToRoot(node); List<? extends IResource> resources = mLintView.getResources(); if (resources != null) { Set<IProject> projects = new HashSet<IProject>(); for (IResource resource : resources) { projects.add(resource.getProject()); } if (projects.size() > 0) { for (IProject project : projects) { page = new LintPreferencePage(); page.setTitle(String.format("Settings for %1$s", project.getName())); page.setElement(project); node = new PreferenceNode(project.getName(), page); manager.addToRoot(node); } } } Shell shell = LintViewPart.this.getSite().getShell(); PreferenceDialog dialog = new PreferenceDialog(shell, manager); dialog.create(); dialog.setSelectedNode(title); dialog.open(); break; } default: assert false : mAction; } updateIssueCount(); } } /** * Shows or reconfigures the LintView to show the lint warnings for the * given project * * @param projects the projects to show lint warnings for */ public static void show(List<? extends IResource> projects) { IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window != null) { IWorkbenchPage page = window.getActivePage(); if (page != null) { try { // Pass initial project context via static field read by constructor sInitialResources = projects; IViewPart view = page.showView(LintViewPart.ID, null, IWorkbenchPage.VIEW_ACTIVATE); if (sInitialResources != null && view instanceof LintViewPart) { // The view must be showing already since the constructor was not // run, so reconfigure the view instead LintViewPart lintView = (LintViewPart) view; lintView.setResources(projects); } } catch (PartInitException e) { AdtPlugin.log(e, "Cannot open Lint View"); } finally { sInitialResources = null; } } } } }