1 /*******************************************************************************
2 * Copyright (C) 2007, IBM Corporation and others
3 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
4 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
5 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
6 * Copyright (C) 2008, Google Inc.
7 * Copyright (C) 2008, Tor Arne Vestbø <torarnv@gmail.com>
9 * All rights reserved. This program and the accompanying materials
10 * are made available under the terms of the Eclipse Public License v1.0
11 * See LICENSE for the full license text, also available.
12 *******************************************************************************/
14 package org
.spearce
.egit
.ui
.internal
.decorators
;
16 import java
.io
.IOException
;
17 import java
.util
.ArrayList
;
18 import java
.util
.Collections
;
19 import java
.util
.HashMap
;
20 import java
.util
.HashSet
;
21 import java
.util
.List
;
25 import org
.eclipse
.core
.resources
.IProject
;
26 import org
.eclipse
.core
.resources
.IResource
;
27 import org
.eclipse
.core
.resources
.IResourceChangeEvent
;
28 import org
.eclipse
.core
.resources
.IResourceChangeListener
;
29 import org
.eclipse
.core
.resources
.IResourceDelta
;
30 import org
.eclipse
.core
.resources
.IResourceDeltaVisitor
;
31 import org
.eclipse
.core
.resources
.IResourceVisitor
;
32 import org
.eclipse
.core
.resources
.ResourcesPlugin
;
33 import org
.eclipse
.core
.resources
.mapping
.ResourceMapping
;
34 import org
.eclipse
.core
.runtime
.CoreException
;
35 import org
.eclipse
.core
.runtime
.IAdaptable
;
36 import org
.eclipse
.core
.runtime
.IStatus
;
37 import org
.eclipse
.jface
.preference
.IPreferenceStore
;
38 import org
.eclipse
.jface
.resource
.ImageDescriptor
;
39 import org
.eclipse
.jface
.util
.IPropertyChangeListener
;
40 import org
.eclipse
.jface
.util
.PropertyChangeEvent
;
41 import org
.eclipse
.jface
.viewers
.IDecoration
;
42 import org
.eclipse
.jface
.viewers
.ILightweightLabelDecorator
;
43 import org
.eclipse
.jface
.viewers
.LabelProvider
;
44 import org
.eclipse
.jface
.viewers
.LabelProviderChangedEvent
;
45 import org
.eclipse
.osgi
.util
.TextProcessor
;
46 import org
.eclipse
.swt
.graphics
.ImageData
;
47 import org
.eclipse
.swt
.widgets
.Display
;
48 import org
.eclipse
.team
.core
.Team
;
49 import org
.eclipse
.team
.ui
.ISharedImages
;
50 import org
.eclipse
.team
.ui
.TeamImages
;
51 import org
.eclipse
.team
.ui
.TeamUI
;
52 import org
.eclipse
.ui
.IContributorResourceAdapter
;
53 import org
.eclipse
.ui
.PlatformUI
;
54 import org
.spearce
.egit
.core
.GitException
;
55 import org
.spearce
.egit
.core
.internal
.util
.ExceptionCollector
;
56 import org
.spearce
.egit
.core
.project
.GitProjectData
;
57 import org
.spearce
.egit
.core
.project
.RepositoryChangeListener
;
58 import org
.spearce
.egit
.core
.project
.RepositoryMapping
;
59 import org
.spearce
.egit
.ui
.Activator
;
60 import org
.spearce
.egit
.ui
.UIIcons
;
61 import org
.spearce
.egit
.ui
.UIPreferences
;
62 import org
.spearce
.egit
.ui
.UIText
;
63 import org
.spearce
.jgit
.dircache
.DirCache
;
64 import org
.spearce
.jgit
.dircache
.DirCacheIterator
;
65 import org
.spearce
.jgit
.lib
.Constants
;
66 import org
.spearce
.jgit
.lib
.IndexChangedEvent
;
67 import org
.spearce
.jgit
.lib
.ObjectId
;
68 import org
.spearce
.jgit
.lib
.RefsChangedEvent
;
69 import org
.spearce
.jgit
.lib
.Repository
;
70 import org
.spearce
.jgit
.lib
.RepositoryChangedEvent
;
71 import org
.spearce
.jgit
.lib
.RepositoryListener
;
72 import org
.spearce
.jgit
.revwalk
.RevWalk
;
73 import org
.spearce
.jgit
.treewalk
.EmptyTreeIterator
;
74 import org
.spearce
.jgit
.treewalk
.TreeWalk
;
75 import org
.spearce
.jgit
.treewalk
.filter
.PathFilterGroup
;
78 * Supplies annotations for displayed resources
80 * This decorator provides annotations to indicate the status of each resource
81 * when compared to <code>HEAD</code>, as well as the index in the relevant
84 * TODO: Add support for colors and font decoration
86 public class GitLightweightDecorator
extends LabelProvider
implements
87 ILightweightLabelDecorator
, IPropertyChangeListener
,
88 IResourceChangeListener
, RepositoryChangeListener
, RepositoryListener
{
91 * Property constant pointing back to the extension point id of the
94 public static final String DECORATOR_ID
= "org.spearce.egit.ui.internal.decorators.GitLightweightDecorator"; //$NON-NLS-1$
97 * Bit-mask describing interesting changes for IResourceChangeListener
100 private static int INTERESTING_CHANGES
= IResourceDelta
.CONTENT
101 | IResourceDelta
.MOVED_FROM
| IResourceDelta
.MOVED_TO
102 | IResourceDelta
.OPEN
| IResourceDelta
.REPLACED
103 | IResourceDelta
.TYPE
;
106 * Collector for keeping the error view from filling up with exceptions
108 private static ExceptionCollector exceptions
= new ExceptionCollector(
109 UIText
.Decorator_exceptionMessage
, Activator
.getPluginId(),
110 IStatus
.ERROR
, Activator
.getDefault().getLog());
113 * Constructs a new Git resource decorator
115 public GitLightweightDecorator() {
116 TeamUI
.addPropertyChangeListener(this);
117 Activator
.addPropertyChangeListener(this);
118 PlatformUI
.getWorkbench().getThemeManager().getCurrentTheme()
119 .addPropertyChangeListener(this);
120 Repository
.addAnyRepositoryChangedListener(this);
121 GitProjectData
.addRepositoryChangeListener(this);
122 ResourcesPlugin
.getWorkspace().addResourceChangeListener(this,
123 IResourceChangeEvent
.POST_CHANGE
);
129 * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
132 public void dispose() {
134 PlatformUI
.getWorkbench().getThemeManager().getCurrentTheme()
135 .removePropertyChangeListener(this);
136 TeamUI
.removePropertyChangeListener(this);
137 Activator
.removePropertyChangeListener(this);
138 Repository
.removeAnyRepositoryChangedListener(this);
139 GitProjectData
.removeRepositoryChangeListener(this);
140 ResourcesPlugin
.getWorkspace().removeResourceChangeListener(this);
144 * This method should only be called by the decorator thread.
146 * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang.Object,
147 * org.eclipse.jface.viewers.IDecoration)
149 public void decorate(Object element
, IDecoration decoration
) {
150 final IResource resource
= getResource(element
);
151 if (resource
== null)
154 // Don't decorate the workspace root
155 if (resource
.getType() == IResource
.ROOT
)
158 // Don't decorate non-existing resources
159 if (!resource
.exists() && !resource
.isPhantom())
162 // Make sure we're dealing with a project under Git revision control
163 final RepositoryMapping mapping
= RepositoryMapping
164 .getMapping(resource
);
168 // Cannot decorate linked resources
169 if (mapping
.getRepoRelativePath(resource
) == null)
172 // Don't decorate if UI plugin is not running
173 Activator activator
= Activator
.getDefault();
174 if (activator
== null)
178 DecorationHelper helper
= new DecorationHelper(activator
179 .getPreferenceStore());
180 helper
.decorate(decoration
,
181 new DecoratableResourceAdapter(resource
));
182 } catch (IOException e
) {
183 handleException(resource
, GitException
.wrapException(e
));
187 private class DecoratableResourceAdapter
implements IDecoratableResource
{
189 private final IResource resource
;
191 private final RepositoryMapping mapping
;
193 private final Repository repository
;
195 private final ObjectId headId
;
197 private String branch
= "";
199 private boolean tracked
= false;
201 private boolean ignored
= false;
203 public DecoratableResourceAdapter(IResource resourceToWrap
)
205 resource
= resourceToWrap
;
206 mapping
= RepositoryMapping
.getMapping(resource
);
207 repository
= mapping
.getRepository();
208 headId
= repository
.resolve(Constants
.HEAD
);
214 * Initialize the various values that are used for making decoration
215 * decisions later on.
217 * We might as well pre-load these now, instead of using lazy
218 * initialization, because they are all read by the decorator when
219 * building variable bindings and computing the preferred overlay.
221 * @throws IOException
223 private void initializeValues() throws IOException
{
225 // Resolve current branch
226 branch
= repository
.getBranch();
228 // Resolve tracked state
229 if (getType() == IResource
.PROJECT
) {
232 final TreeWalk treeWalk
= new TreeWalk(repository
);
234 Set
<String
> repositoryPaths
= Collections
.singleton(mapping
235 .getRepoRelativePath(resource
));
236 if (!(repositoryPaths
.isEmpty() || repositoryPaths
.contains(""))) {
237 treeWalk
.setFilter(PathFilterGroup
238 .createFromStrings(repositoryPaths
));
239 treeWalk
.setRecursive(treeWalk
.getFilter()
240 .shouldBeRecursive());
244 treeWalk
.addTree(new RevWalk(repository
)
247 treeWalk
.addTree(new EmptyTreeIterator());
249 treeWalk
.addTree(new DirCacheIterator(DirCache
251 if (treeWalk
.next()) {
257 // Resolve ignored state (currently only reads the global Eclipse
259 // TODO: Also read ignores from .git/info/excludes et al.
260 if (Team
.isIgnoredHint(resource
)) {
265 public String
getName() {
266 return resource
.getName();
269 public int getType() {
270 return resource
.getType();
273 public String
getBranch() {
277 public boolean isTracked() {
281 public boolean isIgnored() {
287 * Helper class for doing resource decoration, based on the given
290 * Used for real-time decoration, as well as in the decorator preview
293 public static class DecorationHelper
{
296 public static final String BINDING_RESOURCE_NAME
= "name"; //$NON-NLS-1$
299 public static final String BINDING_BRANCH_NAME
= "branch"; //$NON-NLS-1$
301 private IPreferenceStore store
;
304 * Define a cached image descriptor which only creates the image data
307 private static class CachedImageDescriptor
extends ImageDescriptor
{
308 ImageDescriptor descriptor
;
312 public CachedImageDescriptor(ImageDescriptor descriptor
) {
313 this.descriptor
= descriptor
;
316 public ImageData
getImageData() {
318 data
= descriptor
.getImageData();
324 private static ImageDescriptor trackedImage
;
326 private static ImageDescriptor untrackedImage
;
329 trackedImage
= new CachedImageDescriptor(TeamImages
330 .getImageDescriptor(ISharedImages
.IMG_CHECKEDIN_OVR
));
331 untrackedImage
= new CachedImageDescriptor(UIIcons
.OVR_UNTRACKED
);
335 * Constructs a decorator using the rules from the given
336 * <code>preferencesStore</code>
338 * @param preferencesStore
339 * the preferences store with the preferred decorator rules
341 public DecorationHelper(IPreferenceStore preferencesStore
) {
342 store
= preferencesStore
;
346 * Decorates the given <code>decoration</code> based on the state of the
347 * given <code>resource</code>, using the preferences passed when
348 * constructing this decoration helper.
351 * the decoration to decorate
353 * the resource to retrieve state from
355 public void decorate(IDecoration decoration
,
356 IDecoratableResource resource
) {
357 decorateText(decoration
, resource
);
358 decorateIcons(decoration
, resource
);
361 private void decorateText(IDecoration decoration
,
362 IDecoratableResource resource
) {
364 switch (resource
.getType()) {
367 .getString(UIPreferences
.DECORATOR_FILETEXT_DECORATION
);
369 case IResource
.FOLDER
:
371 .getString(UIPreferences
.DECORATOR_FOLDERTEXT_DECORATION
);
373 case IResource
.PROJECT
:
375 .getString(UIPreferences
.DECORATOR_PROJECTTEXT_DECORATION
);
379 Map
<String
, String
> bindings
= new HashMap
<String
, String
>();
380 bindings
.put(BINDING_RESOURCE_NAME
, resource
.getName());
381 bindings
.put(BINDING_BRANCH_NAME
, resource
.getBranch());
383 decorate(decoration
, format
, bindings
);
386 private void decorateIcons(IDecoration decoration
,
387 IDecoratableResource resource
) {
388 if (resource
.isIgnored())
391 if (resource
.isTracked()) {
392 if (store
.getBoolean(UIPreferences
.DECORATOR_SHOW_TRACKED_ICON
))
393 decoration
.addOverlay(trackedImage
);
395 .getBoolean(UIPreferences
.DECORATOR_SHOW_UNTRACKED_ICON
)) {
396 decoration
.addOverlay(untrackedImage
);
401 * Decorates the given <code>decoration</code>, using the specified text
402 * <code>format</code>, and mapped using the variable bindings from
403 * <code>bindings</code>
406 * the decoration to decorate
408 * the format to base the decoration on
410 * the bindings between variables in the format and actual
413 public static void decorate(IDecoration decoration
, String format
,
415 StringBuffer prefix
= new StringBuffer();
416 StringBuffer suffix
= new StringBuffer();
417 StringBuffer output
= prefix
;
419 int length
= format
.length();
423 if ((end
= format
.indexOf('{', start
)) > -1) {
424 output
.append(format
.substring(start
+ 1, end
));
425 if ((start
= format
.indexOf('}', end
)) > -1) {
426 String key
= format
.substring(end
+ 1, start
);
429 // We use the BINDING_RESOURCE_NAME key to determine if
430 // we are doing the prefix or suffix. The name isn't
431 // actually part of either.
432 if (key
.equals(BINDING_RESOURCE_NAME
)) {
436 s
= (String
) bindings
.get(key
);
442 // Support removing prefix character if binding is
444 int curLength
= output
.length();
446 char c
= output
.charAt(curLength
- 1);
447 if (c
== ':' || c
== '@') {
448 output
.deleteCharAt(curLength
- 1);
453 output
.append(format
.substring(end
, length
));
457 output
.append(format
.substring(start
+ 1, length
));
462 String prefixString
= prefix
.toString().replaceAll("^\\s+", "");
463 if (prefixString
!= null) {
464 decoration
.addPrefix(TextProcessor
.process(prefixString
,
465 "()[].")); //$NON-NLS-1$
467 String suffixString
= suffix
.toString().replaceAll("\\s+$", "");
468 if (suffixString
!= null) {
469 decoration
.addSuffix(TextProcessor
.process(suffixString
,
470 "()[].")); //$NON-NLS-1$
475 // -------- Refresh handling --------
478 * Perform a blanket refresh of all decorations
480 public static void refresh() {
481 Display
.getDefault().asyncExec(new Runnable() {
483 Activator
.getDefault().getWorkbench().getDecoratorManager()
484 .update(DECORATOR_ID
);
490 * Callback for IPropertyChangeListener events
492 * If any of the relevant preferences has been changed we refresh all
493 * decorations (all projects and their resources).
495 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
497 public void propertyChange(PropertyChangeEvent event
) {
498 final String prop
= event
.getProperty();
499 // If the property is of any interest to us
500 if (prop
.equals(TeamUI
.GLOBAL_IGNORES_CHANGED
)
501 || prop
.equals(TeamUI
.GLOBAL_FILE_TYPES_CHANGED
)
502 || prop
.equals(Activator
.DECORATORS_CHANGED
)) {
503 postLabelEvent(new LabelProviderChangedEvent(this, null /* all */));
508 * Callback for IResourceChangeListener events
510 * Schedules a refresh of the changed resource
512 * If the preference for computing deep dirty states has been set we walk
513 * the ancestor tree of the changed resource and update all parents as well.
515 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
517 public void resourceChanged(IResourceChangeEvent event
) {
518 final Set
<IResource
> resourcesToUpdate
= new HashSet
<IResource
>();
520 try { // Compute the changed resources by looking at the delta
521 event
.getDelta().accept(new IResourceDeltaVisitor() {
522 public boolean visit(IResourceDelta delta
) throws CoreException
{
523 final IResource resource
= delta
.getResource();
525 if (resource
.getType() == IResource
.ROOT
) {
526 // Continue with the delta
530 if (resource
.getType() == IResource
.PROJECT
) {
531 // If the project is not accessible, don't process it
532 if (!resource
.isAccessible())
536 // If the file has changed but not in a way that we care
538 // (e.g. marker changes to files) then ignore the change
539 if (delta
.getKind() == IResourceDelta
.CHANGED
540 && (delta
.getFlags() & INTERESTING_CHANGES
) == 0) {
544 // All seems good, schedule the resource for update
545 resourcesToUpdate
.add(resource
);
548 }, true /* includePhantoms */);
549 } catch (final CoreException e
) {
550 handleException(null, e
);
553 // If deep decorator calculation is enabled in the preferences we
554 // walk the ancestor tree of each of the changed resources and add
555 // their parents to the update set
556 final IPreferenceStore store
= Activator
.getDefault()
557 .getPreferenceStore();
558 if (store
.getBoolean(UIPreferences
.DECORATOR_CALCULATE_DIRTY
)) {
559 final IResource
[] changedResources
= resourcesToUpdate
560 .toArray(new IResource
[resourcesToUpdate
.size()]);
561 for (int i
= 0; i
< changedResources
.length
; i
++) {
562 IResource current
= changedResources
[i
];
563 while (current
.getType() != IResource
.ROOT
) {
564 current
= current
.getParent();
565 resourcesToUpdate
.add(current
);
570 postLabelEvent(new LabelProviderChangedEvent(this, resourcesToUpdate
575 * Callback for RepositoryListener events
577 * We resolve the repository mapping for the changed repository and forward
578 * that to repositoryChanged(RepositoryMapping).
581 * The original change event
583 private void repositoryChanged(RepositoryChangedEvent e
) {
584 final Set
<RepositoryMapping
> ms
= new HashSet
<RepositoryMapping
>();
585 for (final IProject p
: ResourcesPlugin
.getWorkspace().getRoot()
587 final RepositoryMapping mapping
= RepositoryMapping
.getMapping(p
);
588 if (mapping
!= null && mapping
.getRepository() == e
.getRepository())
591 for (final RepositoryMapping m
: ms
) {
592 repositoryChanged(m
);
600 * org.spearce.jgit.lib.RepositoryListener#indexChanged(org.spearce.jgit
601 * .lib.IndexChangedEvent)
603 public void indexChanged(IndexChangedEvent e
) {
604 repositoryChanged(e
);
611 * org.spearce.jgit.lib.RepositoryListener#refsChanged(org.spearce.jgit.
612 * lib.RefsChangedEvent)
614 public void refsChanged(RefsChangedEvent e
) {
615 repositoryChanged(e
);
619 * Callback for RepositoryChangeListener events, as well as
620 * RepositoryListener events via repositoryChanged()
622 * We resolve the project and schedule a refresh of each resource in the
625 * @see org.spearce.egit.core.project.RepositoryChangeListener#repositoryChanged(org.spearce.egit.core.project.RepositoryMapping)
627 public void repositoryChanged(RepositoryMapping mapping
) {
628 final IProject project
= mapping
.getContainer().getProject();
632 final List
<IResource
> resources
= new ArrayList
<IResource
>();
634 project
.accept(new IResourceVisitor() {
635 public boolean visit(IResource resource
) {
636 resources
.add(resource
);
640 postLabelEvent(new LabelProviderChangedEvent(this, resources
642 } catch (final CoreException e
) {
643 handleException(project
, e
);
647 // -------- Helper methods --------
649 private static IResource
getResource(Object element
) {
650 if (element
instanceof ResourceMapping
) {
651 element
= ((ResourceMapping
) element
).getModelObject();
654 IResource resource
= null;
655 if (element
instanceof IResource
) {
656 resource
= (IResource
) element
;
657 } else if (element
instanceof IAdaptable
) {
658 final IAdaptable adaptable
= (IAdaptable
) element
;
659 resource
= (IResource
) adaptable
.getAdapter(IResource
.class);
660 if (resource
== null) {
661 final IContributorResourceAdapter adapter
= (IContributorResourceAdapter
) adaptable
662 .getAdapter(IContributorResourceAdapter
.class);
664 resource
= adapter
.getAdaptedResource(adaptable
);
672 * Post the label event to the UI thread
677 private void postLabelEvent(final LabelProviderChangedEvent event
) {
678 Display
.getDefault().asyncExec(new Runnable() {
680 fireLabelProviderChanged(event
);
686 * Handle exceptions that occur in the decorator. Exceptions are only logged
687 * for resources that are accessible (i.e. exist in an open project).
690 * The resource that triggered the exception
692 * The exception that occurred
694 private static void handleException(IResource resource
, CoreException e
) {
695 if (resource
== null || resource
.isAccessible())
696 exceptions
.handleException(e
);