/**
* @author Charles McGarvey
- *
+ * The TopCoder Arena editor plug-in providing support for Vim.
+ *
+ * Distributable under the terms and conditions of the 2-clause BSD license;
+ * see the file COPYING for a complete text of the license.
*/
public class Editor
{
+ /**
+ * The problem ID number.
+ */
private String id;
+
+ /**
+ * The name of the class.
+ */
private String name;
+
+ /**
+ * The path of the current source file.
+ */
private File sourceFile;
+
+ /**
+ * The path of the problem directory.
+ */
private File directory;
+ /**
+ * Map languages names to file extensions.
+ */
private static final Map<String,String> languageExtension = new HashMap<String,String>();
static
{
}
+ /**
+ * Construct an editor with the problem objects given us by the Arena.
+ * @param component A container for the particulars of the problem.
+ * @param language The currently selected language.
+ * @param renderer A helper object to help format the problem statement.
+ * @throws Exception If the editor could not set itself up.
+ */
public Editor(ProblemComponentModel component,
Language language,
- Renderer renderer) throws IOException
+ Renderer renderer) throws Exception
{
this.id = String.valueOf(component.getProblem().getProblemID());
this.name = component.getClassName();
+ // Make sure the top-level vimcoder directory exists.
File topDir = new File(System.getProperty("user.home"), ".vimcoder");
if (!topDir.isDirectory())
{
if (!topDir.mkdirs()) throw new IOException(topDir.getPath());
}
+ // Make sure the problem directory exists.
this.directory = new File(topDir, id);
if (!directory.isDirectory())
{
String lang = language.getName();
String ext = languageExtension.get(lang);
+ // Set up the terms used for the template expansion.
HashMap<String,String> terms = new HashMap<String,String>();
- terms.put("RETURNTYPE", component.getReturnType().getDescriptor(language).replaceAll("\\s+", ""));
+ terms.put("RETURNTYPE",component.getReturnType().getDescriptor(language).replaceAll("\\s+", ""));
terms.put("CLASSNAME", name);
terms.put("METHODNAME", component.getMethodName());
terms.put("METHODPARAMS", getMethodParams(component.getParamTypes(),
component.getParamNames(),
language));
- terms.put("METHODPARAMNAMES", Utility.join(component.getParamNames(), ", "));
- terms.put("METHODPARAMSTREAMIN", Utility.join(component.getParamNames(), " >> "));
- terms.put("METHODPARAMSTREAMOUT", Utility.join(component.getParamNames(), " << "));
+ terms.put("METHODPARAMNAMES", Util.join(component.getParamNames(), ", "));
+ terms.put("METHODPARAMSTREAMIN", Util.join(component.getParamNames(), " >> "));
+ terms.put("METHODPARAMSTREAMOUT", Util.join(component.getParamNames(), " << "));
terms.put("METHODPARAMDECLARES", getMethodParamDeclarations(component.getParamTypes(),
component.getParamNames(),
language));
+ // Write the problem statement as an HTML file in the problem directory.
File problemFile = new File(directory, "Problem.html");
if (!problemFile.canRead())
{
{
writer.write(renderer.toHTML(language));
}
- catch (Exception exception)
+ finally
{
+ writer.close();
}
- writer.close();
}
+ // Expand the template for the main class and write it to the current
+ // source file.
sourceFile = new File(directory, name + "." + ext);
if (!sourceFile.canRead())
{
- String text = Utility.expandTemplate(Utility.readResource(lang + "Template"),
- terms);
+ String text = Util.expandTemplate(Util.readResource(lang + "Template"),
+ terms);
FileWriter writer = new FileWriter(sourceFile);
writer.write(text);
writer.close();
}
+
+ // Expand the driver template and write it to a source file.
+ File driverFile = new File(directory, "driver." + ext);
+ if (!driverFile.canRead())
+ {
+ String text = Util.expandTemplate(Util.readResource(lang + "Driver"),
+ terms);
+ FileWriter writer = new FileWriter(driverFile);
+ writer.write(text);
+ writer.close();
+ }
+ // Write the test cases to a text file. The driver code can read this
+ // file and perform the tests based on what it reads.
File testcaseFile = new File(directory, "testcases.txt");
if (!testcaseFile.canRead())
{
writer.close();
}
- File driverFile = new File(directory, "driver." + ext);
- if (!driverFile.canRead())
- {
- String text = Utility.expandTemplate(Utility.readResource(lang + "Driver"),
- terms);
- FileWriter writer = new FileWriter(driverFile);
- writer.write(text);
- writer.close();
- }
-
+ // Finally, expand the Makefile template and write it.
File makeFile = new File(directory, "Makefile");
{
- String text = Utility.expandTemplate(Utility.readResource(lang + "Makefile"),
+ String text = Util.expandTemplate(Util.readResource(lang + "Makefile"),
terms);
FileWriter writer = new FileWriter(makeFile);
writer.write(text);
}
}
+ /**
+ * Save the source code provided by the server, and tell the Vim server to
+ * edit the current source file.
+ * @param source The source code.
+ * @throws Exception If the source couldn't be written or the Vim server
+ * had a problem.
+ */
public void setSource(String source) throws Exception
{
FileWriter writer = new FileWriter(new File(directory, name));
writer.write(source);
writer.close();
- doVimCommand("--remote-tab-silent", sourceFile.getPath());
+ sendVimCommand("--remote-tab-silent", sourceFile.getPath());
}
+ /**
+ * Read the source code from the current source file.
+ * @return The source code.
+ * @throws IOException If the source file could not be read.
+ */
public String getSource() throws IOException
{
- return Utility.readFile(sourceFile) + "\n// Edited by " + VimCoder.version + "\n// " + VimCoder.website + "\n\n";
+ return Util.readFile(sourceFile) + "\n// Edited by " +
+ VimCoder.version + "\n// " + VimCoder.website + "\n\n";
}
- private void doVimCommand(String command, String argument) throws Exception
+ /**
+ * Send a command to the Vim server. If the server isn't running, it will
+ * be started with the name VIMCODER#### where #### is the problem ID.
+ * @param command The command to send to the server.
+ * @param argument A single argument for the remote command.
+ * @throws Exception If the command could not be sent.
+ */
+ private void sendVimCommand(String command,
+ String argument) throws Exception
{
String[] arguments = {argument};
- doVimCommand(command, arguments);
+ sendVimCommand(command, arguments);
}
- private void doVimCommand(String command, String[] arguments) throws Exception
+ /**
+ * Send a command to the Vim server. If the server isn't running, it will
+ * be started with the name VIMCODER#### where #### is the problem ID.
+ * @param command The command to send to the server.
+ * @param argument Arguments for the remote command.
+ * @throws Exception If the command could not be sent.
+ */
+ private void sendVimCommand(String command,
+ String[] arguments) throws Exception
{
- String[] exec = {"gvim", "--servername", "VimCoder" + id,
- command};
- exec = Utility.concat(exec, arguments);
+ String[] exec = {"gvim", "--servername", "VimCoder" + id, command};
+ exec = Util.concat(exec, arguments);
Process child = Runtime.getRuntime().exec(exec, null, directory);
- long expire = System.currentTimeMillis() + 500;
+ /* FIXME: This is a hack with a magic number. The problem is the Vim
+ * process doesn't fork to the background on some systems, so we can't
+ * wait on the child. At the same time, calling this method before the
+ * previous child could finish initializing the server may result in
+ * multiple editor windows popping up. We'd also like to be able to
+ * get the return code from the child if we can. The workaround here is
+ * to stall the thread for a little while or until we know the child
+ * does exit. If the child never exits before the timeout, we will
+ * assume it is not backgrounding and that everything worked. This all
+ * works very well in practice, but perhaps there's a better way... */
+ long expire = System.currentTimeMillis() + 250;
while (System.currentTimeMillis() < expire)
{
+ Thread.yield();
try
{
int exitCode = child.exitValue();
}
catch (IllegalThreadStateException exception)
{
+ // The child has not exited; intentionally ignoring exception.
}
- Thread.yield();
}
}
+ /**
+ * Convert an array of data types to an array of strings according to a
+ * given language.
+ * @param types The data types.
+ * @param language The language to use in the conversion.
+ * @return The array of string representations of the data types.
+ */
+ private String[] getStringTypes(DataType[] types, Language language)
+ {
+ String[] strings = new String[types.length];
+ for (int i = 0; i < types.length; ++i)
+ {
+ strings[i] = types[i].getDescriptor(language).replaceAll("\\s+", "");
+ }
+ return strings;
+ }
+
+ /**
+ * Combine the data types and parameter names into a comma-separated list of
+ * the method parameters. The result could be used inside the parentheses
+ * of a method declaration.
+ * @param types The data types of the parameters.
+ * @param names The names of the parameters.
+ * @param language The language used for representing the data types.
+ * @return The list of parameters.
+ */
private String getMethodParams(DataType[] types,
String[] names,
Language language)
{
- StringBuilder text = new StringBuilder();
-
- text.append(types[0].getDescriptor(language).replaceAll("\\s+", "") + " " + names[0]);
- for (int i = 1; i < names.length; ++i)
- {
- text.append(", " + types[i].getDescriptor(language).replaceAll("\\s+", "") + " " + names[i]);
- }
-
- return text.toString();
+ String[] typeStrings = getStringTypes(types, language);
+ return Util.join(Util.combine(typeStrings, names, " "), ", ");
}
- private String getMethodParamDeclarations (DataType[] types,
+ /**
+ * Combine the data types and parameter names into a group of variable
+ * declarations. Each declaration is separated by a new line and terminated
+ * with a semicolon.
+ * @param types The data types of the parameters.
+ * @param names The names of the parameters.
+ * @param language The language used for representing the data types.
+ * @return The parameters as a block of declarations.
+ */
+ private String getMethodParamDeclarations(DataType[] types,
String[] names,
Language language)
{
- StringBuilder text = new StringBuilder();
-
- for (int i = 0; i < names.length; ++i)
- {
- text.append(types[i].getDescriptor(language).replaceAll("\\s+", "") + "\t" + names[i] + ";" + System.getProperty("line.separator"));
- }
-
- return text.toString();
+ final String end = ";" + System.getProperty("line.separator");
+ String[] typeStrings = getStringTypes(types, language);
+ return Util.join(Util.combine(typeStrings, names, "\t"), end) + end;
}
}
--- /dev/null
+
+package com.dogcows;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * @author Charles McGarvey
+ * The TopCoder Arena editor plug-in providing support for Vim.
+ *
+ * Distributable under the terms and conditions of the 2-clause BSD license;
+ * see the file COPYING for a complete text of the license.
+ */
+public abstract class Util
+{
+ /**
+ * Concatenate two arrays into a single array.
+ * @param a First array.
+ * @param b Second array.
+ * @return The combined array.
+ */
+ public static <T> T[] concat(T[] a, T[] b)
+ {
+ T[] result = Arrays.copyOf(a, a.length + b.length);
+ System.arraycopy(b, 0, result, a.length, b.length);
+ return result;
+ }
+
+ /**
+ * Combined string elements from two arrays into a single array, gluing
+ * together elements of the same index with a delimiter string.
+ * @param a First string array.
+ * @param b Second string array.
+ * @param glue The delimiter string.
+ * @return The combined array.
+ */
+ public static String[] combine(String[] a, String[] b, String glue)
+ {
+ String[] result = new String[Math.min(a.length, b.length)];
+ for (int i = 0; i < result.length; ++i)
+ {
+ result[i] = a[i] + glue + b[i];
+ }
+ return result;
+ }
+
+ /**
+ * Join the elements of a string array with a delimiter.
+ * @param a The array.
+ * @param glue The delimiter string.
+ * @return The joined string.
+ */
+ public static String join(String[] a, String glue)
+ {
+ if (a.length == 0) return "";
+ StringBuilder result = new StringBuilder();
+ result.append(a[0]);
+ for (int i = 1; i < a.length; ++i) result.append(glue).append(a[i]);
+ return result.toString();
+ }
+
+ /**
+ * Quote a string by replacing prepending backslashes and double
+ * quotation characters with an extra backslash.
+ * @param The string to be quoted.
+ * @return The quoted string.
+ */
+ public static String quote(String a)
+ {
+ a = a.replaceAll("\\\\", "\\\\\\\\");
+ a = a.replaceAll("\"", "\\\\\\\"");
+ return a;
+ }
+
+ /**
+ * Simply read a file's contents into a string object.
+ * @param file The file to read.
+ * @return The contents of the file.
+ * @throws IOException If the file is not readable.
+ */
+ public static String readFile(File file) throws IOException
+ {
+ StringBuilder text = new StringBuilder();
+
+ BufferedReader reader = new BufferedReader(new FileReader(file.getPath()));
+ try
+ {
+ String line = null;
+
+ while ((line = reader.readLine()) != null)
+ {
+ text.append(line + System.getProperty("line.separator"));
+ }
+ }
+ finally
+ {
+ reader.close();
+ }
+
+ return text.toString();
+ }
+
+ /**
+ * Read a resource file into a string object. The resources should be
+ * placed in the directory `resources' underneath the parent directory of
+ * this class. Reading resources packaged in a jar is allowable.
+ * @param path Relative path to the resource.
+ * @return The contents of the resource.
+ * @throws IOException If the resource is not readable.
+ */
+ public static String readResource(String path) throws IOException
+ {
+ StringBuilder text = new StringBuilder();
+
+ InputStream stream = Util.class.getResourceAsStream("resources/" + path);
+ if (stream != null)
+ {
+ try
+ {
+ byte[] buffer = new byte[4096];
+ int numBytes = 0;
+ while (0 < (numBytes = stream.read(buffer)))
+ {
+ text.append(new String(buffer, 0, numBytes));
+ }
+ }
+ finally
+ {
+ stream.close();
+ }
+ }
+
+ return text.toString();
+ }
+
+ /**
+ * The poor man's template package. Provide a template and a map of terms
+ * to build the result with the terms expanded into the template. Terms
+ * in the template should appear surrounded with dollar signs. For example,
+ * if $MYTERM$ appears in the template, it will be replaced by the value
+ * into the terms map with the key MYTERM (if it exists in the map).
+ * @param template The template string.
+ * @param terms A map of key/value terms.
+ * @return The string expanded from the template and terms.
+ */
+ public static String expandTemplate(String template, Map<String,String> terms)
+ {
+ String text = template;
+ for (String key : terms.keySet())
+ {
+ text = text.replaceAll("\\$" + key + "\\$",
+ Util.quote(terms.get(key)));
+ }
+ return text;
+ }
+}
+
public class VimCoder
{
/**
- *
+ * The name and version of this plugin.
*/
public final static String version = "VimCoder 0.1";
+
+ /**
+ * The website of the plugin project.
+ */
public final static String website = "http://www.dogcows.com/vimcoder";
+
+ /**
+ * The panel given to the Arena applet when it is requested.
+ */
private JPanel panel;
+
+ /**
+ * The text widget where log messages are appended.
+ */
private JTextArea logArea;
+ /**
+ * The current editor object (or null if there is none).
+ */
private Editor editor;
+ /**
+ * Instantiate the entry point of the editor plugin. Sets up the log widget
+ * and panel.
+ */
public VimCoder()
{
logArea = new JTextArea();
}
+ /**
+ * Called by the Arena when the plugin is about to be used.
+ */
public void startUsing()
{
System.out.println("startUsing");
}
}
+ /**
+ * Called by the Arena when the plugin is no longer needed.
+ */
public void stopUsing()
{
System.out.println("stopUsing");
editor = null;
}
+ /**
+ * Called by the Arena to obtain the editor panel which we will use to show
+ * log messages.
+ * @return The editor panel.
+ */
public JPanel getEditorPanel()
{
System.out.println("getEditorPanel");
return panel;
}
+ /**
+ * Called by the Arena to obtain the current source. This happens when the
+ * user is saving, compiling, and/or submitting.
+ * @return The current source code.
+ * @throws Exception If the source file edited by Vim couldn't be read.
+ */
public String getSource() throws Exception
{
System.out.println("getSource");
}
catch (Exception exception)
{
- logError("Failed to get source code: " + exception.getLocalizedMessage());
+ logError("Failed to get source code: " +
+ exception.getLocalizedMessage());
throw exception;
}
}
-
+
+ /**
+ * Called by the Arena to pass the source it has.
+ * @param source The source code.
+ */
public void setSource(String source)
{
System.out.println("setSource: " + source);
}
catch (Exception exception)
{
- logError("Failed to save the source given by the server: " + exception.getLocalizedMessage());
+ logError("Failed to save the source given by the server: " +
+ exception.getLocalizedMessage());
return;
}
}
+ /**
+ * Called by the Arena to pass along information about the current problem.
+ * @param component A container for the particulars of the problem.
+ * @param language The currently selected language.
+ * @param renderer A helper object to help format the problem statement.
+ */
public void setProblemComponent(ProblemComponentModel component,
Language language,
Renderer renderer)
}
catch (Exception exception)
{
- logError("An error occured while loading the problem: " + exception.getLocalizedMessage());
+ logError("An error occured while loading the problem: " +
+ exception.getLocalizedMessage());
}
}
+ /**
+ * A generic logging function, appends text to the text area. A timestamp
+ * is also prepended to the next text.
+ * @param what The text to append.
+ */
private void log(final String what)
{
Runnable task = new Runnable()
}
}
+ /**
+ * Output non-critical messages to the log.
+ * @param what The text of the message.
+ */
private void logInfo(String what)
{
log(" INFO: " + what + System.getProperty("line.separator"));
}
+ /**
+ * Output potentially important messages to the log.
+ * @param what The text of the message.
+ */
private void logWarning(String what)
{
log(" WARN: " + what + System.getProperty("line.separator"));
}
+ /**
+ * Output critical messages and errors to the log.
+ * @param what The text of the message.
+ */
private void logError(String what)
{
log("ERROR: " + what + System.getProperty("line.separator"));
}
-
-
- public static void main(String args[])
- {
- VimCoder plugin = new VimCoder();
-
- JFrame frame = new JFrame("VimCoder");
- frame.add(plugin.getEditorPanel());
- frame.setSize(640, 480);
- frame.setVisible(true);
- frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-
- plugin.startUsing();
- }
}