| /******************************************************************************* |
| * Copyright (c) 2011, 2020 GitHub Inc. and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Kevin Sawicki (GitHub Inc.) - initial API and implementation |
| * Thomas Wolf <thomas.wolf@paranor.ch> - turn it into a real text editor |
| *******************************************************************************/ |
| package org.eclipse.egit.ui.internal.commit; |
| |
| import static java.util.Arrays.asList; |
| import static org.eclipse.egit.ui.UIPreferences.THEME_DiffAddBackgroundColor; |
| import static org.eclipse.egit.ui.UIPreferences.THEME_DiffRemoveBackgroundColor; |
| |
| import java.io.IOException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.Adapters; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.core.runtime.jobs.IJobChangeEvent; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.core.runtime.jobs.JobChangeAdapter; |
| import org.eclipse.egit.core.internal.IRepositoryCommit; |
| import org.eclipse.egit.ui.Activator; |
| import org.eclipse.egit.ui.JobFamilies; |
| import org.eclipse.egit.ui.UIUtils; |
| import org.eclipse.egit.ui.internal.UIText; |
| import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.DiffRegion; |
| import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.FileDiffRegion; |
| import org.eclipse.egit.ui.internal.history.FileDiff; |
| import org.eclipse.egit.ui.internal.repository.RepositoriesView; |
| import org.eclipse.jface.action.Action; |
| import org.eclipse.jface.action.ActionContributionItem; |
| import org.eclipse.jface.action.IAction; |
| import org.eclipse.jface.action.IContributionItem; |
| import org.eclipse.jface.action.IMenuManager; |
| import org.eclipse.jface.operation.IRunnableContext; |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.jface.preference.PreferenceStore; |
| import org.eclipse.jface.resource.ColorRegistry; |
| import org.eclipse.jface.resource.StringConverter; |
| import org.eclipse.jface.text.Document; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.reconciler.IReconciler; |
| import org.eclipse.jface.text.source.Annotation; |
| import org.eclipse.jface.text.source.AnnotationModel; |
| import org.eclipse.jface.text.source.IAnnotationModel; |
| import org.eclipse.jface.text.source.IAnnotationModelExtension; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| import org.eclipse.jface.text.source.IVerticalRuler; |
| import org.eclipse.jface.text.source.IVerticalRulerColumn; |
| import org.eclipse.jface.text.source.projection.ProjectionAnnotation; |
| import org.eclipse.jface.text.source.projection.ProjectionSupport; |
| import org.eclipse.jface.text.source.projection.ProjectionViewer; |
| import org.eclipse.jface.util.IPropertyChangeListener; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.StructuredSelection; |
| import org.eclipse.jface.window.Window; |
| import org.eclipse.jgit.annotations.NonNull; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.jgit.treewalk.filter.TreeFilter; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.RGB; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.team.ui.history.IHistoryView; |
| import org.eclipse.ui.IEditorInput; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.actions.WorkspaceModifyOperation; |
| import org.eclipse.ui.contexts.IContextService; |
| import org.eclipse.ui.dialogs.SaveAsDialog; |
| import org.eclipse.ui.editors.text.EditorsUI; |
| import org.eclipse.ui.editors.text.FileDocumentProvider; |
| import org.eclipse.ui.editors.text.TextEditor; |
| import org.eclipse.ui.part.FileEditorInput; |
| import org.eclipse.ui.part.IShowInSource; |
| import org.eclipse.ui.part.IShowInTargetList; |
| import org.eclipse.ui.part.ShowInContext; |
| import org.eclipse.ui.progress.IProgressService; |
| import org.eclipse.ui.progress.UIJob; |
| import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; |
| import org.eclipse.ui.texteditor.ChainedPreferenceStore; |
| import org.eclipse.ui.texteditor.IDocumentProvider; |
| import org.eclipse.ui.texteditor.ITextEditorActionConstants; |
| import org.eclipse.ui.themes.IThemeManager; |
| import org.eclipse.ui.views.contentoutline.IContentOutlinePage; |
| |
| /** |
| * A {@link TextEditor} specialized to showing a unified diff of a whole commit. |
| * The editor has an associated {@link DiffEditorOutlinePage}. |
| */ |
| public class DiffEditor extends TextEditor |
| implements IShowInSource, IShowInTargetList { |
| |
| /** External ID used in plugin.xml for this stand-alone editor. */ |
| public static final String EDITOR_ID = "org.eclipse.egit.ui.diffEditor"; //$NON-NLS-1$ |
| |
| private static final String CONTEXT_ID = "org.eclipse.egit.ui.DiffViewer"; //$NON-NLS-1$ |
| |
| private static final String ADD_ANNOTATION_TYPE = "org.eclipse.egit.ui.commitEditor.diffAdded"; //$NON-NLS-1$ |
| |
| private static final String REMOVE_ANNOTATION_TYPE = "org.eclipse.egit.ui.commitEditor.diffRemoved"; //$NON-NLS-1$ |
| |
| private static final String QUICK_OUTLINE_COMMAND = "org.eclipse.egit.ui.commit.DiffEditorQuickOutlineCommand"; //$NON-NLS-1$ |
| |
| private DiffEditorOutlinePage outlinePage; |
| |
| private Annotation[] currentFoldingAnnotations; |
| |
| private Annotation[] currentOverviewAnnotations; |
| |
| /** An {@link IPreferenceStore} for the annotation colors.*/ |
| private ThemePreferenceStore overviewStore; |
| |
| private FileDiffRegion currentFileDiffRange; |
| |
| private OldNewLogicalLineNumberRulerColumn lineNumberColumn; |
| |
| private boolean plainLineNumbers = false; |
| |
| /** |
| * Creates a new {@link DiffEditor}. |
| */ |
| public DiffEditor() { |
| setPartName(UIText.DiffEditor_Title); |
| } |
| |
| @Override |
| public <T> T getAdapter(Class<T> adapter) { |
| if (IContentOutlinePage.class.equals(adapter)) { |
| if (outlinePage == null) { |
| outlinePage = createOutlinePage(); |
| outlinePage.setInput( |
| getDocumentProvider().getDocument(getEditorInput())); |
| if (currentFileDiffRange != null) { |
| outlinePage.setSelection( |
| new StructuredSelection(currentFileDiffRange)); |
| } |
| } |
| return adapter.cast(outlinePage); |
| } |
| return super.getAdapter(adapter); |
| } |
| |
| private DiffEditorOutlinePage createOutlinePage() { |
| DiffEditorOutlinePage page = new DiffEditorOutlinePage(); |
| page.addSelectionChangedListener( |
| event -> setSelectionAndActivate(event.getSelection(), false)); |
| page.addOpenListener( |
| event -> setSelectionAndActivate(event.getSelection(), true)); |
| return page; |
| } |
| |
| /** |
| * Retrieves the editor's outline page. |
| * |
| * @return this editor's outline page, or {@code null} if none was created |
| */ |
| protected DiffEditorOutlinePage getOutlinePage() { |
| return outlinePage; |
| } |
| |
| /** |
| * Sets the selection and potentially activates the editor. |
| * |
| * @param selection |
| * to set |
| * @param activate |
| * whether to activate the editor |
| */ |
| protected void setSelectionAndActivate(ISelection selection, |
| boolean activate) { |
| if (activate) { |
| getSite().getPage().activate(this); |
| } |
| doSetSelection(selection); |
| } |
| |
| @Override |
| public void dispose() { |
| if (overviewStore != null) { |
| overviewStore.dispose(); |
| overviewStore = null; |
| } |
| super.dispose(); |
| } |
| |
| // TextEditor specifics: |
| |
| @Override |
| public boolean isSaveAsAllowed() { |
| IDocumentProvider provider = getDocumentProvider(); |
| return provider != null && provider |
| .getDocument(getEditorInput()) instanceof DiffDocument; |
| } |
| |
| @Override |
| protected void performSaveAs(IProgressMonitor progressMonitor) { |
| SaveAsDialog dialog = new SaveAsDialog(getSite().getShell()); |
| IPath path = dialog.open() == Window.CANCEL ? null : dialog.getResult(); |
| |
| if (path == null) { |
| if (progressMonitor != null) { |
| progressMonitor.setCanceled(true); |
| } |
| return; |
| } |
| |
| IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(path); |
| IEditorInput newInput = new FileEditorInput(file); |
| |
| WorkspaceModifyOperation op = new WorkspaceModifyOperation() { |
| |
| @Override |
| public void execute(final IProgressMonitor monitor) |
| throws CoreException { |
| IDocumentProvider provider = getDocumentProvider(); |
| if (provider != null) { |
| provider.saveDocument(monitor, newInput, |
| provider.getDocument(getEditorInput()), true); |
| } |
| } |
| }; |
| |
| try { |
| getSite().getService(IProgressService.class).busyCursorWhile(op); |
| } catch (InterruptedException e) { |
| // Cancelled. |
| } catch (InvocationTargetException e) { |
| Activator.handleError(UIText.DiffEditor_SaveError, e.getCause(), |
| true); |
| } |
| } |
| |
| @Override |
| protected ISourceViewer createSourceViewer(Composite parent, |
| IVerticalRuler ruler, int styles) { |
| DiffViewer viewer = new DiffViewer(parent, ruler, getOverviewRuler(), |
| isOverviewRulerVisible(), styles, |
| getSite().getService(IContextService.class)) { |
| @Override |
| protected void setFont(Font font) { |
| // Don't do anything; AbstractTextEditor handles this. |
| } |
| }; |
| getSourceViewerDecorationSupport(viewer); |
| ProjectionSupport projector = new ProjectionSupport(viewer, |
| getAnnotationAccess(), getSharedColors()); |
| projector.install(); |
| viewer.getTextWidget().addCaretListener(event -> { |
| if (outlinePage != null) { |
| FileDiffRegion region = getFileDiffRange(event.caretOffset); |
| if (region != null && !region.equals(currentFileDiffRange)) { |
| currentFileDiffRange = region; |
| outlinePage.setSelection(new StructuredSelection(region)); |
| } else { |
| currentFileDiffRange = region; |
| } |
| } |
| }); |
| return viewer; |
| } |
| |
| @Override |
| protected void initializeKeyBindingScopes() { |
| setKeyBindingScopes(new String[] { CONTEXT_ID }); |
| } |
| |
| @Override |
| protected IVerticalRulerColumn createLineNumberRulerColumn() { |
| lineNumberColumn = new OldNewLogicalLineNumberRulerColumn( |
| plainLineNumbers); |
| initializeLineNumberRulerColumn(lineNumberColumn); |
| return lineNumberColumn; |
| } |
| |
| @Override |
| protected void initializeEditor() { |
| super.initializeEditor(); |
| overviewStore = new ThemePreferenceStore(); |
| setPreferenceStore(new ChainedPreferenceStore(new IPreferenceStore[] { |
| overviewStore, EditorsUI.getPreferenceStore() })); |
| setDocumentProvider(new DiffDocumentProvider()); |
| setSourceViewerConfiguration( |
| new DiffViewer.Configuration(getPreferenceStore()) { |
| @Override |
| public IReconciler getReconciler( |
| ISourceViewer sourceViewer) { |
| // Switch off spell-checking |
| return null; |
| } |
| }); |
| } |
| |
| @Override |
| protected void doSetInput(IEditorInput input) throws CoreException { |
| super.doSetInput(input); |
| currentFileDiffRange = null; |
| if (input instanceof DiffEditorInput) { |
| DiffEditorInput diffInput = (DiffEditorInput) input; |
| if (diffInput.getDocument() != null) { |
| setFolding(); |
| FileDiffRegion region = getFileDiffRange(0); |
| currentFileDiffRange = region; |
| setOverviewAnnotations(); |
| } else { |
| formatDiff(diffInput); |
| } |
| } |
| if (outlinePage != null) { |
| outlinePage.setInput(getDocumentProvider().getDocument(input)); |
| if (currentFileDiffRange != null) { |
| outlinePage.setSelection( |
| new StructuredSelection(currentFileDiffRange)); |
| } |
| } |
| } |
| |
| @Override |
| protected void doSetSelection(ISelection selection) { |
| if (!selection.isEmpty() && selection instanceof StructuredSelection) { |
| Object selected = ((StructuredSelection) selection) |
| .getFirstElement(); |
| if (selected instanceof FileDiffRegion) { |
| FileDiffRegion newRange = (FileDiffRegion) selected; |
| if (!newRange.equals(currentFileDiffRange)) { |
| currentFileDiffRange = newRange; |
| selectAndReveal(newRange.getOffset(), 0); |
| if (outlinePage != null) { |
| outlinePage.setSelection( |
| new StructuredSelection(selected)); |
| } |
| } |
| return; |
| } |
| } |
| super.doSetSelection(selection); |
| } |
| |
| @Override |
| protected void editorContextMenuAboutToShow(IMenuManager menu) { |
| super.editorContextMenuAboutToShow(menu); |
| addAction(menu, ITextEditorActionConstants.GROUP_COPY, |
| ITextEditorActionConstants.SELECT_ALL); |
| // TextEditor always adds these, even if the document is not editable. |
| menu.remove(ITextEditorActionConstants.SHIFT_RIGHT); |
| menu.remove(ITextEditorActionConstants.SHIFT_LEFT); |
| menu.appendToGroup(ITextEditorActionConstants.GROUP_OPEN, |
| getAction(QUICK_OUTLINE_COMMAND)); |
| } |
| |
| @Override |
| protected void createActions() { |
| super.createActions(); |
| createQuickOutlineAction(); |
| } |
| |
| private void createQuickOutlineAction() { |
| Action outlineAction = new Action( |
| UIText.DiffEditor_QuickOutlineAction) { |
| @Override |
| public void run() { |
| DiffEditorOutlinePage.openQuickOutline( |
| getDocumentProvider().getDocument(getEditorInput()), |
| getEditorSite().getSelectionProvider()); |
| } |
| }; |
| outlineAction.setActionDefinitionId(QUICK_OUTLINE_COMMAND); |
| setAction(outlineAction.getActionDefinitionId(), outlineAction); |
| } |
| |
| @Override |
| protected void rulerContextMenuAboutToShow(IMenuManager menu) { |
| super.rulerContextMenuAboutToShow(menu); |
| // AbstractDecoratedTextEditor's menu presumes a |
| // LineNumberChangeRulerColumn, which we don't have. |
| IContributionItem showLineNumbers = menu |
| .find(ITextEditorActionConstants.LINENUMBERS_TOGGLE); |
| boolean isShowingLineNumbers = EditorsUI.getPreferenceStore() |
| .getBoolean( |
| AbstractDecoratedTextEditorPreferenceConstants.EDITOR_LINE_NUMBER_RULER); |
| if (showLineNumbers instanceof ActionContributionItem) { |
| ((ActionContributionItem) showLineNumbers).getAction() |
| .setChecked(isShowingLineNumbers); |
| } |
| if (isShowingLineNumbers) { |
| // Add an action to toggle between physical and logical line numbers |
| boolean plain = lineNumberColumn.isPlain(); |
| IAction togglePlain = new Action( |
| UIText.DiffEditor_ToggleLineNumbers, |
| IAction.AS_CHECK_BOX) { |
| |
| @Override |
| public void run() { |
| plainLineNumbers = !plain; |
| lineNumberColumn.setPlain(!plain); |
| } |
| }; |
| togglePlain.setChecked(!plain); |
| menu.appendToGroup(ITextEditorActionConstants.GROUP_RULERS, |
| togglePlain); |
| } |
| } |
| |
| // "Show In..." specifics: |
| |
| @Override |
| public ShowInContext getShowInContext() { |
| IRepositoryCommit commit = Adapters.adapt(getEditorInput(), |
| IRepositoryCommit.class); |
| if (commit != null) { |
| return new ShowInContext(getEditorInput(), |
| new StructuredSelection(commit)); |
| } |
| return null; |
| } |
| |
| @Override |
| public String[] getShowInTargetIds() { |
| return new String[] { IHistoryView.VIEW_ID, RepositoriesView.VIEW_ID }; |
| } |
| |
| // Diff specifics: |
| |
| private void setFolding() { |
| ProjectionViewer viewer = (ProjectionViewer) getSourceViewer(); |
| if (viewer == null) { |
| return; |
| } |
| IDocument document = viewer.getDocument(); |
| if (document instanceof DiffDocument) { |
| FileDiffRegion[] regions = ((DiffDocument) document) |
| .getFileRegions(); |
| if (regions == null || regions.length <= 1) { |
| viewer.disableProjection(); |
| return; |
| } |
| viewer.enableProjection(); |
| Map<Annotation, Position> newAnnotations = new HashMap<>(); |
| for (FileDiffRegion region : regions) { |
| newAnnotations.put(new ProjectionAnnotation(), |
| new Position(region.getOffset(), region.getLength())); |
| } |
| viewer.getProjectionAnnotationModel().modifyAnnotations( |
| currentFoldingAnnotations, newAnnotations, null); |
| currentFoldingAnnotations = newAnnotations.keySet() |
| .toArray(new Annotation[0]); |
| } else { |
| viewer.disableProjection(); |
| currentFoldingAnnotations = null; |
| } |
| } |
| |
| private void setOverviewAnnotations() { |
| IDocumentProvider documentProvider = getDocumentProvider(); |
| IDocument document = documentProvider.getDocument(getEditorInput()); |
| if (!(document instanceof DiffDocument)) { |
| return; |
| } |
| IAnnotationModel annotationModel = documentProvider |
| .getAnnotationModel(getEditorInput()); |
| if (annotationModel == null) { |
| return; |
| } |
| DiffRegion[] diffs = ((DiffDocument) document).getRegions(); |
| if (diffs == null || diffs.length == 0) { |
| return; |
| } |
| Map<Annotation, Position> newAnnotations = new HashMap<>(); |
| for (DiffRegion region : diffs) { |
| if (DiffRegion.Type.ADD.equals(region.getType())) { |
| newAnnotations.put( |
| new Annotation(ADD_ANNOTATION_TYPE, true, null), |
| new Position(region.getOffset(), region.getLength())); |
| } else if (DiffRegion.Type.REMOVE.equals(region.getType())) { |
| newAnnotations.put( |
| new Annotation(REMOVE_ANNOTATION_TYPE, true, null), |
| new Position(region.getOffset(), region.getLength())); |
| } |
| } |
| if (annotationModel instanceof IAnnotationModelExtension) { |
| ((IAnnotationModelExtension) annotationModel).replaceAnnotations( |
| currentOverviewAnnotations, newAnnotations); |
| } else { |
| if (currentOverviewAnnotations != null) { |
| for (Annotation existing : currentOverviewAnnotations) { |
| annotationModel.removeAnnotation(existing); |
| } |
| } |
| for (Map.Entry<Annotation, Position> entry : newAnnotations |
| .entrySet()) { |
| annotationModel.addAnnotation(entry.getKey(), entry.getValue()); |
| } |
| } |
| currentOverviewAnnotations = newAnnotations.keySet() |
| .toArray(new Annotation[0]); |
| } |
| |
| private FileDiffRegion getFileDiffRange(int widgetOffset) { |
| DiffViewer viewer = (DiffViewer) getSourceViewer(); |
| if (viewer != null) { |
| int offset = viewer.widgetOffset2ModelOffset(widgetOffset); |
| IDocument document = getDocumentProvider() |
| .getDocument(getEditorInput()); |
| if (document instanceof DiffDocument) { |
| return ((DiffDocument) document).findFileRegion(offset); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Asynchronously gets the diff of the commit set on our |
| * {@link CommitEditorInput}, formats it into a {@link DiffDocument}, and |
| * then re-sets this editors's input to a {@link DiffEditorInput} which will |
| * cause this document to be shown. |
| * |
| * @param input |
| * of this editor |
| */ |
| private void formatDiff(@NonNull DiffEditorInput input) { |
| IRepositoryCommit commit = input.getTip(); |
| IRepositoryCommit base = input.getBase(); |
| if (base == null && commit.getRevCommit().getParentCount() > 1) { |
| if (!(commit instanceof RepositoryCommit) |
| || !((RepositoryCommit) commit).isStash()) { |
| input.setDocument(new Document()); |
| setInput(input); |
| return; |
| } |
| } |
| DiffJob job = getDiffer(commit, base); |
| job.addJobChangeListener(new JobChangeAdapter() { |
| @Override |
| public void done(IJobChangeEvent event) { |
| if (!event.getResult().isOK()) { |
| return; |
| } |
| new UIJob(UIText.DiffEditor_TaskUpdatingViewer) { |
| |
| @Override |
| public IStatus runInUIThread(IProgressMonitor uiMonitor) { |
| if (UIUtils |
| .isUsable(getSourceViewer().getTextWidget())) { |
| input.setDocument(job.getDocument()); |
| setInput(input); |
| } |
| return Status.OK_STATUS; |
| } |
| }.schedule(); |
| } |
| }); |
| job.schedule(); |
| } |
| |
| /** |
| * A {@link Job} computing a diff. |
| */ |
| public static abstract class DiffJob extends Job { |
| |
| private DiffDocument document; |
| |
| /** |
| * Creates a new {@link DiffJob}. |
| */ |
| protected DiffJob() { |
| super(UIText.DiffEditor_TaskGeneratingDiff); |
| } |
| |
| /** |
| * @return the final {@link DiffDocument} |
| */ |
| public DiffDocument getDocument() { |
| return document; |
| } |
| |
| /** |
| * Sets the final document. |
| * |
| * @param document |
| * to set |
| */ |
| public void setDocument(DiffDocument document) { |
| this.document = document; |
| } |
| } |
| |
| /** |
| * Obtains a {@link DiffJob} computing the diff between the two commits, or |
| * between the {@code tip} and its parent. |
| * |
| * @param tip |
| * commit to diff |
| * @param base |
| * commit to diff against; may be {@code null} |
| * @return the job |
| */ |
| public static DiffJob getDiffer(@NonNull IRepositoryCommit tip, |
| IRepositoryCommit base) { |
| return new DiffJob() { |
| |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| SubMonitor progress = SubMonitor.convert(monitor, 2); |
| FileDiff diffs[] = getDiffs(progress.newChild(1)); |
| setDocument(formatDiffs(diffs, progress.newChild(1))); |
| return Status.OK_STATUS; |
| } |
| |
| private FileDiff[] getDiffs(IProgressMonitor monitor) { |
| List<FileDiff> diffResult = new ArrayList<>(); |
| |
| if (base == null) { |
| if (tip instanceof RepositoryCommit) { |
| diffResult.addAll( |
| asList(((RepositoryCommit) tip).getDiffs())); |
| if (tip.getRevCommit().getParentCount() > 2) { |
| RevCommit untrackedCommit = tip.getRevCommit() |
| .getParent( |
| StashEditorPage.PARENT_COMMIT_UNTRACKED); |
| diffResult.addAll(asList( |
| new RepositoryCommit(tip.getRepository(), |
| untrackedCommit).getDiffs())); |
| } |
| } |
| } else { |
| // Compute the diffs between tip and base |
| Repository repository = tip.getRepository(); |
| RevCommit tipCommit = tip.getRevCommit(); |
| RevCommit baseCommit = base.getRevCommit(); |
| FileDiff[] diffsResult = null; |
| try (RevWalk revWalk = new RevWalk(repository); |
| TreeWalk treewalk = new TreeWalk( |
| revWalk.getObjectReader())) { |
| treewalk.setRecursive(true); |
| treewalk.setFilter(TreeFilter.ANY_DIFF); |
| revWalk.parseBody(tipCommit); |
| revWalk.parseBody(baseCommit); |
| diffsResult = FileDiff.compute(repository, treewalk, |
| tipCommit, new RevCommit[] { baseCommit }, |
| monitor, TreeFilter.ALL); |
| } catch (IOException e) { |
| diffsResult = new FileDiff[0]; |
| } |
| return diffsResult; |
| } |
| |
| FileDiff[] result = diffResult.toArray(new FileDiff[0]); |
| Arrays.sort(result, FileDiff.PATH_COMPARATOR); |
| return result; |
| } |
| |
| private DiffDocument formatDiffs(FileDiff[] diffs, |
| IProgressMonitor monitor) { |
| SubMonitor progress = SubMonitor.convert(monitor, diffs.length); |
| DiffDocument document = new DiffDocument(); |
| try (DiffRegionFormatter formatter = new DiffRegionFormatter( |
| document)) { |
| for (FileDiff diff : diffs) { |
| if (progress.isCanceled()) { |
| break; |
| } |
| progress.subTask(diff.getPath()); |
| try { |
| formatter.write(diff); |
| } catch (IOException ignore) { |
| // Ignored |
| } |
| progress.worked(1); |
| } |
| document.connect(formatter); |
| } |
| return document; |
| } |
| |
| @Override |
| public boolean belongsTo(Object family) { |
| return JobFamilies.DIFF == family || super.belongsTo(family); |
| } |
| }; |
| } |
| |
| /** |
| * A document provider that knows about {@link DiffEditorInput}. Derived |
| * from {@link FileDocumentProvider} to get the default save implementation |
| * needed for "Save As...". |
| */ |
| private static class DiffDocumentProvider extends FileDocumentProvider { |
| |
| @Override |
| public IStatus getStatus(Object element) { |
| if (element instanceof DiffEditorInput) { |
| DiffEditorInput input = (DiffEditorInput) element; |
| if (input.getBase() == null) { |
| IRepositoryCommit commit = input.getTip(); |
| if (commit.getRevCommit() != null |
| && commit.getRevCommit().getParentCount() > 1) { |
| if (!(commit instanceof RepositoryCommit) |
| || !((RepositoryCommit) commit).isStash()) { |
| return Activator.createErrorStatus( |
| UIText.DiffEditor_WarningNoDiffForMerge); |
| } |
| } |
| } |
| } |
| return Status.OK_STATUS; |
| } |
| |
| @Override |
| protected IDocument createDocument(Object element) |
| throws CoreException { |
| if (element instanceof DiffEditorInput) { |
| IDocument document = ((DiffEditorInput) element).getDocument(); |
| if (document != null) { |
| return document; |
| } |
| } |
| return new Document(); |
| } |
| |
| @Override |
| protected IAnnotationModel createAnnotationModel(Object element) |
| throws CoreException { |
| return new AnnotationModel(); |
| } |
| |
| @Override |
| protected IRunnableContext getOperationRunner( |
| IProgressMonitor monitor) { |
| return null; |
| } |
| |
| } |
| |
| /** |
| * An ephemeral {@link PreferenceStore} that sets the annotation colors |
| * based on the theme-dependent colors defined for the line backgrounds in a |
| * unified diff. This ensures that the annotation colors are always |
| * consistent with the line backgrounds. Plus the user doesn't have to |
| * configure several related colors, and the annotations update when the |
| * theme changes. |
| */ |
| private static class ThemePreferenceStore extends PreferenceStore { |
| |
| // The colors defined in plugin.xml for these annotations are not taken |
| // into account. We always use auto-computed colors based on the current |
| // settings for the line background colors. |
| |
| private static final String ADD_ANNOTATION_COLOR_PREFERENCE = "org.eclipse.egit.ui.commitEditor.diffAddedColor"; //$NON-NLS-1$ |
| |
| private static final String REMOVE_ANNOTATION_COLOR_PREFERENCE = "org.eclipse.egit.ui.commitEditor.diffRemovedColor"; //$NON-NLS-1$ |
| |
| private final IPropertyChangeListener listener = event -> { |
| String property = event.getProperty(); |
| if (IThemeManager.CHANGE_CURRENT_THEME.equals(property)) { |
| setColorRegistry(); |
| initColors(); |
| } else if (THEME_DiffAddBackgroundColor.equals(property) |
| || THEME_DiffRemoveBackgroundColor.equals(property)) { |
| initColors(); |
| } |
| }; |
| |
| private ColorRegistry currentColors; |
| |
| public ThemePreferenceStore() { |
| super(); |
| setColorRegistry(); |
| initColors(); |
| PlatformUI.getWorkbench().getThemeManager() |
| .addPropertyChangeListener(listener); |
| } |
| |
| private void setColorRegistry() { |
| if (currentColors != null) { |
| currentColors.removeListener(listener); |
| } |
| currentColors = PlatformUI.getWorkbench().getThemeManager() |
| .getCurrentTheme().getColorRegistry(); |
| currentColors.addListener(listener); |
| } |
| |
| private void initColors() { |
| // The overview ruler tones down colors. Since our background colors |
| // usually are already rather pale, let's saturate them more and |
| // brighten them, otherwise the annotations will be barely visible. |
| RGB rgb = adjust(currentColors.getRGB(THEME_DiffAddBackgroundColor), |
| 4.0); |
| setValue(ADD_ANNOTATION_COLOR_PREFERENCE, |
| StringConverter.asString(rgb)); |
| rgb = adjust(currentColors.getRGB(THEME_DiffRemoveBackgroundColor), |
| 4.0); |
| setValue(REMOVE_ANNOTATION_COLOR_PREFERENCE, |
| StringConverter.asString(rgb)); |
| } |
| |
| /** |
| * Increases the saturation (simple multiplier), and brightens dark |
| * colors. |
| * |
| * @param rgb |
| * to modify |
| * @param saturation |
| * multiplier |
| * @return A new {@link RGB} for the new saturated and possibly |
| * brightened color |
| */ |
| private RGB adjust(RGB rgb, double saturation) { |
| float[] hsb = rgb.getHSB(); |
| // We also brighten the color because otherwise the color |
| // manipulations in OverviewRuler result in a fill color barely |
| // discernible from a dark background. |
| return new RGB(hsb[0], (float) Math.min(hsb[1] * saturation, 1.0), |
| hsb[2] < 0.5 ? hsb[2] * 2 : hsb[2]); |
| } |
| |
| public void dispose() { |
| PlatformUI.getWorkbench().getThemeManager() |
| .removePropertyChangeListener(listener); |
| if (currentColors != null) { |
| currentColors.removeListener(listener); |
| currentColors = null; |
| } |
| } |
| } |
| } |