/*
* Vimplugin
*
* Copyright (c) 2007 - 2012 by The Vimplugin Project.
*
* Released under the GNU General Public License
* with ABSOLUTELY NO WARRANTY.
*
* See the file COPYING for more information.
*/
package org.vimplugin.editors;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URI;
import org.eclim.logging.Logger;
import org.eclim.util.CommandExecutor;
import org.eclim.util.file.FileUtils;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
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.Status;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceDialog;
import org.eclipse.jface.text.IDocument;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
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.IMemento;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.PreferencesUtil;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.internal.part.StatusPart;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.vimplugin.DisplayUtils;
import org.vimplugin.VimConnection;
import org.vimplugin.VimPlugin;
import org.vimplugin.VimServer;
import org.vimplugin.preferences.PreferenceConstants;
/**
* Provides an Editor to Eclipse which is backed by a Vim instance.
*/
public class VimEditor
extends TextEditor
{
private static final Logger logger = Logger.getLogger(VimEditor.class);
private static final String ECLIMD_VIEW_ID = "org.eclim.eclipse.ui.EclimdView";
/** ID of the VimServer. */
private int serverID;
/** Buffer ID in Vim instance. */
private int bufferID;
private Canvas editorGUI;
private IFile selectedFile;
private IDocument document;
private VimViewer viewer;
private VimDocumentProvider documentProvider;
private boolean dirty;
private boolean alreadyClosed = false;
private long lastFocus = 0;
private Composite parent;
/**
* The field to grab for Windows/Win32.
*/
public static final String win32WID = "handle";
/**
* The field to grab for GTK2.
*/
public static final String gtkWID = "embeddedHandle";
/**
* a shell to open {@link MessageDialog MessageDialogs}.
*/
private Shell shell;
private boolean embedded;
private boolean tabbed;
private boolean documentListen;
/**
* The constructor.
*/
public VimEditor() {
super();
bufferID = -1; // not really necessary but set it to an invalid buffer
setDocumentProvider(documentProvider = new VimDocumentProvider());
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.texteditor.AbstractDecoratedTextEditor#createPartControl(org.eclipse.swt.widgets.Composite)
*/
@Override
public void createPartControl(Composite parent) {
this.parent = parent;
this.shell = parent.getShell();
VimPlugin plugin = VimPlugin.getDefault();
if (!plugin.gvimAvailable()) {
MessageDialog dialog = new MessageDialog(
shell, "Vimplugin", null,
plugin.getMessage("gvim.not.found.dialog"),
MessageDialog.ERROR,
new String[]{IDialogConstants.OK_LABEL, IDialogConstants.CANCEL_LABEL}, 0)
{
protected void buttonPressed(int buttonId) {
super.buttonPressed(buttonId);
if (buttonId == IDialogConstants.OK_ID){
PreferenceDialog prefs = PreferencesUtil.createPreferenceDialogOn(
shell, "org.vimplugin.preferences.VimPreferences", null, null);
if (prefs != null){
prefs.open();
}
}
}
};
dialog.open();
if (!plugin.gvimAvailable()) {
throw new RuntimeException(plugin.getMessage("gvim.not.found"));
}
}
IPreferenceStore prefs = plugin.getPreferenceStore();
tabbed = prefs.getBoolean(PreferenceConstants.P_TABBED);
embedded = prefs.getBoolean(PreferenceConstants.P_EMBED);
// disabling documentListen until there is a really good reason to have,
// cause it is by far the buggest part of vim's netbeans interface.
documentListen = false; //plugin.gvimNbDocumentListenSupported();
if (embedded){
if (!plugin.gvimEmbedSupported()){
String message = plugin.getMessage(
"gvim.not.supported",
plugin.getMessage("gvim.embed.not.supported"));
throw new RuntimeException(message);
}
}
if (!plugin.gvimNbSupported()){
String message = plugin.getMessage(
"gvim.not.supported",
plugin.getMessage("gvim.nb.not.enabled"));
throw new RuntimeException(message);
}
//set some flags
alreadyClosed = false;
dirty = false;
String projectPath = null;
String filePath = null;
IEditorInput input = getEditorInput();
if (input instanceof IFileEditorInput){
selectedFile = ((IFileEditorInput)input).getFile();
IProject project = selectedFile.getProject();
IPath path = project.getRawLocation();
if(path == null){
String name = project.getName();
path = ResourcesPlugin.getWorkspace().getRoot().getRawLocation();
path = path.append(name);
}
projectPath = path.toPortableString();
filePath = selectedFile.getRawLocation().toPortableString();
if (filePath.toLowerCase().indexOf(projectPath.toLowerCase()) != -1){
filePath = filePath.substring(projectPath.length() + 1);
}
}else{
URI uri = ((IURIEditorInput)input).getURI();
filePath = uri.toString().substring("file:".length());
filePath = filePath.replaceFirst("^/([A-Za-z]:)", "$1");
}
if (filePath != null){
editorGUI = new Canvas(parent, SWT.EMBEDDED);
//create a vim instance
VimConnection vc = createVim(projectPath, filePath, parent);
viewer = new VimViewer(
bufferID, vc, editorGUI != null ? editorGUI : parent, SWT.EMBEDDED);
viewer.getTextWidget().setVisible(false);
viewer.setDocument(document);
viewer.setEditable(isEditable());
try{
Field fSourceViewer =
AbstractTextEditor.class.getDeclaredField("fSourceViewer");
fSourceViewer.setAccessible(true);
fSourceViewer.set(this, viewer);
}catch(Exception e){
logger.error("Unable to access source viewer field.", e);
}
// open eclimd view if necessary
boolean startEclimd = plugin.getPreferenceStore()
.getBoolean(PreferenceConstants.P_START_ECLIMD);
if (startEclimd){
IWorkbenchPage page = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getActivePage();
try{
if (page != null && page.findView(ECLIMD_VIEW_ID) == null){
page.showView(ECLIMD_VIEW_ID);
}
}catch(PartInitException pie){
logger.error("Unable to open eclimd view.", pie);
}
}
// on initial open, our part listener isn't firing for some reason.
if(embedded){
plugin.getPartListener().partOpened(this);
plugin.getPartListener().partBroughtToTop(this);
plugin.getPartListener().partActivated(this);
}
}
}
/**
* Create a vim instance figuring out if it should be external or embedded.
*
* @param workingDir
* @param filePath
* @param parent
*/
private VimConnection createVim(
String workingDir, String filePath, Composite parent)
{
VimPlugin plugin = VimPlugin.getDefault();
//get bufferId
bufferID = plugin.getNumberOfBuffers();
plugin.setNumberOfBuffers(bufferID + 1);
IStatus status = null;
VimConnection vc = null;
if (embedded) {
try {
vc = createEmbeddedVim(workingDir, filePath, editorGUI);
} catch (Exception e) {
embedded = false;
vc = createExternalVim(workingDir, filePath, parent);
String message = plugin.getMessage(
e instanceof NoSuchFieldException ?
"embed.unsupported" : "embed.fallback");
status = new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, 0, message, e);
}
} else {
vc = createExternalVim(workingDir, filePath, parent);
String message = plugin.getMessage("gvim.external.success");
status = new Status(IStatus.OK, PlatformUI.PLUGIN_ID, message);
}
if (status != null){
editorGUI.dispose();
editorGUI = null;
new StatusPart(parent, status);
// remove the "Show the Error Log View" button if the status is OK
if (status.getSeverity() == IStatus.OK){
for (Control c : parent.getChildren()){
if (c instanceof Composite){
for (Control ch : ((Composite)c).getChildren()){
if (ch instanceof Button){
ch.setVisible(false);
}
}
}
}
}
}
plugin.getVimserver(serverID).getEditors().add(this);
return vc;
}
/**
* Create an external Vim instance.
*
* @param workingDir
* @param filePath
* @param parent
*/
private VimConnection createExternalVim(
String workingDir, String filePath, Composite parent)
{
VimPlugin plugin = VimPlugin.getDefault();
boolean first = plugin.getVimserver(VimPlugin.DEFAULT_VIMSERVER_ID) == null;
serverID = tabbed ? plugin.getDefaultVimServer() : plugin.createVimServer();
plugin.getVimserver(serverID).start(workingDir, filePath, tabbed, first);
VimConnection vc = plugin.getVimserver(serverID).getVc();
vc.command(bufferID, "editFile", "\"" + filePath + "\"");
if (documentListen){
vc.command(bufferID, "startDocumentListen", "");
}else{
vc.command(bufferID, "stopDocumentListen", "");
}
return vc;
}
/**
* Creates an embedded Vim instance (platform-dependent!). Gets the Window
* ID/Handle of the SWT Widget given, uses reflection since the code is
* platform specific and this allows us to distribute just one plugin for
* all platforms.
*
* @param workingDir
* @param filePath
* @param parent
* @throws Exception
*/
private VimConnection createEmbeddedVim(
String workingDir, String filePath, Composite parent)
throws Exception
{
long wid = 0;
Field f = null;
try{
f = Composite.class.getField(VimEditor.gtkWID);
}catch(NoSuchFieldException nsfe){
f = Control.class.getField(VimEditor.win32WID);
}
wid = f.getLong(parent);
VimPlugin plugin = VimPlugin.getDefault();
serverID = plugin.createVimServer();
plugin.getVimserver(serverID).start(workingDir, wid);
//int h = parent.getClientArea().height;
//int w = parent.getClientArea().width;
VimConnection vc = plugin.getVimserver(serverID).getVc();
//vc.command(bufferID, "setLocAndSize", h + " " + w);
vc.command(bufferID, "editFile", "\"" + filePath + "\"");
if (documentListen){
vc.command(bufferID, "startDocumentListen", "");
}else{
vc.command(bufferID, "stopDocumentListen", "");
}
return vc;
}
/**
* This function will be called by vimserver when it gets an event
* disconnect or killed It doesn't ask to save modifications since vim takes
* care of that.
*/
public void forceDispose() {
final VimEditor editor = this;
Display display = getSite().getShell().getDisplay();
display.asyncExec(new Runnable() {
public void run() {
if (editor != null && !editor.alreadyClosed) {
editor.setDirty(false);
editor.showBusy(true);
editor.close(false);
getSite().getPage().closeEditor(editor, false);
// editor.alreadyClosed = true;
}
}
});
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.editors.text.TextEditor#dispose()
*/
@Override
public void dispose() {
// closing the eclipse tab directly calls dispose, but not close.
close(true);
if (viewer != null) {
viewer.getTextWidget().dispose();
viewer = null;
}
if (editorGUI != null) {
editorGUI.dispose();
editorGUI = null;
}
document = null;
super.dispose();
}
/**
* This function will be called when we close the window If the
* <code>save</code> is true then we call command save If the current
* buffer is the last one the vim will be closed, else only the buffer will
* be closed.
*/
@Override
public void close(boolean save) {
if (this.alreadyClosed) {
super.close(false);
return;
}
VimPlugin plugin = VimPlugin.getDefault();
alreadyClosed = true;
VimServer server = plugin.getVimserver(serverID);
if (server != null){
server.getEditors().remove(this);
if (save && dirty) {
server.getVc().command(bufferID, "save", "");
dirty = false;
firePropertyChange(PROP_DIRTY);
}
if (server.getEditors().size() > 0) {
server.getVc().command(bufferID, "close", "");
String gvim = VimPlugin.getDefault().getPreferenceStore().getString(
PreferenceConstants.P_GVIM);
String[] args = new String[5];
args[0] = gvim;
args[1] = "--servername";
args[2] = String.valueOf(server.getID());
args[3] = "--remote-send";
args[4] = "<esc>:redraw!<cr>";
try{
CommandExecutor.execute(args, 1000);
}catch(Exception e){
logger.error("Error redrawing vim after file close.", e);
}
} else {
try {
VimConnection vc = server.getVc();
if (vc != null){
server.getVc().function(bufferID, "saveAndExit", "");
}
plugin.stopVimServer(serverID);
} catch (IOException e) {
message(plugin.getMessage("server.stop.failed"), e);
}
}
}
super.close(false);
}
/*
* public void setHighlightRange(int offset,int length,boolean moveCursor){
* System.out.println("--Highlighted-"+offset+length+"-- OK!");
* if(moveCursor){ VimPlugin.getDefault().getVimserver().getVc().command(
* bufferID, "setDot", "2/1"); } }
*/
/**
* We can't modify the file in the eclipse source viewer.. We use that only
* for showing error messages...
*/
@Override
public boolean isEditable() {
return false;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.texteditor.AbstractTextEditor#doSave(org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public void doSave(IProgressMonitor monitor) {
VimPlugin.getDefault().getVimserver(serverID).getVc()
.command(bufferID, "save", "");
dirty = false;
firePropertyChange(PROP_DIRTY);
}
/**
* Since we have a copy of edited text in document, we can perform saveAs
* operation
*/
@Override
public void doSaveAs() {
performSaveAs(null);
}
/**
* Initialisation goes here..
*/
@Override
public void init(IEditorSite site, IEditorInput input)
throws PartInitException
{
setSite(site);
setInput(input);
try {
document = documentProvider.createDocument(input);
} catch (Exception e) {
VimPlugin plugin = VimPlugin.getDefault();
message(plugin.getMessage("document.create.failed"), e);
}
}
/**
* Returns <code>true</code> if the file was modified, else return
* <code>false</code>
*/
@Override
public boolean isDirty() {
return dirty;
}
/**
* Returns <code>true</code> if save as is allowed, else return
* <code>false</code>
*/
@Override
public boolean isSaveAsAllowed() {
return true;
}
private String getFilePath(IEditorInput input)
{
String filePath = null;
if (input instanceof IFileEditorInput){
selectedFile = ((IFileEditorInput)input).getFile();
filePath = selectedFile.getRawLocation().toPortableString();
}else{
URI uri = ((IURIEditorInput)input).getURI();
filePath = uri.toString().substring("file:".length());
filePath = filePath.replaceFirst("^/([A-Za-z]:)", "$1");
}
return filePath;
}
/**
* Makes the present editor dirty. Thus the IDE knows that file was
* modified. Asynchronous call of firePropertyChangeAsync.
*
* @param result
*/
public void setDirty(boolean result) {
dirty = result;
final VimEditor vime = this;
Display display = getSite().getShell().getDisplay();
display.asyncExec(new Runnable() {
public void run() {
vime.firePropertyChangeAsync(PROP_DIRTY);
}
});
}
/**
* Needed to allow calls from asyncronous threads. Simply delegates to
* super.firePropertyChange
*
* @param prop the property to change
*/
public void firePropertyChangeAsync(int prop) {
super.firePropertyChange(prop);
}
/**
* Sets focus (brings to top in Vim) to the buffer.. this function will be
* called when user activates this editor window
*/
@Override
public void setFocus() {
if (alreadyClosed) {
getSite().getPage().closeEditor(this, false);
return;
}
// let the parent composite handle setting the focus on the tab first.
if (embedded){
parent.setFocus();
}
VimPlugin plugin = VimPlugin.getDefault();
VimConnection conn = plugin.getVimserver(serverID).getVc();
// get the current offset which "setDot" requires.
//String offset = "0";
try{
String cursor = conn.function(bufferID, "getCursor", "");
if (cursor == null){
// the only case that i know of where this happens is if the file is
// open somewhere else and gvim is prompting the user as to how to
// proceed. Exit now or the gvim prompt will be sent to the background.
return;
}
}catch(IOException ioe){
logger.error("Unable to get cursor position.", ioe);
}
// Brings the corresponding buffer to top
//conn.command(bufferID, "setDot", offset);
// Brings the vim editor window to top
conn.command(bufferID, "raise", "");
// to fully focus gvim, we need to simulate a mouse click.
// Should this be optional, via a preference? There is the potential for
// weirdness here.
if (embedded && parent.getDisplay().getActiveShell() != null){
boolean autoClickFocus = plugin.getPreferenceStore()
.getBoolean(PreferenceConstants.P_FOCUS_AUTO_CLICK);
if (autoClickFocus){
// hack: setFocus may be called more than once, so attempt to only
// simulate a click only on the first one.
long now = System.currentTimeMillis();
long delta = (now - lastFocus);
lastFocus = now;
if (delta > 300) {
Rectangle bounds = parent.getBounds();
final Point point = parent.toDisplay(
bounds.x + 5, bounds.y + bounds.height - 25);
new Thread(){
public void run()
{
DisplayUtils.doClick(parent.getDisplay(), point.x, point.y, true);
}
}.start();
}
}
}
}
/**
* Sets the editor window title to path.. need to change path to file name..
*
* @param path
*/
public void setTitleTo(final String path) {
Display.getDefault().asyncExec(new Runnable(){
public void run()
{
String filename = path.substring(path.lastIndexOf(File.separator) + 1);
setPartName(filename);
}
});
}
// /////// Handling Document content.. ///////////
/**
* Returns the document provider
*/
@Override
public IDocumentProvider getDocumentProvider() {
return documentProvider;
}
/**
* Sets the document content to given text
*
* @param text The text for the editor.
*/
public void setDocumentText(String text) {
document.set(text);
setDirty(true);
}
/**
* Remove the backslashes from the given string.
*
* @param text String to remove from
* @return The processed string
*/
private String removeBackSlashes(String text) {
if (text.length() <= 2)
return text;
int offset = 0, length = text.length(), offset1 = 0;
String newText = "";
while (offset < length) {
offset1 = text.indexOf('\\', offset);
if (offset1 < 0) {
newText = newText + text.substring(offset);
break;
}
newText = newText +
text.substring(offset, offset1) +
(text.length() > offset1 + 1 ? text.substring(offset1 + 1, offset1 + 2) : "");
offset = offset1 + 2;
}
return newText;
}
/**
* Inserts text into document.
*
* @param text The text to insert.
* @param offset The offset to insert it at.
*/
public void insertDocumentText(String text, int offset) {
text = removeBackSlashes(text);
try {
String contents = document.get();
// FIXME: determine file encoding.
offset = FileUtils.byteOffsetToCharOffset(
new ByteArrayInputStream(contents.getBytes()), offset, null);
String first = contents.substring(0, offset);
String last = contents.substring(offset);
if (text.equals(new String("\\n"))) {
first = first + System.getProperty("line.separator") + last;
} else {
first = first + text + last;
}
document.set(first);
setDirty(true);
} catch(Exception e) {
VimPlugin plugin = VimPlugin.getDefault();
logger.error(plugin.getMessage("document.insert.failed"), e);
}
}
/**
* Removes text in the document
*
* @param offset The offset of the cursor in the text.
* @param length The amount of text to remove.
*/
public void removeDocumentText(int offset, int length) {
try {
String contents = document.get();
// FIXME: determine file encoding.
int loffset = FileUtils.byteOffsetToCharOffset(
new ByteArrayInputStream(contents.getBytes()), offset + length, null);
offset = FileUtils.byteOffsetToCharOffset(
new ByteArrayInputStream(contents.getBytes()), offset, null);
length = loffset - offset;
String first = contents.substring(0, offset);
String last = contents.substring(offset + length);
first = first + last;
document.set(first);
setDirty(true);
} catch(Exception e) {
VimPlugin plugin = VimPlugin.getDefault();
logger.error(plugin.getMessage("document.remove.failed"), e);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.editors.text.TextEditor#createActions()
*/
@Override
protected void createActions() {
super.createActions();
}
/**
* {@inheritDoc}
* @see TextEditor#doSetInput(IEditorInput)
*/
@Override
protected void doSetInput(IEditorInput input)
throws CoreException
{
if(getEditorInput() != null){
String oldFilePath = getFilePath(getEditorInput());
String newFilePath = getFilePath(input);
if (!oldFilePath.equals(newFilePath)){
VimConnection vc = VimPlugin.getDefault()
.getVimserver(serverID).getVc();
if (input instanceof IFileEditorInput){
IProject project = selectedFile.getProject();
IPath path = project.getRawLocation();
if(path == null){
String name = project.getName();
path = ResourcesPlugin.getWorkspace().getRoot().getRawLocation();
path = path.append(name);
}
String projectPath = path.toPortableString();
if (newFilePath.toLowerCase().indexOf(projectPath.toLowerCase()) != -1){
newFilePath = newFilePath.substring(projectPath.length() + 1);
}
}
if (isDirty()){
vc.remotesend("<esc>:saveas! " + newFilePath.replace(" ", "\\ ") + "<cr>");
}else{
vc.command(bufferID, "editFile", "\"" + newFilePath + "\"");
}
}
}
super.doSetInput(input);
}
/**
* {@inheritDoc}
* @see org.eclipse.ui.IPersistable#saveState(IMemento)
*/
@Override
public void saveState(IMemento arg0) {
// no-op for now. prevents error on closing of eclipse while a VimEditor
// instance is open.
}
/**
* @return the file.
*/
public IFile getSelectedFile() {
return selectedFile;
}
/**
* @return the gvim server id.
*/
public int getServerID() {
return serverID;
}
/**
* @return the bufferID
*/
public int getBufferID() {
return bufferID;
}
/**
* Determines if this editor is running an embedded gvim instance or not.
*
* @return True if the gvim instance is embedded, false otherwise.
*/
public boolean isEmbedded() {
return embedded;
}
/**
* simple one-liner to display error-messages using {@link MessageDialog}.
* @param message the string to display
*/
private void message(String message, Throwable e) {
//convert stacktrace to string
String stacktrace;
StringWriter sw = null;
PrintWriter pw = null;
try {
sw = new StringWriter();
pw = new PrintWriter(sw);
e.printStackTrace(pw);
stacktrace = sw.toString();
} finally {
try {
if (pw != null)
pw.close();
if (sw != null)
sw.close();
} catch (IOException ignore) {
}
}
MessageDialog.openError(shell, "Vimplugin", message + stacktrace);
}
}