Saturday, August 22, 2009

clojure tab completion on windows

windows sucks because it does.

so, clji.bat:

@echo off
setlocal
set CLOJURE_HOME=%~dp0\clojure
set CLASS_PATH=%CLOJURE_HOME%\jline-0.9.94.jar;%CLOJURE_HOME%\clojure.jar;%CLOJURE_HOME%\clojure-contrib.jar
java -Djline.words="%CLOJURE_HOME%\all_names.txt" -cp "%CLASS_PATH%" jline.ConsoleRunner clojure.main %*
endlocal

clj.bat:

@echo off
setlocal
set CLOJURE_HOME=%~dp0\clojure
set CLASS_PATH=%CLOJURE_HOME%\jline-0.9.94.jar;%CLOJURE_HOME%\clojure.jar;%CLOJURE_HOME%\clojure-contrib.jar
IF (%1)==() (
    java -Djline.words="%CLOJURE_HOME%\all_names.txt" -cp "%CLASS_PATH%" jline.ConsoleRunner clojure.main
) ELSE (
    java -Djline.words="%CLOJURE_HOME%\all_names.txt" -cp "%CLASS_PATH%" clojure.main %1 -- %*
)
endlocal

clji.bat always starts repl. clj.bat starts repl when no argument is passed. otherwise, it runs the first argument (should be .clj script). it expects there's clojure/ directory at the same level of the batch files. the directory should have some jar files (clojure.jar, clojure-contrib.jar, jline..) and also clojure/ contains allnames.txt that is generated by allnames.clj:

(def all-names (concat (map (fn [p] (keys (ns-publics (find-ns p))))
    '(clojure.core clojure.set clojure.xml clojure.zip))))

(println (apply str
    (interleave (reduce concat all-names)
                (repeat "\n"))))

to run it:

$ clj all_names.clj > all_names.txt

now, modified ConsoleRunner.java (from http://jline.sf.net) that'll actually use all_names.txt:

/*
 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 */
package jline;

import java.io.*;
import java.util.*;

/**
 *  <p>
 *  A pass-through application that sets the system input stream to a
 *  {@link ConsoleReader} and invokes the specified main method.
 *  </p>
 *  @author  <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
 */
public class ConsoleRunner {
    public static final String HISTORY = "jline.history";
    public static final String WORDS = "jline.words";

    public static String[] getWords(String wordsFile) {
        List result = new ArrayList();
        try {
            BufferedReader input = new BufferedReader(
                new FileReader(wordsFile)
            );
            try {
                String line = null;
                while (null != (line = input.readLine())) {
                    result.add(line);
                }
            } finally {
                input.close();
            }
        } catch (FileNotFoundException err) {
            err.printStackTrace();
        } catch (IOException err) {
            err.printStackTrace();
        }
        String[] array = new String[result.size()];
        result.toArray(array);
        return array;
    }

    public static void main(final String[] args) throws Exception {
        String historyFileName = null;

        List argList = new ArrayList(Arrays.asList(args));

        if (argList.size() == 0) {
            usage();

            return;
        }

        historyFileName = System.getProperty(ConsoleRunner.HISTORY, null);

        // invoke the main() method
        String mainClass = (String) argList.remove(0);

        // setup the inpout stream
        ConsoleReader reader = new ConsoleReader();

        if (historyFileName != null) {
            reader.setHistory(new History (new File
                (System.getProperty("user.home"),
                    ".jline-" + mainClass
                        + "." + historyFileName + ".history")));
        } else {
            reader.setHistory(new History(new File
                (System.getProperty("user.home"),
                    ".jline-" + mainClass + ".history")));
        }

        String wordsFileName = System.getProperty(WORDS, null);
        if (null != wordsFileName) {
            //List list = new ArrayList();
            //list.add(new SimpleCompletor(getWords(wordsFileName)));
            //list.add(new FileNameCompletor());
            //list.add(new NullCompletor());
            Completor completor =
                new MultiCompletor(new Completor[] {
                    new SimpleCompletor(getWords(wordsFileName))
                    , new FileNameCompletor()
                });
            reader.addCompletor(
                new ArgumentCompletor(new Completor[] {
                    completor
                    , completor
                }, new SExpr())
            );
        } else {
        }

        String completors = System.getProperty
            (ConsoleRunner.class.getName() + ".completors", "");
        List completorList = new ArrayList();


        for (StringTokenizer tok = new StringTokenizer(completors, ",");
            tok.hasMoreTokens();) {
            completorList.add
                ((Completor) Class.forName(tok.nextToken()).newInstance());
        }

        if (completorList.size() > 0) {
            reader.addCompletor(new ArgumentCompletor(completorList));
        }

        //reader.setCompletionHandler(new CandidateListCompletionHandler());

        ConsoleReaderInputStream.setIn(reader);

        try {
            Class.forName(mainClass).
                getMethod("main", new Class[] { String[].class }).
                invoke(null, new Object[] { argList.toArray(new String[0]) });
        } finally {
            // just in case this main method is called from another program
            ConsoleReaderInputStream.restoreIn();
        }
    }

    private static void usage() {
        System.out.println("Usage: \n   java " + "[-Djline.history='name'] "
            + "[-Djline.words=<path to words file for autocompletion>] "
            + ConsoleRunner.class.getName()
            + " <target class name> [args]"
            + "\n\nThe -Djline.history option will avoid history"
            + "\nmangling when running ConsoleRunner on the same application."
            + "\n\nargs will be passed directly to the target class name.");
    }
}

class SExpr
        extends ArgumentCompletor.AbstractArgumentDelimiter {
    public boolean isDelimiterChar(String buffer, int pos) {
        String c = buffer.substring(pos, pos+1);
        return c.matches("[^\\w-*<>=?]");
    }
}

if -Djline.words=path/to/file/that/has/word/list is passed, it'll use that file to do tab completion. it's quick and dirty. i don't get how jline works. somehow, i had to pass completor (new MultiCompletor(...)) twice to ArgumentCompletor so that tab completion works into middle of line. Also, SExpr is argument delimiter that considers things other than [\w-*<>=?] a delimiter.

so, run mvn package to build jline jar. then put jline jar file under clojure/. then clj.bat and clji.bat would work.

i always spend time to set up development environment then lose interest :P