/******************************************************************************* * Copyright (c) 2009-2012 Andrey Loskutov. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * Contributor: Andrey Loskutov - initial API and implementation *******************************************************************************/ package de.loskutov.anyedit.compare; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.Iterator; import org.eclipse.compare.CompareUI; import org.eclipse.compare.IEditableContent; import org.eclipse.compare.IEditableContentExtension; import org.eclipse.compare.ISharedDocumentAdapter; import org.eclipse.compare.IStreamContentAccessor; import org.eclipse.compare.ITypedElement; import org.eclipse.compare.SharedDocumentAdapter; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.DefaultPositionUpdater; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.TextSelection; 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.swt.graphics.Image; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IPartListener2; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPartReference; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.texteditor.IDocumentProvider; import de.loskutov.anyedit.AnyEditToolsPlugin; import de.loskutov.anyedit.ui.editor.AbstractEditor; public class TextStreamContent implements StreamContent, IStreamContentAccessor, IEditableContent, IEditableContentExtension { private static final String ANY_EDIT_COMPARE = "AnyEditTools.compare"; private final String selectedText; private final Position position; private final ContentWrapper content; private final AbstractEditor editor; private byte[] bytes; private boolean dirty; private final IPartListener2 partListener; private DefaultPositionUpdater positionUpdater; private IDocumentListener docListener; private Annotation lineAnnotation; private AnyeditCompareInput compareInput; private boolean disposed; private EditableSharedDocumentAdapter sharedDocumentAdapter; /** * @param content NOT null * @param editor might be null */ public TextStreamContent(ContentWrapper content, AbstractEditor editor) { this(content, editor, editor != null ? editor.getSelectedText() : null, createPosition(content.getSelection())); } private TextStreamContent(ContentWrapper content, AbstractEditor editor, String selectedText, Position position) { this.content = content; this.editor = editor == null? new AbstractEditor(null) : editor; this.selectedText = selectedText; this.position = position; this.partListener = new PartListener2Impl(); if(this.editor.getPart() != null) { PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().addPartListener( partListener); IDocument document = this.editor.getDocument(); // trigger recompare if(document != null) { docListener = new IDocumentListener(){ @Override public void documentAboutToBeChanged(DocumentEvent event) { // noop } @Override public void documentChanged(DocumentEvent event) { updateCompareEditor(event); } }; document.addDocumentListener(docListener); } } if (selectedText != null && isEditable()) { hookOnSelection(); } } void updateCompareEditor(DocumentEvent event){ if (event.getText() != null) { if ((event.fOffset >= position.offset && event.fOffset < position.offset + position.length) || (event.fOffset <= position.offset && event.fOffset + event.fLength > position.offset)) { compareInput.reuseEditor(); } } } private static Position createPosition(ITextSelection selection) { if (selection != null) { return new Position(selection.getOffset(), selection.getLength()); } Position pos = new Position(0, 0); pos.isDeleted = true; return pos; } private void hookOnSelection() { try { IDocument document = editor.getDocument(); positionUpdater = new DefaultPositionUpdater(ANY_EDIT_COMPARE); document.addPositionCategory(ANY_EDIT_COMPARE); document.addPositionUpdater(positionUpdater); document.addPosition(ANY_EDIT_COMPARE, position); addSelectionAnnotation(); } catch (BadLocationException e) { AnyEditToolsPlugin.logError("Can't create position in document", e); } catch (BadPositionCategoryException e) { AnyEditToolsPlugin.logError("Can't create position in document", e); } } private String getChangedCompareText() { if(bytes == null){ if(sharedDocumentAdapter != null) { IEditorInput editorInput = sharedDocumentAdapter.getDocumentKey(this); if(editorInput == null) { return null; } IDocumentProvider documentProvider = SharedDocumentAdapter.getDocumentProvider(editorInput); if(documentProvider != null) { IDocument document = documentProvider.getDocument(editorInput); if(document != null) { return document.get(); } } } return null; } // use charset from editor String charset = editor.computeEncoding(); try { return new String(bytes, charset); } catch (UnsupportedEncodingException e) { return new String(bytes); } } @Override public Image getImage() { return CompareUI.getImage(getType()); } @Override public String getName() { return selectedText == null ? content.getName() : "Selection in " + content.getName(); } @Override public String getFullName() { return selectedText == null ? content.getFullName() : "Selection in " + content.getFullName(); } @Override public String getType() { return content.getFileExtension(); } @Override public Object[] getChildren() { return new StreamContent[0]; } @Override public boolean commitChanges(IProgressMonitor pm) throws CoreException { if (!dirty || !isEditable()) { return true; } IDocument document = editor.getDocument(); ITextSelection selection = content.getSelection(); String text = getChangedCompareText(); if(text == null){ dirty = false; return true; } dirty = false; if (selection == null) { boolean sharedSaveOk = false; if(sharedDocumentAdapter != null) { IEditorInput editorInput = sharedDocumentAdapter.getDocumentKey(this); if(editorInput != null) { sharedSaveOk = sharedDocumentAdapter.saveDocument(editorInput, true, pm); } } if(!sharedSaveOk){ document.set(text); } } else { try { document.replace(position.getOffset(), position.getLength(), text); } catch (BadLocationException e) { AnyEditToolsPlugin.logError("Can't update text in editor", e); position.isDeleted = true; document.removePositionUpdater(positionUpdater); removeSelectionAnnotation(); return false; } } return true; } @Override public boolean isDirty() { return dirty; } @Override public InputStream getContents() throws CoreException { String charset = editor.computeEncoding(); if (selectedText != null) { byte[] bytes2; try { bytes2 = selectedText.getBytes(charset); } catch (UnsupportedEncodingException e) { bytes2 = selectedText.getBytes(); } return new ByteArrayInputStream(bytes2); } String documentContent = editor.getText(); if (documentContent != null) { byte[] bytes2; try { bytes2 = documentContent.getBytes(charset); } catch (UnsupportedEncodingException e) { bytes2 = documentContent.getBytes(); } return new ByteArrayInputStream(bytes2); } return null; } @Override public final boolean isEditable() { if (selectedText != null) { return !position.isDeleted && content.isModifiable() && !editor.isDisposed(); } return content.isModifiable() && editor.getDocument() != null; } @Override public ITypedElement replace(ITypedElement dest, ITypedElement src) { return null; } @Override public void setContent(byte[] newContent) { bytes = newContent; if(isEditable()) { dirty = true; } } @Override public void dispose() { IWorkbench workbench = PlatformUI.getWorkbench(); if(workbench.isClosing()){ return; } IWorkbenchPage page = workbench.getActiveWorkbenchWindow().getActivePage(); page.removePartListener(partListener); if (selectedText != null) { removeSelectionAnnotation(); } IDocument document = editor.getDocument(); if(document != null) { document.removeDocumentListener(docListener); document.removePosition(position); document.removePositionUpdater(positionUpdater); } position.isDeleted = true; lineAnnotation = null; editor.dispose(); dirty = false; if (sharedDocumentAdapter != null) { sharedDocumentAdapter.releaseBuffer(); } disposed = true; } @Override public boolean isDisposed() { return disposed; } private synchronized void addSelectionAnnotation() { if(editor.isDisposed()){ return; } IDocumentProvider documentProvider = editor.getDocumentProvider(); if(documentProvider == null){ return; } IEditorInput input = editor.getInput(); if(input == null){ return; } IAnnotationModel extension = documentProvider.getAnnotationModel(input); if (!(extension instanceof IAnnotationModelExtension)) { return; } lineAnnotation = new Annotation(ANY_EDIT_COMPARE, false, "This text is being compared with anoter one"); IAnnotationModelExtension modelExtension = (IAnnotationModelExtension) extension; IAnnotationModel model = modelExtension.getAnnotationModel(TextStreamContent.class); if (model == null) { model = new AnnotationModel(); } model.addAnnotation(lineAnnotation, new Position(position.offset, position.length)); modelExtension.addAnnotationModel(TextStreamContent.class, model); } private synchronized void removeSelectionAnnotation() { if(editor.isDisposed()){ return; } IDocumentProvider documentProvider = editor.getDocumentProvider(); if(documentProvider == null){ return; } IEditorInput input = editor.getInput(); if(input == null){ return; } IAnnotationModel extension = documentProvider.getAnnotationModel(input); if (!(extension instanceof IAnnotationModelExtension)) { return; } IAnnotationModelExtension modelExtension = (IAnnotationModelExtension) extension; IAnnotationModel model = modelExtension.getAnnotationModel(TextStreamContent.class); if(model == null){ return; } for (Iterator<?> iterator = model.getAnnotationIterator(); iterator.hasNext();) { Annotation ann = (Annotation) iterator.next(); if (lineAnnotation == ann) { model.removeAnnotation(ann); } } if (!model.getAnnotationIterator().hasNext()) { modelExtension.removeAnnotationModel(TextStreamContent.class); } } private final class PartListener2Impl implements IPartListener2 { @Override public void partClosed(IWorkbenchPartReference partRef) { if (editor.getPart() == partRef.getPart(false)) { positionUpdater = null; dispose(); } } @Override public void partInputChanged(IWorkbenchPartReference partRef) { partClosed(partRef); } @Override public void partActivated(IWorkbenchPartReference partRef) { // noop } @Override public void partBroughtToTop(IWorkbenchPartReference partRef) { // noop } @Override public void partDeactivated(IWorkbenchPartReference partRef) { // noop } @Override public void partHidden(IWorkbenchPartReference partRef) { // noop } @Override public void partOpened(IWorkbenchPartReference partRef) { // noop } @Override public void partVisible(IWorkbenchPartReference partRef) { // noop } } @Override public void init(AnyeditCompareInput input) { this.compareInput = input; } /** create new one with the selection re-created from annotation, if any */ @Override public StreamContent recreate() { TextStreamContent tc; // we should not dispose editor OR should re-create editor here if (selectedText == null || position.isDeleted) { tc = new TextStreamContent(content, editor.recreate(), null, createPosition(null)); } else { Position pos = new Position(position.getOffset(), position.getLength()); TextSelection sel = new TextSelection(editor.getDocument(), pos.getOffset(), pos.getLength()); content.setSelection(sel); String text = getText(pos); tc = new TextStreamContent(content, editor.recreate(), text, pos); } return tc; } private String getText(Position pos) { String text = null; try { text = editor.getDocument().get(pos.getOffset(), pos.getLength()); } catch (BadLocationException e) { // ignore, shit happens } return text; } @Override public boolean isReadOnly() { return editor.isEditorInputModifiable(); } @Override public IStatus validateEdit(Shell shell) { boolean state = editor.validateEditorInputState(); if(state){ return Status.OK_STATUS; } return Status.CANCEL_STATUS; } @Override public Object getAdapter(Class adapter) { if(selectedText == null) { if (adapter == ISharedDocumentAdapter.class) { return getSharedDocumentAdapter(); } } if(adapter == IFile.class) { return content.getIFile(); } return Platform.getAdapterManager().getAdapter(this, adapter); } /** * The code below is copy from org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement * and is required to add full Java editor capabilities (content assist, navigation etc) to the compare editor * @return */ private synchronized ISharedDocumentAdapter getSharedDocumentAdapter() { if (sharedDocumentAdapter == null) { sharedDocumentAdapter = new EditableSharedDocumentAdapter(this); } return sharedDocumentAdapter; } @Override public void setDirty(boolean dirty) { this.dirty = dirty; } }