/** * Copyright (c) 2010, 2012 Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text of * such license is available at www.eclipse.org. */ package org.cloudsmith.geppetto.pp.dsl.ui.linked; import java.io.File; import org.cloudsmith.geppetto.pp.dsl.ui.preferences.data.FormatterGeneralPreferences; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; 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.QualifiedName; import org.eclipse.emf.common.ui.URIEditorInput; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.common.util.WrappedException; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorSite; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IURIEditorInput; import org.eclipse.ui.PartInitException; import org.eclipse.ui.ide.FileStoreEditorInput; import org.eclipse.ui.internal.editors.text.EditorsPlugin; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.SourceViewerDecorationSupport; import org.eclipse.ui.views.contentoutline.IContentOutlinePage; import org.eclipse.xtext.ui.editor.XtextEditor; import org.eclipse.xtext.ui.editor.outline.impl.OutlinePage; import com.google.inject.Inject; /** * This class extends the standard XtextEditor to make it capable of * opening and saving external files by managing them as linked resources. * * This implementation also supports temporary files than when saved will change to * a SaveAs operation (also see {@link TmpFileStoreEditorInput}). * * An editor customizer can also be bound and it will receive calls to manage the content * of the context menu (see {@link IExtXtextEditorCustomizer}). * * Also see {@link ExtLinkedXtextEditorMatchingStrategy} which is required to ensure multiple * editors are not opened for the same file. * */ public class ExtLinkedXtextEditor extends XtextEditor { @Inject private IExtXtextEditorCustomizer editorCustomizer; /** * Property for last saved location - property stored as persistent property in the * workspace root. * TODO: this should come from the customizer */ public static final QualifiedName LAST_SAVEAS_LOCATION = new QualifiedName( "org.cloudsmith.geppetto.pp.dsl.ui", "lastSaveLocation"); @Inject ISaveActions saveActions; /** * Preference key for showing print margin ruler. */ private final static String PRINT_MARGIN = AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN; /** * Preference key for print margin ruler color. */ private final static String PRINT_MARGIN_COLOR = AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLOR; // This is the preference for the default print margin, PP uses the margin set in the formatter preferences // /** // * Preference key for print margin ruler column. // */ // private final static String PRINT_MARGIN_COLUMN = AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN; // // @Inject // private IPreferenceStoreAccess preferenceAccess; @Inject public ExtLinkedXtextEditor() { super(); } @Override protected void configureSourceViewerDecorationSupport(SourceViewerDecorationSupport support) { super.configureSourceViewerDecorationSupport(support); support.setMarginPainterPreferenceKeys( PRINT_MARGIN, PRINT_MARGIN_COLOR, FormatterGeneralPreferences.FORMATTER_MAXWIDTH); // support.setCharacterPairMatcher(characterPairMatcher); // support.setMatchingCharacterPainterPreferenceKeys(BracketMatchingPreferencesInitializer.IS_ACTIVE_KEY, // BracketMatchingPreferencesInitializer.COLOR_KEY); } /** * When editor is disposed the unlinking behavior is triggered (depending on reason of dispose, the linked file * may be unlinked). * See {@link ExtLinkedFileHelper#unlinkInput(IFileEditorInput)} for more info, */ @Override public void dispose() { // Unlink the input if it was linked IEditorInput input = getEditorInput(); if(input instanceof IFileEditorInput) ExtLinkedFileHelper.unlinkInput((IFileEditorInput) input); super.dispose(); } /* * (non-Javadoc) * * @see org.eclipse.xtext.ui.editor.XtextEditor#doSave(org.eclipse.core.runtime.IProgressMonitor) */ @Override public void doSave(IProgressMonitor progressMonitor) { // If document is a temporary / untitled document, change "save" to "saveAs" final IEditorInput input = getEditorInput(); if(input instanceof IFileEditorInput && ((IFileEditorInput) input).getFile().isLinked() && ((IFileEditorInput) input).getFile().getProject().getName().equals( ExtLinkedFileHelper.AUTOLINK_PROJECT_NAME)) { String val; try { val = ((FileEditorInput) input).getFile().getPersistentProperty( TmpFileStoreEditorInput.UNTITLED_PROPERTY); } catch(CoreException e) { // Don't know what to do here - this is really bad, but doSave does not expect // any errors. throw new WrappedException(e); } if(val != null && "true".equals(val)) { doSaveAs(); return; } } rememberSelection(); saveActions.perform(getResource(), getDocument()); restoreSelection(); super.doSave(progressMonitor); } /* * (non-Javadoc) * * @see org.eclipse.xtext.ui.editor.XtextEditor#doSaveAs() */ @Override public void doSaveAs() { saveActions.perform(getResource(), getDocument()); super.doSaveAs(); // make sure the outline is fully refreshed IContentOutlinePage outlinePage = (IContentOutlinePage) getAdapter(IContentOutlinePage.class); if(outlinePage instanceof OutlinePage) ((OutlinePage) outlinePage).scheduleRefresh(); } /** * Allows customization of the editor title. * * * @see org.eclipse.xtext.ui.editor.XtextEditor#doSetInput(org.eclipse.ui.IEditorInput) */ @Override protected void doSetInput(IEditorInput input) throws CoreException { super.doSetInput(input); String customTitle = editorCustomizer.customEditorTitle(input); if(customTitle != null) this.setPartName(customTitle); } // @Override // protected void initializeEditor() { // setPreferenceStore(preferenceAccess.getPreferenceStore()); // // setPreferenceStore(EditorsPlugin.getDefault().getPreferenceStore()); // } /** * Overridden to allow customization of editor context menu via injected handler * * @see org.eclipse.ui.editors.text.TextEditor#editorContextMenuAboutToShow(org.eclipse.jface.action.IMenuManager) */ @Override protected void editorContextMenuAboutToShow(IMenuManager menu) { super.editorContextMenuAboutToShow(menu); editorCustomizer.customizeEditorContextMenu(menu); } private IFile getWorkspaceFile(IFileStore fileStore) { IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); IFile[] files = workspaceRoot.findFilesForLocationURI(fileStore.toURI()); if(files != null && files.length == 1) return files[0]; return null; } @Override protected void handlePreferenceStoreChanged(PropertyChangeEvent event) { // deal with indent property changing // deal with events that should NOT reach the parent ISourceViewer sourceViewer = getSourceViewer(); if(sourceViewer == null) return; String property = event.getProperty(); // System.out.println("Property Event: " + property); if(FormatterGeneralPreferences.FORMATTER_INDENTSIZE.equals(property) || FormatterGeneralPreferences.FORMATTER_SPACES_FOR_TABS.equals(property)) { IPreferenceStore store = getPreferenceStore(); if(store != null) sourceViewer.getTextWidget().setTabs(store.getInt(FormatterGeneralPreferences.FORMATTER_INDENTSIZE)); uninstallTabsToSpacesConverter(); if(store.getBoolean(FormatterGeneralPreferences.FORMATTER_SPACES_FOR_TABS)) installTabsToSpacesConverter(); return; } if(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH.equals(property)) { // INHIBIT THIS - Puppet editor is always "spaces for tabs" and does NOT follow the // Editor tab width setting, it is always the same as the indent size for formatting. return; } if(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SPACES_FOR_TABS.equals(property)) { // INHIBIT THIS CHANGE - Puppet editor is always "spaces for tabs" return; } super.handlePreferenceStoreChanged(event); } /** * Translates an incoming IEditorInput being an FilestoreEditorInput, or IURIEditorInput * that is not also a IFileEditorInput. * FilestoreEditorInput is used when opening external files in an IDE environment. * The result is that the regular XtextEditor gets an IEFSEditorInput which is also an * IStorageEditorInput. */ @Override public void init(IEditorSite site, IEditorInput input) throws PartInitException { // Can't do this until this time due to stupid callbacks from constructor before // injections have taken place. // ISourceViewer sourceViewer = getSourceViewer(); // SourceViewerDecorationSupport decorationSupport = getSourceViewerDecorationSupport(sourceViewer); // decorationSupport.setMarginPainterPreferenceKeys( // PRINT_MARGIN, PRINT_MARGIN_COLOR, PPPreferenceConstants.FORMATTER_MAXWIDTH); // THE ISSUE HERE: // In the IDE, the File Open Dialog (and elsewhere) uses a FilestoreEditorInput class // which is an IDE specific implementation. // The state at this point: // 1. When creating a file, the IEditorInput is an IURIEditorInput // 2. The only (non IDE specific) interface implemented by FilestoreEditorInput is IURIEditorInput // 3. The creation of a file is however also an IFileEditorInput // // Remedy: if(input instanceof IURIEditorInput && !(input instanceof IFileEditorInput)) { java.net.URI uri = ((IURIEditorInput) input).getURI(); String name = ((IURIEditorInput) input).getName(); // Check if this is linkable input if(uri.getScheme().equals("file")) { IFile linkedFile = null; // check and process tmp file input (untitled) if(input instanceof TmpFileStoreEditorInput) try { linkedFile = ExtLinkedFileHelper.obtainLink(uri, true); linkedFile.setPersistentProperty(TmpFileStoreEditorInput.UNTITLED_PROPERTY, "true"); } catch(CoreException e) { throw new PartInitException(e.getStatus()); } else { linkedFile = ExtLinkedFileHelper.obtainLink(uri, false); } IFileEditorInput linkedInput = new FileEditorInput(linkedFile); super.init(site, linkedInput); } else { // use EMF URI (readonly) input - will open without validation though... // (Could be improved, i.e. perform a download, make readonly, and keep in project, // or stored in tmp, and processed as the other linked resources.. URIEditorInput uriInput = new URIEditorInput(URI.createURI(uri.toString()), name); super.init(site, uriInput); return; } return; } super.init(site, input); } /* * @see org.eclipse.ui.texteditor.AbstractTextEditor#isTabConversionEnabled() * * @since 3.3 */ @Override protected boolean isTabsToSpacesConversionEnabled() { IPreferenceStore store = getPreferenceStore(); return store == null || store.getBoolean(FormatterGeneralPreferences.FORMATTER_SPACES_FOR_TABS); } // SaveAs support for linked files - saves them on local disc, not to workspace if file is in special // hidden external file link project. @Override protected void performSaveAs(IProgressMonitor progressMonitor) { Shell shell = getSite().getShell(); final IEditorInput input = getEditorInput(); // Customize save as if the file is linked, and it is in the special external link project // if(input instanceof IFileEditorInput && ((IFileEditorInput) input).getFile().isLinked() && ((IFileEditorInput) input).getFile().getProject().getName().equals( ExtLinkedFileHelper.AUTOLINK_PROJECT_NAME)) { final IEditorInput newInput; IDocumentProvider provider = getDocumentProvider(); // 1. If file is "untitled" suggest last save location // 2. ...otherwise use the file's location (i.e. likely to be a rename in same folder) // 3. If a "last save location" is unknown, use user's home // String suggestedName = null; String suggestedPath = null; { // is it "untitled" java.net.URI uri = ((IURIEditorInput) input).getURI(); String tmpProperty = null; try { tmpProperty = ((IFileEditorInput) input).getFile().getPersistentProperty( TmpFileStoreEditorInput.UNTITLED_PROPERTY); } catch(CoreException e) { // ignore - tmpProperty will be null } boolean isUntitled = tmpProperty != null && "true".equals(tmpProperty); // suggested name IPath oldPath = URIUtil.toPath(uri); // TODO: input.getName() is probably always correct suggestedName = isUntitled ? input.getName() : oldPath.lastSegment(); // suggested path try { suggestedPath = isUntitled ? ((IFileEditorInput) input).getFile().getWorkspace().getRoot().getPersistentProperty( LAST_SAVEAS_LOCATION) : oldPath.toOSString(); } catch(CoreException e) { // ignore, suggestedPath will be null } if(suggestedPath == null) { // get user.home suggestedPath = System.getProperty("user.home"); } } FileDialog dialog = new FileDialog(shell, SWT.SAVE); if(suggestedName != null) dialog.setFileName(suggestedName); if(suggestedPath != null) dialog.setFilterPath(suggestedPath); dialog.setFilterExtensions(new String[] { "*.pp", "*.*" }); String path = dialog.open(); if(path == null) { if(progressMonitor != null) progressMonitor.setCanceled(true); return; } // Check whether file exists and if so, confirm overwrite final File localFile = new File(path); if(localFile.exists()) { MessageDialog overwriteDialog = new MessageDialog(shell, "Save As", null, path + " already exists.\nDo you want to replace it?", MessageDialog.WARNING, new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL }, 1); // 'No' is the default if(overwriteDialog.open() != Window.OK) { if(progressMonitor != null) { progressMonitor.setCanceled(true); return; } } } IFileStore fileStore; try { fileStore = EFS.getStore(localFile.toURI()); } catch(CoreException ex) { EditorsPlugin.log(ex.getStatus()); String title = "Problems During Save As..."; String msg = "Save could not be completed. " + ex.getMessage(); MessageDialog.openError(shell, title, msg); return; } IFile file = getWorkspaceFile(fileStore); if(file != null) newInput = new FileEditorInput(file); else { IURIEditorInput uriInput = new FileStoreEditorInput(fileStore); java.net.URI uri = uriInput.getURI(); IFile linkedFile = ExtLinkedFileHelper.obtainLink(uri, false); newInput = new FileEditorInput(linkedFile); } if(provider == null) { // editor has been closed while the dialog was open return; } boolean success = false; try { provider.aboutToChange(newInput); provider.saveDocument(progressMonitor, newInput, provider.getDocument(input), true); success = true; } catch(CoreException x) { final IStatus status = x.getStatus(); if(status == null || status.getSeverity() != IStatus.CANCEL) { String title = "Problems During Save As..."; String msg = "Save could not be completed. " + x.getMessage(); MessageDialog.openError(shell, title, msg); } } finally { provider.changed(newInput); if(success) setInput(newInput); // the linked file must be removed (esp. if it is an "untitled" link). ExtLinkedFileHelper.unlinkInput(((IFileEditorInput) input)); // remember last saveAs location String lastLocation = URIUtil.toPath(((FileEditorInput) newInput).getURI()).toOSString(); try { ((FileEditorInput) newInput).getFile().getWorkspace().getRoot().setPersistentProperty( LAST_SAVEAS_LOCATION, lastLocation); } catch(CoreException e) { // ignore } } if(progressMonitor != null) progressMonitor.setCanceled(!success); return; } super.performSaveAs(progressMonitor); } }