/*
* Vimplugin
*
* Copyright (c) 2007 - 2011 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;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashSet;
import java.util.regex.Pattern;
import org.vimplugin.editors.VimEditor;
import org.vimplugin.listeners.BufferEnter;
import org.vimplugin.listeners.FeedKeys;
import org.vimplugin.listeners.FileClosed;
import org.vimplugin.listeners.FileModified;
import org.vimplugin.listeners.FileOpened;
import org.vimplugin.listeners.FileUnmodified;
import org.vimplugin.listeners.IVimListener;
import org.vimplugin.listeners.Logger;
import org.vimplugin.listeners.ServerDisconnect;
import org.vimplugin.listeners.ServerStarted;
import org.vimplugin.listeners.TextInsert;
import org.vimplugin.listeners.TextRemoved;
import org.vimplugin.preferences.PreferenceConstants;
/**
* Manage the communication channel with Vim. This is the main interface to a
* Vim instance. Important functions are: start/close tcp communication, and
* sending of commands/functions. The protocol is explained in detail at vimdoc.
* Events generated by Vim are consumed by
* {@link org.vimplugin.listeners.IVimListener Listeners} (ObserverPattern).
*
* @see <a
* href="http://www.vim.org/htmldoc/netbeans.html#netbeans-protocol">Protocol
* specification</a>
*
*/
public class VimConnection implements Runnable
{
private static final org.eclim.logging.Logger logger =
org.eclim.logging.Logger.getLogger(VimConnection.class);
/* pattern to match events lines to differencient from function response while
waiting on function response */
private static final Pattern EVENT = Pattern.compile("^\\d+:\\w+=\\d+.*");
/** is a vim instance running? */
private boolean serverRunning = false;
/** did the vim instance report "startupDone"? */
private boolean startupDone = false;
/** the set of VimListeners. Observer-Pattern. */
final HashSet<IVimListener> listeners = new HashSet<IVimListener>();
/** the id of the calling vim instance (as given in VimServer) */
final int vimID;
/** the channel we can get messages from */
private BufferedReader in;
/** the channel we can write messages to */
private PrintWriter out;
private final int port;
private ServerSocket socket;
/** the socket the vim instance runs on */
private Socket vimSocket;
private volatile boolean functionCalled = false;
private Object functionMonitor = new Object();
private String functionResult;
/** creates a connection object (but does not start the connection ..). */
public VimConnection(int instanceID) {
port = VimPlugin.getDefault().getPreferenceStore().getInt(
PreferenceConstants.P_PORT);
vimID = instanceID;
}
/**
* Establishes a TCP-Connection, adds
* {@link org.vimplugin.listeners.IVimListener listeners} and creates VimEvents to be
* consumed by the listeners.
*
* @see java.lang.Runnable#run()
*/
public void run() {
try {
// start server
logger.debug("Server starting on port " + (port + vimID));
socket = new ServerSocket(port + vimID);
logger.debug("Server started and listening");
// accept client
vimSocket = socket.accept();
out = new PrintWriter(vimSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(vimSocket
.getInputStream()));
logger.debug("Connection established");
// Add Listeners
listeners.add(new Logger());
listeners.add(new ServerStarted());
listeners.add(new ServerDisconnect());
listeners.add(new TextInsert());
listeners.add(new TextRemoved());
listeners.add(new FileOpened());
listeners.add(new FileClosed());
listeners.add(new FileModified());
listeners.add(new FileUnmodified());
listeners.add(new BufferEnter());
listeners.add(new FeedKeys());
// handle Events
String line;
try {
while (!startupDone && (line = in.readLine()) != null) {
//ignore "special messages" (see :help nb-special)
if (!line.startsWith("AUTH")) {
VimEvent ve = new VimEvent(line, this);
for (IVimListener listener : listeners) {
listener.handleEvent(ve);
}
}
}
} catch (VimException ve) {
// TODO : better ErrorHandling (Connection Thread)
logger.error("error:", ve);
}
try {
while ((serverRunning && (line = in.readLine()) != null)) {
if(functionCalled && !EVENT.matcher(line).matches()){
synchronized(functionMonitor){
functionResult = line;
functionMonitor.notify();
}
}else{
VimEvent ve = new VimEvent(line, this);
for (IVimListener listener : listeners) {
listener.handleEvent(ve);
}
}
}
} catch (SocketException se) {
// the connection to vim was closed, so close the editor.
VimPlugin plugin = VimPlugin.getDefault();
VimServer server = plugin.getVimserver(getVimID());
if (server != null){
for (VimEditor editor : server.getEditors()){
if (editor != null) {
editor.forceDispose();
}
}
}
close();
}
} catch (Exception e) {
logger.error("error:", e);
throw new RuntimeException(e);
}
}
/**
* shuts down the TCP-Connection to the vim instance.
*
* @return always true.
*/
public boolean close() throws IOException {
if (vimSocket != null){
vimSocket.close();
}
if (socket != null){
socket.close();
}
serverRunning = false;
return true;
}
/**
* Sends a <i>command</i> (no replay) as specified by the netbeans-protocol
* to the vim instance.
*
* @param bufID the vim buffer that is adressed
* @param name the "name" of the command
* @param param possible parameters
* @see <a
* href="http://www.vim.org/htmldoc/netbeans.html#netbeans-protocol">Protocol
* specification</a>
*/
public void command(int bufID, String name, String param) {
int seqno = VimPlugin.getDefault().nextSeqNo();
String cmd = bufID + ":" + name + "!" + seqno + " " + param;
logger.debug("command: " + cmd);
out.println(cmd);
}
/**
* Sends a <i>function</i> (reply with a String as return value) as
* specified by the netbeans-protocol to the vim instance.
*
* @param bufID the vim buffer that is adressed
* @param name the "name" of the command
* @param param possible parameters
* @return the return value of the (vim)function or null if the function was
* "saveAndExit".
*/
public String function(int bufID, String name, String param)
throws IOException
{
int seqno = VimPlugin.getDefault().nextSeqNo();
String tmp = bufID + ":" + name + "/" + seqno + " " + param;
logger.debug("function: " + tmp);
// FIXME: need to find a better way to handle reading of function result
// while run() is continously reading _all_ input from gvim.
// FIXME: make use of the seqno for recognizing a function response.
synchronized(functionMonitor){
functionCalled = true;
out.println(tmp);
if ("saveAndExit".equals(name)){
return null;
}
try{
functionMonitor.wait(1000);
}catch(InterruptedException ie){
logger.error("Interrupted while waiting for function result.");
return null;
}
}
String result = functionResult;
functionResult = null;
logger.debug("result: " + result);
return result;
}
/**
* Sends a plain string to the vim instance. The user is responsible to
* comply to the protocol syntax.
*
* @param s the string to send.
* @see <a
* href="http://www.vim.org/htmldoc/netbeans.html#netbeans-protocol">Protocol
* specification</a>
*/
public void plain(String s) {
logger.debug("plain: " + s);
out.println(s);
}
/**
* Executes a --remote-send to the gvim instance.
*
* @param s The string to supply to the remote-send call.
*/
public void remotesend(String s) {
String[] args = {
"vim", "--servername", String.valueOf(vimID), "--remote-send", s
};
try {
logger.debug("remote-send: " + s);
Process process = new ProcessBuilder(args).start();
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
logger.debug(line);
}
process.waitFor();
logger.debug("result: " + process.exitValue());
} catch (IOException ioe) {
logger.error("Error sending command.", ioe);
} catch (InterruptedException ie) {
logger.error("Error sending command.", ie);
}
}
/**
* Adds a Listener to the list of observers. On each event all listeners are
* informed about the event and may react to it. (Observer-Pattern).
*
* @param vl the new listener.
*/
public void addListener(IVimListener vl) {
listeners.add(vl);
}
/**
* Simple Getter.
*
* @return the id of this connection
*/
public int getVimID() {
return vimID;
}
/**
* Simple Setter.
*
* @param startupDone
*/
public void setStartupDone(boolean startupDone) {
this.startupDone = startupDone;
}
/**
* Simple Getter.
*
* @return did Vim threw already "startupDone" Message?
*/
public boolean isStartupDone() {
return startupDone;
}
/**
* Simple Setter.
*
* @param serverRunning
*/
public void setServerRunning(boolean serverRunning) {
this.serverRunning = serverRunning;
}
/**
* Simple getter.
*
* @return whether a vim instance is running.
*/
public boolean isServerRunning() {
return serverRunning;
}
}