├── .gitignore ├── README.md ├── clopad.png ├── pom.xml ├── samples └── defpure.clj └── src ├── main └── java │ ├── Advent.java │ ├── ArrayCharSequence.java │ ├── Clojure.java │ ├── ClojureIndenter.java │ ├── Console.java │ ├── DocumentAdapter.java │ ├── Flexer.java │ ├── FreditorUI_symbol.java │ ├── FreditorWriter.java │ ├── ISeqSpliterator.java │ ├── Java.java │ ├── Main.java │ ├── MainFrame.java │ ├── NamespaceExplorer.java │ ├── PrintFormToWriter.java │ └── SpecialForm.java └── test └── java └── ClojureIndenterTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.idea/ 3 | /*.iml 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![clopad](clopad.png) 2 | 3 | ## Background 4 | 5 | Do you struggle with setting up complicated Clojure development environments just to define your first function and evaluate some expressions? 6 | Welcome to Clopad, a minimalistic Clojure code editor that will support you through your first steps! 7 | 8 | ## How do I compile clopad into an executable jar? 9 | ``` 10 | git clone https://github.com/fredoverflow/freditor 11 | cd freditor 12 | mvn install 13 | cd .. 14 | git clone https://github.com/fredoverflow/clopad 15 | cd clopad 16 | mvn package 17 | ``` 18 | The executable `clopad-x.y.z-SNAPSHOT-jar-with-dependencies.jar` will be located inside the `target` folder. 19 | -------------------------------------------------------------------------------- /clopad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fredoverflow/clopad/83152b12a59af4ea2c3130df529091ab5afc9cbf/clopad.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | fredoverflow 6 | clopad 7 | 0.1.0-SNAPSHOT 8 | 9 | 10 | UTF-8 11 | 11 12 | 11 13 | 11 14 | Main 15 | 16 | 17 | 18 | 19 | fredoverflow 20 | freditor 21 | 0.1.0-SNAPSHOT 22 | 23 | 24 | 25 | org.clojure 26 | clojure 27 | 1.11.1 28 | 29 | 30 | 31 | junit 32 | junit 33 | 4.13.1 34 | test 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-assembly-plugin 43 | 3.6.0 44 | 45 | 46 | make-assembly 47 | package 48 | 49 | single 50 | 51 | 52 | 53 | 54 | ${main.class} 55 | 56 | 57 | 58 | jar-with-dependencies 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /samples/defpure.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.string :as string] 3 | [clojure.test :refer [do-report run-tests]])) 4 | 5 | 6 | 7 | (defn- filter-stack-trace! [^Throwable throwable] 8 | (->> (for [^StackTraceElement element (. throwable getStackTrace) 9 | :when (. *source-path* equals (. element getFileName))] 10 | element) 11 | (into-array StackTraceElement) 12 | (. throwable setStackTrace)) 13 | throwable) 14 | 15 | (defn- register-test [v, inputs->output-map, do-report] 16 | (alter-meta! v assoc :test 17 | #(doseq [[inputs output] inputs->output-map] 18 | (try 19 | (let [actual (apply @v inputs)] 20 | (do-report {:type (if (= output actual) :pass :fail) 21 | :message (str " inputs: " inputs) 22 | :expected output 23 | :actual actual})) 24 | (catch Throwable throwable 25 | (do-report {:type :error 26 | :message (str " inputs: " inputs) 27 | :expected output 28 | :actual (filter-stack-trace! throwable)})))))) 29 | 30 | (defmacro defpure 31 | "Defines a pure function, illustrated by an exemplary inputs->output map" 32 | [name, inputs->output-map & rest] 33 | `(do 34 | (defn ~name ~@rest) 35 | (register-test (var ~name) ~inputs->output-map #(do-report %)))) 36 | 37 | 38 | 39 | (defpure square 40 | {[0] 0 41 | [2] 4 42 | [3] 9} 43 | "squares its input" 44 | [^Number x] 45 | (/ x x)) 46 | 47 | 48 | 49 | (run-tests) 50 | -------------------------------------------------------------------------------- /src/main/java/Advent.java: -------------------------------------------------------------------------------- 1 | import java.io.IOException; 2 | import java.net.HttpURLConnection; 3 | import java.net.URI; 4 | import java.net.http.HttpClient; 5 | import java.net.http.HttpRequest; 6 | import java.net.http.HttpResponse; 7 | import java.nio.file.Files; 8 | import java.nio.file.NoSuchFileException; 9 | import java.nio.file.Paths; 10 | import java.time.*; 11 | import java.time.temporal.ChronoUnit; 12 | 13 | public class Advent { 14 | public static final String CACHE_FOLDER = System.getProperty("user.home") + "/Downloads"; 15 | public static final String CACHE_FORMAT = "%d_%02d.txt"; 16 | public static final String REMOTE_FORMAT = "https://adventofcode.com/%d/day/%d/input"; 17 | public static final String SESSION_COOKIE = "advent.txt"; 18 | public static final ZoneId RELEASE_ZONE = ZoneId.of("US/Eastern"); 19 | 20 | public static String get(int year, int day) throws IOException, InterruptedException { 21 | valiDate(year, day); 22 | String content; 23 | 24 | var cache = Paths.get(CACHE_FOLDER, String.format(CACHE_FORMAT, year, day)); 25 | try { 26 | content = Files.readString(cache); 27 | } catch (NoSuchFileException notCachedYet) { 28 | content = downloadContent(year, day); 29 | Files.writeString(cache, content); 30 | } 31 | return content; 32 | } 33 | 34 | private static String downloadContent(int year, int day) throws IOException, InterruptedException { 35 | var client = HttpClient.newBuilder().build(); 36 | 37 | var session = Files.readString(Paths.get(CACHE_FOLDER, SESSION_COOKIE)).trim(); 38 | 39 | var request = HttpRequest.newBuilder() 40 | .uri(URI.create(String.format(REMOTE_FORMAT, year, day))) 41 | .header("Cookie", "session=" + session) 42 | .GET() 43 | .build(); 44 | 45 | var response = client.send(request, HttpResponse.BodyHandlers.ofString()); 46 | 47 | if (response.statusCode() == HttpURLConnection.HTTP_OK) { 48 | return response.body(); 49 | } else { 50 | throw new IOException("HTTP status code " + response.statusCode() + ", session cookie length " + session.length()); 51 | } 52 | } 53 | 54 | private static void valiDate(int year, int day) { 55 | if (!between(1, day, 25)) throw new IllegalArgumentException("illegal day " + day); 56 | var now = ZonedDateTime.now(RELEASE_ZONE); 57 | if (!between(2015, year, now.getYear())) throw new IllegalArgumentException("illegal year " + year); 58 | 59 | var desired = ZonedDateTime.of(LocalDate.of(year, Month.DECEMBER, day), LocalTime.MIDNIGHT, RELEASE_ZONE); 60 | var seconds = ChronoUnit.SECONDS.between(now, desired); 61 | if (seconds > 0) { 62 | var hours = ChronoUnit.HOURS.between(now, desired); 63 | if (hours > 0) { 64 | throw new IllegalArgumentException(hours + " hours until release..."); 65 | } 66 | throw new IllegalArgumentException(seconds + " seconds until release..."); 67 | } 68 | } 69 | 70 | private static boolean between(int min, int value, int max) { 71 | return min <= value && value <= max; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/ArrayCharSequence.java: -------------------------------------------------------------------------------- 1 | public class ArrayCharSequence implements CharSequence { 2 | private final char[] array; 3 | private final int offset; 4 | private final int length; 5 | 6 | public ArrayCharSequence(char[] array, int offset, int length) { 7 | this.array = array; 8 | this.offset = offset; 9 | this.length = length; 10 | } 11 | 12 | @Override 13 | public int length() { 14 | return length; 15 | } 16 | 17 | @Override 18 | public char charAt(int index) { 19 | return array[offset + index]; 20 | } 21 | 22 | @Override 23 | public CharSequence subSequence(int start, int end) { 24 | return new ArrayCharSequence(array, offset + start, end - start); 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return new String(array, offset, length); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/Clojure.java: -------------------------------------------------------------------------------- 1 | import clojure.lang.Compiler; 2 | import clojure.lang.*; 3 | 4 | import java.io.IOException; 5 | import java.io.PushbackReader; 6 | import java.io.StringReader; 7 | import java.nio.file.Path; 8 | import java.util.function.Consumer; 9 | 10 | import static clojure.lang.Compiler.*; 11 | 12 | public class Clojure { 13 | public static final Keyword doc; 14 | public static final Symbol null_ns; 15 | public static final Symbol clojure_core_ns; 16 | public static final Var printLength; 17 | public static final Var warnOnReflection; 18 | 19 | public static final IFn macroexpand; 20 | public static final IFn macroexpandAll; 21 | public static final IFn nsInterns; 22 | public static final IFn pprint; 23 | public static final IFn sourceFn; 24 | 25 | static { 26 | IFn require = Var.find(Symbol.create("clojure.core", "require")); 27 | require.invoke(Symbol.create("clojure.pprint")); 28 | require.invoke(Symbol.create("clojure.repl")); 29 | require.invoke(Symbol.create("clojure.walk")); 30 | 31 | doc = Keyword.intern("doc"); 32 | null_ns = Symbol.create(null, "ns"); 33 | clojure_core_ns = Symbol.create("clojure.core", "ns"); 34 | printLength = Var.find(Symbol.create("clojure.core", "*print-length*")); 35 | warnOnReflection = Var.find(Symbol.create("clojure.core", "*warn-on-reflection*")); 36 | 37 | macroexpand = Var.find(Symbol.create("clojure.core", "macroexpand")); 38 | macroexpandAll = Var.find(Symbol.create("clojure.walk", "macroexpand-all")); 39 | nsInterns = Var.find(Symbol.create("clojure.core", "ns-interns")); 40 | pprint = Var.find(Symbol.create("clojure.pprint", "pprint")); 41 | sourceFn = Var.find(Symbol.create("clojure.repl", "source-fn")); 42 | } 43 | 44 | public static String firstForm(String text) { 45 | LineNumberingPushbackReader reader = new LineNumberingPushbackReader(new StringReader(text)); 46 | reader.captureString(); 47 | LispReader.read(reader, false, null, false, null); 48 | return reader.getString(); 49 | } 50 | 51 | public static void loadFromScratch(String text, Path file, 52 | Consumer resultContinuation) { 53 | Object[] result = new Object[1]; 54 | feedFormsBefore(text, file, Integer.MAX_VALUE, Integer.MAX_VALUE, form -> { 55 | if (isNamespaceForm(form)) { 56 | Namespace.remove((Symbol) ((PersistentList) form).next().first()); 57 | } 58 | result[0] = Compiler.eval(form, false); 59 | }, formAtCursor -> { 60 | resultContinuation.accept(result[0]); 61 | }); 62 | } 63 | 64 | public static boolean isNamespaceForm(Object form) { 65 | if (!(form instanceof PersistentList)) return false; 66 | Object first = ((PersistentList) form).first(); 67 | return null_ns.equals(first) || clojure_core_ns.equals(first); 68 | } 69 | 70 | public static void evaluateNamespaceFormsBefore(String text, Path file, 71 | int row, int column, 72 | Runnable updateNamespaces, 73 | Consumer formContinuation) { 74 | feedFormsBefore(text, file, row, column, form -> { 75 | if (isNamespaceForm(form)) { 76 | Compiler.eval(form, false); 77 | updateNamespaces.run(); 78 | } 79 | }, formContinuation); 80 | } 81 | 82 | private static void feedFormsBefore(String text, Path file, 83 | int row, int column, 84 | Consumer formConsumer, 85 | Consumer formContinuation) { 86 | LineNumberingPushbackReader reader = new LineNumberingPushbackReader(new StringReader(text)); 87 | 88 | Var.pushThreadBindings(RT.mapUniqueKeys(LOADER, RT.makeClassLoader(), 89 | SOURCE_PATH, file.toString(), 90 | SOURCE, file.getFileName().toString(), 91 | METHOD, null, 92 | LOCAL_ENV, null, 93 | LOOP_LOCALS, null, 94 | NEXT_LOCAL_NUM, 0, 95 | RT.READEVAL, RT.T, 96 | RT.CURRENT_NS, RT.CURRENT_NS.deref(), 97 | LINE, -1, 98 | COLUMN, -1, 99 | RT.UNCHECKED_MATH, RT.UNCHECKED_MATH.deref(), 100 | warnOnReflection, warnOnReflection.deref(), 101 | RT.DATA_READERS, RT.DATA_READERS.deref())); 102 | try { 103 | Object form = null; 104 | long rowColumn = combine(row, column); 105 | while (skipWhitespace(reader) != -1 && combine(reader.getLineNumber(), reader.getColumnNumber()) <= rowColumn) { 106 | LINE.set(reader.getLineNumber()); 107 | COLUMN.set(reader.getColumnNumber()); 108 | form = LispReader.read(reader, false, null, false, null); 109 | formConsumer.accept(form); 110 | } 111 | formContinuation.accept(form); 112 | } catch (LispReader.ReaderException ex) { 113 | throw new CompilerException(file.toString(), ex.line, ex.column, null, CompilerException.PHASE_READ, ex.getCause()); 114 | } catch (CompilerException ex) { 115 | throw ex; 116 | } catch (Throwable ex) { 117 | throw new CompilerException(file.toString(), (Integer) LINE.deref(), (Integer) COLUMN.deref(), ex); 118 | } finally { 119 | Var.popThreadBindings(); 120 | } 121 | } 122 | 123 | private static long combine(int hi, int lo) { 124 | return (long) hi << 32 | lo; 125 | } 126 | 127 | private static int skipWhitespace(PushbackReader reader) { 128 | try { 129 | int ch; 130 | do { 131 | ch = reader.read(); 132 | } while (Character.isWhitespace(ch) || ch == ','); 133 | if (ch != -1) { 134 | reader.unread(ch); 135 | } 136 | return ch; 137 | } catch (IOException ex) { 138 | throw Util.sneakyThrow(ex); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/ClojureIndenter.java: -------------------------------------------------------------------------------- 1 | import freditor.FlexerState; 2 | import freditor.Freditor; 3 | import freditor.Indenter; 4 | import freditor.ephemeral.IntStack; 5 | 6 | public class ClojureIndenter extends Indenter { 7 | public static final ClojureIndenter instance = new ClojureIndenter(); 8 | 9 | @Override 10 | public int[] corrections(Freditor text) { 11 | final int len = text.length(); 12 | final int rows = text.rows(); 13 | int[] corrections = new int[rows]; 14 | IntStack indentations = new IntStack(); 15 | int indentation = 0; 16 | int i = 0; 17 | for (int row = 0; row < rows; ++row) { 18 | int column = text.leadingSpaces(i); 19 | i += column; 20 | int correction = indentation - column; 21 | corrections[row] = correction; 22 | final int columns = text.lengthOfRow(row); 23 | for (; column < columns; ++column) { 24 | FlexerState state = text.stateAt(i++); 25 | if (state == Flexer.OPENING_BRACE || state == Flexer.OPENING_BRACKET) { 26 | indentations.push(indentation); 27 | indentation = column + correction + 1; 28 | } else if (state == Flexer.OPENING_PAREN) { 29 | indentations.push(indentation); 30 | indentation = column + correction + 2; 31 | 32 | if (i < len && text.stateAt(i) == Flexer.KEYWORD_HEAD) { 33 | final int start = i; 34 | i = skipKeywordAndSpaces(text, i); 35 | column += i - start; 36 | 37 | if (text.stateAt(i) != Flexer.NEWLINE) { 38 | indentation = column + 1 + correction; 39 | } 40 | } 41 | } else if (state == Flexer.CLOSING_BRACE || state == Flexer.CLOSING_BRACKET || state == Flexer.CLOSING_PAREN) { 42 | indentation = indentations.isEmpty() ? 0 : indentations.pop(); 43 | } 44 | } 45 | ++i; // new line 46 | } 47 | return corrections; 48 | } 49 | 50 | private int skipKeywordAndSpaces(Freditor text, int i) { 51 | while (text.stateAt(++i) == Flexer.KEYWORD_TAIL) { 52 | } 53 | if (text.stateAt(i) == Flexer.SPACE_HEAD) { 54 | while (text.stateAt(++i) == Flexer.SPACE_TAIL) { 55 | } 56 | } 57 | return i; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/Console.java: -------------------------------------------------------------------------------- 1 | import clojure.lang.Compiler; 2 | import clojure.lang.*; 3 | import freditor.FreditorUI; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.io.IOException; 8 | import java.io.PrintWriter; 9 | import java.io.StringWriter; 10 | import java.util.Arrays; 11 | import java.util.function.Supplier; 12 | 13 | public class Console { 14 | private static final int PRINT_LENGTH = 100; 15 | 16 | private final JTabbedPane tabs; 17 | private final FreditorUI output; 18 | private final Supplier input; 19 | 20 | private final FreditorWriter freditorWriter; 21 | public final PrintWriter printWriter; 22 | 23 | public Console(JTabbedPane tabs, FreditorUI output, Supplier input) { 24 | this.tabs = tabs; 25 | this.output = output; 26 | this.input = input; 27 | 28 | freditorWriter = new FreditorWriter(output); 29 | printWriter = new PrintWriter(freditorWriter); 30 | 31 | Compiler.SOURCE_PATH.bindRoot(input.get().getFile().toString()); 32 | Compiler.SOURCE.bindRoot(input.get().getFile().getFileName().toString()); 33 | RT.OUT.bindRoot(printWriter); 34 | RT.ERR.bindRoot(printWriter); 35 | Clojure.printLength.bindRoot(PRINT_LENGTH); 36 | } 37 | 38 | public void run(boolean setCursor, Runnable body) { 39 | freditorWriter.beforeFirstWrite = () -> { 40 | int position = output.length(); 41 | EventQueue.invokeLater(() -> { 42 | output.scrollBottom(-2); 43 | output.setCursorTo(position); 44 | }); 45 | tabs.setSelectedComponent(output); 46 | }; 47 | Var.pushThreadBindings(RT.map(RT.CURRENT_NS, RT.CURRENT_NS.deref())); 48 | try { 49 | input.get().saveWithBackup(); 50 | body.run(); 51 | } catch (Compiler.CompilerException ex) { 52 | Throwable cause = ex.getCause(); 53 | StackTraceElement[] fullStackTrace = cause.getStackTrace(); 54 | StackTraceElement[] userStackTrace = Arrays.stream(fullStackTrace) 55 | .filter(element -> input.get().getFile().getFileName().toString().equals(element.getFileName())) 56 | .toArray(StackTraceElement[]::new); 57 | if (userStackTrace.length > 0) { 58 | printStackTrace(cause, userStackTrace); 59 | if (setCursor) { 60 | int line = userStackTrace[0].getLineNumber(); 61 | input.get().setCursorTo(line - 1, 0); 62 | } 63 | printStackTrace(cause, fullStackTrace); 64 | } else { 65 | printStackTrace(cause, fullStackTrace); 66 | if (setCursor && ex.line > 0) { 67 | IPersistentMap data = ex.getData(); 68 | Integer column = (Integer) data.valAt(Compiler.CompilerException.ERR_COLUMN); 69 | input.get().setCursorTo(ex.line - 1, column - 1); 70 | } 71 | } 72 | } finally { 73 | Var.popThreadBindings(); 74 | } 75 | } 76 | 77 | private void printStackTrace(Throwable cause, StackTraceElement[] stackTrace) { 78 | printWriter.println(cause.getClass().getName()); 79 | printWriter.println(cause.getLocalizedMessage()); 80 | for (StackTraceElement element : stackTrace) { 81 | // trim stack trace at first Clopad type (default package; unqualified class name) 82 | if (Character.isUpperCase(element.getClassName().charAt(0))) break; 83 | printWriter.println("\tat " + element); 84 | } 85 | printWriter.println(); 86 | } 87 | 88 | public void print(PrintFormToWriter printFormToWriter, String prefix, Object form, String suffix) { 89 | StringWriter stringWriter = new StringWriter(); 90 | try { 91 | stringWriter.append(prefix); 92 | printFormToWriter.print(form, stringWriter); 93 | ensureEmptyLine(stringWriter); 94 | stringWriter.append(suffix); 95 | } catch (IOException ex) { 96 | throw Util.sneakyThrow(ex); 97 | } finally { 98 | printWriter.append(stringWriter.toString()); 99 | } 100 | } 101 | 102 | private void ensureEmptyLine(StringWriter stringWriter) { 103 | StringBuffer buffer = stringWriter.getBuffer(); 104 | int length = buffer.length(); 105 | if (length > 0 && buffer.charAt(length - 1) != '\n') { 106 | stringWriter.append('\n'); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/DocumentAdapter.java: -------------------------------------------------------------------------------- 1 | import javax.swing.event.DocumentEvent; 2 | import javax.swing.event.DocumentListener; 3 | import java.util.function.Consumer; 4 | 5 | public class DocumentAdapter implements DocumentListener { 6 | private final Consumer consumer; 7 | 8 | public DocumentAdapter(Consumer consumer) { 9 | this.consumer = consumer; 10 | } 11 | 12 | @Override 13 | public void insertUpdate(DocumentEvent e) { 14 | consumer.accept(e); 15 | } 16 | 17 | @Override 18 | public void removeUpdate(DocumentEvent e) { 19 | consumer.accept(e); 20 | } 21 | 22 | @Override 23 | public void changedUpdate(DocumentEvent e) { 24 | consumer.accept(e); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/Flexer.java: -------------------------------------------------------------------------------- 1 | import freditor.FlexerState; 2 | import freditor.FlexerStateBuilder; 3 | import freditor.persistent.ChampMap; 4 | 5 | import static freditor.FlexerState.EMPTY; 6 | import static freditor.FlexerState.THIS; 7 | 8 | public class Flexer extends freditor.Flexer { 9 | public static final Flexer instance = new Flexer(); 10 | 11 | private static final FlexerState COMMA = EMPTY.head(); 12 | 13 | private static final FlexerState COMMENT_TAIL = new FlexerState('\n', null).setDefault(THIS); 14 | private static final FlexerState COMMENT_HEAD = COMMENT_TAIL.head(); 15 | 16 | private static final FlexerState CHAR_CONSTANT_TAIL = new FlexerState("09AFaz", THIS); 17 | private static final FlexerState CHAR_CONSTANT_HEAD = new FlexerState("!?@~", CHAR_CONSTANT_TAIL).head(); 18 | 19 | private static final FlexerState STRING_LITERAL_END = EMPTY.tail(); 20 | private static final FlexerState STRING_LITERAL_ESCAPE = new FlexerState('\n', null); 21 | private static final FlexerState STRING_LITERAL_TAIL = new FlexerState('\"', STRING_LITERAL_END, '\\', STRING_LITERAL_ESCAPE).setDefault(THIS); 22 | private static final FlexerState STRING_LITERAL_HEAD = STRING_LITERAL_TAIL.head(); 23 | 24 | static { 25 | STRING_LITERAL_ESCAPE.setDefault(STRING_LITERAL_TAIL); 26 | } 27 | 28 | private static final FlexerState NUMBER_TAIL = new FlexerState(".9AFMMNNRRXXafrrxx", THIS); 29 | private static final FlexerState NUMBER_HEAD = NUMBER_TAIL.head(); 30 | 31 | static final FlexerState SYMBOL_TAIL = new FlexerState("!!$$''*+-: lexemeColors = ChampMap.of(ERROR, 0xff0000) 77 | .put(COMMENT_HEAD, COMMENT_TAIL, 0x999988) 78 | .put(CHAR_CONSTANT_HEAD, CHAR_CONSTANT_TAIL, 0x00a67a) 79 | .put(STRING_LITERAL_HEAD, STRING_LITERAL_TAIL, STRING_LITERAL_ESCAPE, STRING_LITERAL_END, 0x00a67a) 80 | .put(NUMBER_HEAD, NUMBER_TAIL, 0x143dfb) 81 | .put(START.read("false", "nil", "true"), 0x143dfb) 82 | .put(KEYWORD_HEAD, KEYWORD_TAIL, 0x990073); 83 | 84 | private static final ChampMap afterOpeningParen = lexemeColors 85 | .put(START.read("a", "aa", "-", "f", "fa", "fal", "fals", "n", "ni", "t", "tr", "tru"), 0xcc55ca); 86 | 87 | @Override 88 | public boolean preventInsertion(FlexerState nextState) { 89 | return nextState == CLOSING_PAREN 90 | || nextState == CLOSING_BRACKET 91 | || nextState == CLOSING_BRACE 92 | || nextState == STRING_LITERAL_END; 93 | } 94 | 95 | @Override 96 | public String synthesizeOnInsert(FlexerState state, FlexerState nextState) { 97 | if (state == OPENING_PAREN) return ")"; 98 | if (state == OPENING_BRACKET) return "]"; 99 | if (state == OPENING_BRACE) return "}"; 100 | if (state == STRING_LITERAL_HEAD) return "\""; 101 | return ""; 102 | } 103 | 104 | @Override 105 | public boolean arePartners(FlexerState opening, FlexerState closing) { 106 | if (opening == OPENING_PAREN) return (closing == CLOSING_PAREN); 107 | if (opening == OPENING_BRACKET) return (closing == CLOSING_BRACKET); 108 | if (opening == OPENING_BRACE) return (closing == CLOSING_BRACE); 109 | if (opening == STRING_LITERAL_HEAD) return (closing == STRING_LITERAL_END); 110 | return false; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/FreditorUI_symbol.java: -------------------------------------------------------------------------------- 1 | import clojure.lang.Symbol; 2 | import freditor.Flexer; 3 | import freditor.FreditorUI; 4 | import freditor.Indenter; 5 | 6 | public class FreditorUI_symbol extends FreditorUI { 7 | public final Symbol symbol; 8 | 9 | public FreditorUI_symbol(Flexer flexer, Indenter indenter, int columns, int rows, Symbol symbol) { 10 | super(flexer, indenter, columns, rows); 11 | this.symbol = symbol; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/FreditorWriter.java: -------------------------------------------------------------------------------- 1 | import freditor.FreditorUI; 2 | 3 | import java.io.Writer; 4 | 5 | public class FreditorWriter extends Writer { 6 | private final FreditorUI output; 7 | public Runnable beforeFirstWrite = null; 8 | 9 | public FreditorWriter(FreditorUI output) { 10 | super(output); // synchronize on output 11 | this.output = output; 12 | } 13 | 14 | @Override 15 | public void write(char[] cbuf, int off, int len) { 16 | if (beforeFirstWrite != null) { 17 | beforeFirstWrite.run(); 18 | beforeFirstWrite = null; 19 | } 20 | if (len == 2 && cbuf[off] == '\r') { 21 | // PrintWriter.newLine() writes System.lineSeparator() 22 | output.append("\n"); 23 | } else { 24 | output.append(new ArrayCharSequence(cbuf, off, len)); 25 | } 26 | } 27 | 28 | @Override 29 | public void flush() { 30 | } 31 | 32 | @Override 33 | public void close() { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/ISeqSpliterator.java: -------------------------------------------------------------------------------- 1 | import clojure.lang.ISeq; 2 | 3 | import java.util.Spliterator; 4 | import java.util.function.Consumer; 5 | import java.util.stream.Stream; 6 | import java.util.stream.StreamSupport; 7 | 8 | public class ISeqSpliterator implements Spliterator { 9 | private ISeq seq; 10 | 11 | public ISeqSpliterator(ISeq seq) { 12 | this.seq = seq; 13 | } 14 | 15 | public static Stream stream(ISeq seq) { 16 | return StreamSupport.stream(new ISeqSpliterator<>(seq), false); 17 | } 18 | 19 | @SuppressWarnings("unchecked") 20 | @Override 21 | public boolean tryAdvance(Consumer action) { 22 | if (seq == null) return false; 23 | 24 | action.accept((T) seq.first()); 25 | seq = seq.next(); 26 | return true; 27 | } 28 | 29 | @Override 30 | public Spliterator trySplit() { 31 | return null; 32 | } 33 | 34 | @Override 35 | public long estimateSize() { 36 | return Long.MAX_VALUE; 37 | } 38 | 39 | @Override 40 | public int characteristics() { 41 | return IMMUTABLE; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/Java.java: -------------------------------------------------------------------------------- 1 | import java.lang.reflect.*; 2 | import java.util.ArrayList; 3 | import java.util.Arrays; 4 | import java.util.Comparator; 5 | import java.util.HashSet; 6 | import java.util.function.IntPredicate; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | public class Java { 13 | public static String classChain(Class clazz) { 14 | ArrayList> result = classChainList(clazz); 15 | if (result.size() < 2) return ""; 16 | 17 | return result.stream() 18 | .map(Java::shrinkLangPackages) 19 | .collect(Collectors.joining(" -> ", "", "\n")); 20 | } 21 | 22 | private static ArrayList> classChainList(Class clazz) { 23 | ArrayList> result = new ArrayList<>(); 24 | while (clazz != null) { 25 | result.add(clazz); 26 | clazz = clazz.getSuperclass(); 27 | } 28 | return result; 29 | } 30 | 31 | public static String allInterfaces(Class clazz, int columns) { 32 | HashSet> result = new HashSet<>(); 33 | for (Class ancestor : classChainList(clazz)) { 34 | insertAllInterfaces(ancestor, result); 35 | } 36 | if (result.isEmpty()) return ""; 37 | 38 | String prefix; 39 | String indent; 40 | if (clazz.isInterface()) { 41 | prefix = "extends* "; 42 | indent = " "; 43 | } else { 44 | prefix = "implements* "; 45 | indent = " "; 46 | } 47 | Stream stream = result.stream() 48 | .sorted(Comparator.comparing(Class::getSimpleName)) 49 | .map(Java::shrinkLangPackages); 50 | return join2D(columns, stream::iterator, prefix, ", ", indent, "\n"); 51 | } 52 | 53 | private static void insertAllInterfaces(Class clazz, HashSet> result) { 54 | for (Class directInterface : clazz.getInterfaces()) { 55 | if (result.add(directInterface)) { 56 | insertAllInterfaces(directInterface, result); 57 | } 58 | } 59 | } 60 | 61 | private static String join2D(int columns, Iterable strings, String prefix, String delimiter, String indent, String suffix) { 62 | StringBuilder builder = new StringBuilder(prefix); 63 | int width = prefix.length(); 64 | String delim = ""; 65 | for (String s : strings) { 66 | builder.append(delim); 67 | width += delim.length(); 68 | if (width + s.length() > columns) { 69 | builder.append("\n").append(indent); 70 | width = indent.length(); 71 | } 72 | builder.append(s); 73 | width += s.length(); 74 | delim = delimiter; 75 | } 76 | return builder.append(suffix).toString(); 77 | } 78 | 79 | public static String sortedConstructors(Class clazz, IntPredicate modifiersFilter, String suffix) { 80 | return textBlock(suffix, Arrays.stream(clazz.getConstructors()) 81 | .filter(constructor -> modifiersFilter.test(constructor.getModifiers())) 82 | .sorted(Comparator.comparing(Constructor::getParameterCount)) 83 | .map(constructor -> clazz.getSimpleName() + parameters(constructor) + exceptionTypes(constructor))); 84 | } 85 | 86 | public static String sortedFields(Class clazz, IntPredicate modifiersFilter, String suffix) { 87 | return textBlock(suffix, Arrays.stream(clazz.getFields()) 88 | .filter(method -> modifiersFilter.test(method.getModifiers())) 89 | .sorted(Comparator.comparing(Field::getName)) 90 | .map(field -> field.getName() + ": " + shrinkLangPackages(field.getType()))); 91 | } 92 | 93 | public static String sortedMethods(Class clazz, IntPredicate modifiersFilter, String suffix) { 94 | Stream stream = Arrays.stream(clazz.getMethods()); 95 | if (clazz != Object.class) { 96 | stream = stream.filter(method -> !rootMethods.contains(method)); 97 | } 98 | return textBlock(suffix, stream.filter(method -> modifiersFilter.test(method.getModifiers())) 99 | .sorted(Comparator.comparing(Method::getName).thenComparing(Method::getParameterCount)) 100 | .map(method -> method.getName() + parameters(method) + ": " + shrinkLangPackages(method.getReturnType()) + exceptionTypes(method))); 101 | } 102 | 103 | private static final HashSet rootMethods = new HashSet<>(Arrays.asList(Object.class.getDeclaredMethods())); 104 | 105 | private static String parameters(Executable executable) { 106 | return Arrays.stream(executable.getParameters()) 107 | .map(Java::parameter) 108 | .collect(Collectors.joining(", ", "(", ")")); 109 | } 110 | 111 | private static String parameter(Parameter parameter) { 112 | String name = parameter.getName(); 113 | String type = shrinkLangPackages(parameter.getType()); 114 | if (SYNTHESIZED_PARAMETER.matcher(name).matches()) { 115 | return type; 116 | } else { 117 | return name + ": " + type; 118 | } 119 | } 120 | 121 | private static String exceptionTypes(Executable executable) { 122 | Class[] exceptionTypes = executable.getExceptionTypes(); 123 | if (exceptionTypes.length == 0) return ""; 124 | 125 | return Arrays.stream(exceptionTypes) 126 | .map(Java::shrinkLangPackages) 127 | .collect(Collectors.joining(", ", " (", ")")); 128 | } 129 | 130 | private static final Pattern SYNTHESIZED_PARAMETER = Pattern.compile("arg\\d+"); 131 | 132 | private static String shrinkLangPackages(Class type) { 133 | String name = type.getTypeName(); 134 | Matcher matcher = JAVA_LANG.matcher(name); 135 | if (matcher.matches()) { 136 | return matcher.group(1); 137 | } 138 | matcher = CLOJURE_LANG.matcher(name); 139 | if (matcher.matches()) { 140 | return '©' + matcher.group(1); 141 | } 142 | return name; 143 | } 144 | 145 | private static final Pattern JAVA_LANG = Pattern.compile("java[.]lang[.]([^.]+)"); 146 | private static final Pattern CLOJURE_LANG = Pattern.compile("clojure[.]lang[.](.+)"); 147 | 148 | public static String expandClojureLangPackage(String lexeme) { 149 | return (lexeme.charAt(0) == '©') ? "clojure.lang." + lexeme.substring(1) : lexeme; 150 | } 151 | 152 | private static String textBlock(String suffix, Stream stream) { 153 | StringBuilder builder = new StringBuilder(); 154 | stream.forEach(string -> builder.append(string).append("\n")); 155 | return (builder.length() == 0) ? "" : builder.append(suffix).toString(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import clojure.lang.RT; 2 | import freditor.SwingConfig; 3 | 4 | import java.awt.*; 5 | 6 | public class Main { 7 | public static void main(String[] args) { 8 | SwingConfig.metalWithDefaultFont(SwingConfig.SANS_SERIF_PLAIN_16); 9 | EventQueue.invokeLater(MainFrame::new); 10 | RT.init(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/MainFrame.java: -------------------------------------------------------------------------------- 1 | import clojure.lang.Compiler; 2 | import clojure.lang.*; 3 | import freditor.FreditorUI; 4 | import freditor.Fronts; 5 | import freditor.Release; 6 | import freditor.TabbedEditors; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.event.KeyAdapter; 11 | import java.awt.event.KeyEvent; 12 | import java.awt.event.MouseAdapter; 13 | import java.awt.event.MouseEvent; 14 | import java.lang.reflect.Modifier; 15 | import java.time.LocalTime; 16 | import java.time.format.DateTimeFormatter; 17 | import java.util.HashMap; 18 | import java.util.Optional; 19 | import java.util.function.Consumer; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | 23 | import static java.lang.reflect.Modifier.isPublic; 24 | import static java.lang.reflect.Modifier.isStatic; 25 | 26 | public class MainFrame extends JFrame { 27 | private TabbedEditors tabbedInputs; 28 | 29 | private FreditorUI input() { 30 | return tabbedInputs.getSelectedEditor(); 31 | } 32 | 33 | private NamespaceExplorer namespaceExplorer; 34 | 35 | private FreditorUI output; 36 | private HashMap infos; 37 | private JTabbedPane tabs; 38 | private final int emptyTabsWidth; 39 | private JSplitPane split; 40 | 41 | private Console console; 42 | 43 | public MainFrame() { 44 | tabbedInputs = new TabbedEditors("clopad", Flexer.instance, ClojureIndenter.instance, freditor -> { 45 | FreditorUI input = new FreditorUI(freditor, 72, 25); 46 | 47 | input.addKeyListener(new KeyAdapter() { 48 | @Override 49 | public void keyPressed(KeyEvent event) { 50 | switch (event.getKeyCode()) { 51 | case KeyEvent.VK_F1: 52 | printHelpInCurrentNamespace(input().symbolNearCursor(Flexer.SYMBOL_TAIL)); 53 | break; 54 | 55 | case KeyEvent.VK_F5: 56 | evaluateWholeProgram(selectPrintFormToWriter(event)); 57 | break; 58 | 59 | case KeyEvent.VK_F11: 60 | macroexpandFormAtCursor(isolateSelectedForm(), selectMacroexpand(event), selectPrintFormToWriter(event)); 61 | break; 62 | 63 | case KeyEvent.VK_F12: 64 | evaluateFormAtCursor(isolateSelectedForm(), selectPrintFormToWriter(event)); 65 | break; 66 | } 67 | } 68 | 69 | private IFn selectMacroexpand(KeyEvent event) { 70 | return FreditorUI.isControlRespectivelyCommandDown(event) ? Clojure.macroexpandAll : Clojure.macroexpand; 71 | } 72 | 73 | private PrintFormToWriter selectPrintFormToWriter(KeyEvent event) { 74 | return event.isShiftDown() ? RT::print : Clojure.pprint::invoke; 75 | } 76 | }); 77 | 78 | registerRightClickIn(input, event -> { 79 | if (event.getClickCount() == 2) { 80 | evaluateFormAtCursor(isolateSelectedForm(), event.isShiftDown() ? RT::print : Clojure.pprint::invoke); 81 | } 82 | }, this::printHelpInCurrentNamespace); 83 | 84 | return input; 85 | }); 86 | 87 | if (input().length() == 0) { 88 | input().load(helloWorld); 89 | } 90 | 91 | namespaceExplorer = new NamespaceExplorer(this::printHelpFromExplorer); 92 | 93 | output = new FreditorUI(Flexer.instance, ClojureIndenter.instance, 72, 8); 94 | 95 | infos = new HashMap<>(); 96 | 97 | tabs = new JTabbedPane(); 98 | tabs.setFont(Fronts.sansSerif); 99 | emptyTabsWidth = tabs.getPreferredSize().width; 100 | tabs.addTab("output", output); 101 | tabs.addTab("ns explorer", namespaceExplorer); 102 | printHelp(Clojure.clojure_core_ns, Clojure.sourceFn.invoke(Clojure.clojure_core_ns)); 103 | tabs.setSelectedIndex(2); 104 | 105 | split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, tabs, tabbedInputs.tabs); 106 | split.setResizeWeight(0.0); 107 | add(split); 108 | 109 | console = new Console(tabs, output, this::input); 110 | 111 | addListeners(); 112 | boringStuff(); 113 | } 114 | 115 | private static final Pattern NOT_NEWLINE = Pattern.compile("[^\n]"); 116 | 117 | private String isolateSelectedForm() { 118 | String text = input().getText(); 119 | if (input().selectionIsEmpty()) return text; 120 | 121 | // For simplicity, assume first form is the namespace form 122 | String namespaceForm = Clojure.firstForm(text); 123 | int namespaceEnd = namespaceForm.length(); 124 | int selectionStart = input().selectionStart(); 125 | if (namespaceEnd <= selectionStart) { 126 | String betweenNamespaceAndSelection = text.substring(namespaceEnd, selectionStart); 127 | String selection = text.substring(selectionStart, input().selectionEnd()); 128 | // The whitespace preserves absolute positions for better error messages 129 | String whitespace = NOT_NEWLINE.matcher(betweenNamespaceAndSelection).replaceAll(" "); 130 | return namespaceForm + whitespace + selection; 131 | } else { 132 | return namespaceForm; 133 | } 134 | } 135 | 136 | private void addListeners() { 137 | registerRightClickIn(output, this::printHelpInCurrentNamespace); 138 | 139 | tabs.addMouseListener(new MouseAdapter() { 140 | @Override 141 | public void mouseClicked(MouseEvent event) { 142 | if (event.getButton() == MouseEvent.BUTTON3) { 143 | Component selectedComponent = tabs.getSelectedComponent(); 144 | if (!(selectedComponent instanceof FreditorUI_symbol)) { 145 | tabs.removeAll(); 146 | tabs.addTab("output", output); 147 | tabs.addTab("ns explorer", namespaceExplorer); 148 | tabs.setSelectedComponent(selectedComponent); 149 | infos.clear(); 150 | } else { 151 | FreditorUI_symbol selectedSource = (FreditorUI_symbol) selectedComponent; 152 | tabs.remove(selectedSource); 153 | infos.remove(selectedSource.symbol); 154 | } 155 | input().requestFocusInWindow(); 156 | } 157 | } 158 | }); 159 | 160 | split.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, event -> { 161 | EventQueue.invokeLater(() -> { 162 | final int frontWidth = Fronts.front.width; 163 | int rest = (tabs.getWidth() - emptyTabsWidth) % frontWidth; 164 | if (rest > 0) { 165 | if ((int) event.getNewValue() > (int) event.getOldValue()) { 166 | rest -= frontWidth; 167 | } 168 | split.setDividerLocation(split.getDividerLocation() - rest); 169 | } 170 | }); 171 | }); 172 | } 173 | 174 | private void registerRightClickIn(FreditorUI editor, Consumer afterSelect, Consumer defaultConsumer) { 175 | editor.onRightClick2 = (lexeme, event) -> { 176 | switch (lexeme.charAt(0)) { 177 | case '(': 178 | case '[': 179 | case '{': 180 | editor.setCursorTo(editor.cursor() + 1); 181 | // intentional fallthrough 182 | case ')': 183 | case ']': 184 | case '}': 185 | case ' ': 186 | case '\n': 187 | case ',': 188 | editor.selectContainingForm(); 189 | afterSelect.accept(event); 190 | break; 191 | 192 | default: 193 | defaultConsumer.accept(lexeme); 194 | } 195 | }; 196 | } 197 | 198 | private void registerRightClickIn(FreditorUI editor, Consumer defaultConsumer) { 199 | registerRightClickIn(editor, event -> { 200 | }, defaultConsumer); 201 | } 202 | 203 | private void printHelpInCurrentNamespace(String lexeme) { 204 | console.run(false, () -> { 205 | Pattern userLocation = Pattern.compile(".*(?:\\Q" + input().getFile().getFileName() + "\\E)?:(\\d+)(?::(\\d+))?"); 206 | Matcher matcher = userLocation.matcher(lexeme); 207 | if (matcher.matches()) { 208 | int line = Integer.parseInt(matcher.group(1)); 209 | int column = Optional.ofNullable(matcher.group(2)).map(Integer::parseInt).orElse(1); 210 | input().setCursorTo(line - 1, column - 1); 211 | EventQueue.invokeLater(input()::requestFocusInWindow); 212 | } else { 213 | evaluateNamespaceFormsBeforeCursor(input().getText(), formAtCursor -> { 214 | Namespace namespace = (Namespace) RT.CURRENT_NS.deref(); 215 | printPotentiallySpecialHelp(namespace, Symbol.create(lexeme)); 216 | }); 217 | } 218 | }); 219 | } 220 | 221 | private void printHelpFromHelp(String shrunkLexeme) { 222 | String lexeme = Java.expandClojureLangPackage(shrunkLexeme); 223 | console.run(false, () -> { 224 | FreditorUI_symbol selected = (FreditorUI_symbol) tabs.getSelectedComponent(); 225 | String selectedSymbolNamespace = selected.symbol.getNamespace(); 226 | if (selectedSymbolNamespace != null) { 227 | Namespace namespace = Namespace.find(Symbol.create(selectedSymbolNamespace)); 228 | printPotentiallySpecialHelp(namespace, Symbol.create(lexeme)); 229 | } else { 230 | printHelpInCurrentNamespace(lexeme); 231 | } 232 | }); 233 | } 234 | 235 | private void printPotentiallySpecialHelp(Namespace namespace, Symbol symbol) { 236 | String specialHelp = SpecialForm.help(symbol.toString()); 237 | if (specialHelp != null) { 238 | printHelp(symbol, specialHelp); 239 | } else { 240 | printHelp(namespace, symbol); 241 | } 242 | } 243 | 244 | public void printHelpFromExplorer(Namespace namespace, Symbol symbol) { 245 | console.run(false, () -> printHelp(namespace, symbol)); 246 | input().requestFocusInWindow(); 247 | } 248 | 249 | private void printHelp(Namespace namespace, Symbol symbol) { 250 | String name = symbol.toString(); 251 | if (name.endsWith(".") && !name.startsWith(".")) { 252 | printHelpConstructor(namespace, Symbol.create(name.substring(0, name.length() - 1))); 253 | } else { 254 | printHelpNonConstructor(namespace, symbol); 255 | } 256 | } 257 | 258 | private void printHelpConstructor(Namespace namespace, Symbol symbol) { 259 | Object obj = Compiler.maybeResolveIn(namespace, symbol); 260 | if (obj == null) throw new RuntimeException("Unable to resolve symbol: " + symbol + " in this context"); 261 | 262 | if (obj instanceof Class) { 263 | Class clazz = (Class) obj; 264 | String constructors = Java.sortedConstructors(clazz, Modifier::isPublic, ""); 265 | printHelp(symbol, constructors); 266 | } else { 267 | console.printWriter.println(obj); 268 | } 269 | } 270 | 271 | private void printHelpNonConstructor(Namespace namespace, Symbol symbol) { 272 | Object obj = Compiler.maybeResolveIn(namespace, symbol); 273 | if (obj == null) throw new RuntimeException("Unable to resolve symbol: " + symbol + " in this context"); 274 | 275 | if (obj instanceof Var) { 276 | Var var = (Var) obj; 277 | Symbol resolved = Symbol.create(var.ns.toString(), var.sym.getName()); 278 | printHelp(var, resolved); 279 | } else if (obj instanceof Class) { 280 | Class clazz = (Class) obj; 281 | printHelpMembers(symbol, clazz); 282 | } else { 283 | console.printWriter.println(obj); 284 | } 285 | } 286 | 287 | private void printHelpMembers(Symbol symbol, Class clazz) { 288 | String header = Java.classChain(clazz) + Java.allInterfaces(clazz, output.visibleColumns()); 289 | if (!header.isEmpty()) { 290 | header += "\n"; 291 | } 292 | 293 | String methods = Java.sortedMethods(clazz, mod -> isPublic(mod) && !isStatic(mod), "\n"); 294 | String staticFields = Java.sortedFields(clazz, mod -> isPublic(mod) && isStatic(mod), "\n"); 295 | String staticMethods = Java.sortedMethods(clazz, mod -> isPublic(mod) && isStatic(mod), ""); 296 | 297 | if (staticFields.isEmpty() && staticMethods.isEmpty()) { 298 | printHelp(symbol, header + methods); 299 | } else { 300 | printHelp(symbol, header + methods + "======== static members ========\n\n" + staticFields + staticMethods); 301 | } 302 | } 303 | 304 | private void printHelp(Var var, Symbol resolved) { 305 | Object help = Clojure.sourceFn.invoke(resolved); 306 | if (help == null) { 307 | IPersistentMap meta = var.meta(); 308 | if (meta != null) { 309 | help = meta.valAt(Clojure.doc); 310 | } 311 | if (help == null) throw new RuntimeException("No source or doc found for symbol: " + resolved); 312 | } 313 | printHelp(resolved, help); 314 | } 315 | 316 | private void printHelp(Symbol resolved, Object help) { 317 | FreditorUI_symbol info = infos.computeIfAbsent(resolved, this::newInfo); 318 | info.load(help.toString()); 319 | tabs.setSelectedComponent(info); 320 | } 321 | 322 | private FreditorUI_symbol newInfo(Symbol symbol) { 323 | FreditorUI_symbol info = new FreditorUI_symbol(Flexer.instance, ClojureIndenter.instance, 72, 8, symbol); 324 | registerRightClickIn(info, this::printHelpFromHelp); 325 | tabs.addTab(symbol.getName(), info); 326 | return info; 327 | } 328 | 329 | private void macroexpandFormAtCursor(String text, IFn macroexpand, PrintFormToWriter printFormToWriter) { 330 | console.run(true, () -> { 331 | evaluateNamespaceFormsBeforeCursor(text, formAtCursor -> { 332 | console.print(printFormToWriter, "", formAtCursor, "¤\n"); 333 | Object expansion = macroexpand.invoke(formAtCursor); 334 | printResultValueAndType(printFormToWriter, expansion); 335 | }); 336 | }); 337 | } 338 | 339 | private void evaluateWholeProgram(PrintFormToWriter printFormToWriter) { 340 | console.run(true, () -> { 341 | output.load(""); 342 | Clojure.loadFromScratch(input().getText(), input().getFile(), result -> { 343 | namespaceExplorer.updateNamespaces(); 344 | printResultValueAndType(printFormToWriter, result); 345 | }); 346 | }); 347 | } 348 | 349 | private void printResultValueAndType(PrintFormToWriter printFormToWriter, Object result) { 350 | if (result == null) { 351 | console.print(printFormToWriter, "", null, "\n" + timestamp() + "\n\n"); 352 | } else { 353 | console.print(printFormToWriter, "", result, "\n" + result.getClass().getTypeName() + "\n" + timestamp() + "\n\n"); 354 | } 355 | } 356 | 357 | private static final DateTimeFormatter _HH_mm_ss_SSS = DateTimeFormatter.ofPattern(";HH:mm:ss.SSS"); 358 | 359 | private static String timestamp() { 360 | return LocalTime.now().format(_HH_mm_ss_SSS); 361 | } 362 | 363 | private void evaluateFormAtCursor(String text, PrintFormToWriter printFormToWriter) { 364 | console.run(true, () -> { 365 | evaluateNamespaceFormsBeforeCursor(text, formAtCursor -> { 366 | console.print(printFormToWriter, "", formAtCursor, "\n"); 367 | Object result = Clojure.isNamespaceForm(formAtCursor) ? null : Compiler.eval(formAtCursor, false); 368 | printResultValueAndType(printFormToWriter, result); 369 | }); 370 | }); 371 | } 372 | 373 | private void evaluateNamespaceFormsBeforeCursor(String text, Consumer formContinuation) { 374 | Clojure.evaluateNamespaceFormsBefore(text, input().getFile(), 375 | input().row() + 1, input().column() + 1, namespaceExplorer::updateNamespaces, formContinuation); 376 | } 377 | 378 | private void boringStuff() { 379 | setTitle("clopad version " + Release.compilationDate(MainFrame.class) + " @ " + input().getFile().getParent()); 380 | setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 381 | tabbedInputs.saveOnExit(this); 382 | pack(); 383 | setVisible(true); 384 | input().requestFocusInWindow(); 385 | } 386 | 387 | private static final String helloWorld = "" 388 | + ";; F1 show source or doc-string (same as right-click)\n" 389 | + ";; F5 evaluate whole program\n" 390 | + ";; F11 macroexpand top-level or selected form (CTRL also expands nested macros)\n" 391 | + ";; F12 evaluate top-level or selected form\n" 392 | + ";; SHIFT with F5/F11/F12 disables pretty-printing\n\n" 393 | + "(ns user\n (:require [clojure.string :as string]))\n\n" 394 | + "(defn square [x]\n (* x x))\n\n" 395 | + "(->> (range 1 11)\n (map square )\n (string/join \", \" ))\n"; 396 | } 397 | -------------------------------------------------------------------------------- /src/main/java/NamespaceExplorer.java: -------------------------------------------------------------------------------- 1 | import clojure.lang.ISeq; 2 | import clojure.lang.Namespace; 3 | import clojure.lang.RT; 4 | import clojure.lang.Symbol; 5 | import freditor.Fronts; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.util.Comparator; 10 | import java.util.function.BiConsumer; 11 | 12 | public class NamespaceExplorer extends JPanel { 13 | private final JComboBox namespaces; 14 | private final JList names; 15 | private final JTextField filter; 16 | 17 | public NamespaceExplorer(BiConsumer onSymbolSelected) { 18 | super(new BorderLayout()); 19 | 20 | namespaces = new JComboBox<>(); 21 | namespaces.setFont(Fronts.sansSerif); 22 | namespaces.addItemListener(event -> filterSymbols()); 23 | 24 | names = new JList<>(); 25 | names.setFont(Fronts.sansSerif); 26 | names.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 27 | names.addListSelectionListener(event -> { 28 | if (event.getValueIsAdjusting()) return; 29 | 30 | Object namespace = namespaces.getSelectedItem(); 31 | if (namespace == null) return; 32 | 33 | Symbol symbol = names.getSelectedValue(); 34 | if (symbol == null) return; 35 | 36 | onSymbolSelected.accept((Namespace) namespace, symbol); 37 | }); 38 | 39 | filter = new JTextField(); 40 | filter.setFont(Fronts.sansSerif); 41 | filter.getDocument().addDocumentListener(new DocumentAdapter(event -> filterSymbols())); 42 | 43 | add(namespaces, BorderLayout.NORTH); 44 | add(new JScrollPane(names), BorderLayout.CENTER); 45 | add(filter, BorderLayout.SOUTH); 46 | 47 | updateNamespaces(); 48 | } 49 | 50 | public void updateNamespaces() { 51 | ISeq allNamespaces = Namespace.all(); 52 | if (RT.count(allNamespaces) != namespaces.getItemCount()) { 53 | namespaces.removeAllItems(); 54 | ISeqSpliterator.stream(allNamespaces) 55 | .sorted(Comparator.comparing(Namespace::toString)) 56 | .forEach(namespaces::addItem); 57 | } 58 | } 59 | 60 | private void filterSymbols() { 61 | Namespace namespace = (Namespace) namespaces.getSelectedItem(); 62 | if (namespace == null) return; 63 | 64 | String filterText = filter.getText(); 65 | 66 | ISeq interns = RT.keys(Clojure.nsInterns.invoke(namespace.name)); 67 | Symbol[] symbols = ISeqSpliterator.stream(interns) 68 | .filter(symbol -> symbol.getName().contains(filterText)) 69 | .sorted(Comparator.comparing(Symbol::getName)) 70 | .toArray(Symbol[]::new); 71 | names.setListData(symbols); 72 | 73 | filter.setBackground(filterText.isEmpty() || symbols.length > 0 ? Color.WHITE : Color.PINK); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/PrintFormToWriter.java: -------------------------------------------------------------------------------- 1 | import java.io.IOException; 2 | import java.io.Writer; 3 | 4 | @FunctionalInterface 5 | public interface PrintFormToWriter { 6 | void print(Object x, Writer w) throws IOException; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/SpecialForm.java: -------------------------------------------------------------------------------- 1 | public class SpecialForm { 2 | public static String help(String lexeme) { 3 | switch (lexeme) { 4 | case ".": 5 | return " (.instanceMember instance args*)\n" + 6 | " (.instanceMember Classname args*)\n" + 7 | " (Classname/staticMethod args*)\n" + 8 | " Classname/staticField\n" + 9 | "Special Form\n" + 10 | " The instance member form works for both fields and methods.\n" + 11 | " They all expand into calls to the dot operator at macroexpansion time."; 12 | case "def": 13 | return " (def symbol doc-string? init?)\n" + 14 | "Special Form\n" + 15 | " Creates and interns a global var with the name\n" + 16 | " of symbol in the current namespace (*ns*) or locates such a var if\n" + 17 | " it already exists. If init is supplied, it is evaluated, and the\n" + 18 | " root binding of the var is set to the resulting value. If init is\n" + 19 | " not supplied, the root binding of the var is unaffected."; 20 | case "do": 21 | return " (do exprs*)\n" + 22 | "Special Form\n" + 23 | " Evaluates the expressions in order and returns the value of\n" + 24 | " the last. If no expressions are supplied, returns nil."; 25 | case "if": 26 | return " (if test then else?)\n" + 27 | "Special Form\n" + 28 | " Evaluates test. If not the singular values nil or false,\n" + 29 | " evaluates and yields then, otherwise, evaluates and yields else. If\n" + 30 | " else is not supplied it defaults to nil."; 31 | case "monitor-enter": 32 | return " (monitor-enter x)\n" + 33 | "Special Form\n" + 34 | " Synchronization primitive that should be avoided\n" + 35 | " in user code. Use the 'locking' macro."; 36 | case "monitor-exit": 37 | return " (monitor-exit x)\n" + 38 | "Special Form\n" + 39 | " Synchronization primitive that should be avoided\n" + 40 | " in user code. Use the 'locking' macro."; 41 | case "new": 42 | return " (Classname. args*)\n" + 43 | " (new Classname args*)\n" + 44 | "Special Form\n" + 45 | " The args, if any, are evaluated from left to right, and\n" + 46 | " passed to the constructor of the class named by Classname. The\n" + 47 | " constructed object is returned."; 48 | case "quote": 49 | return " (quote form)\n" + 50 | "Special Form\n" + 51 | " Yields the unevaluated form."; 52 | case "recur": 53 | return " (recur exprs*)\n" + 54 | "Special Form\n" + 55 | " Evaluates the exprs in order, then, in parallel, rebinds\n" + 56 | " the bindings of the recursion point to the values of the exprs.\n" + 57 | " Execution then jumps back to the recursion point, a loop or fn method."; 58 | case "set!": 59 | return " (set! var-symbol expr)\n" + 60 | " (set! (. instance-expr instanceFieldName-symbol) expr)\n" + 61 | " (set! (. Classname-symbol staticFieldName-symbol) expr)\n" + 62 | "Special Form\n" + 63 | " Used to set thread-local-bound vars, Java object instance\n" + 64 | " fields, and Java class static fields."; 65 | case "throw": 66 | return " (throw expr)\n" + 67 | "Special Form\n" + 68 | " The expr is evaluated and thrown, therefore it should\n" + 69 | " yield an instance of some derivee of Throwable."; 70 | case "try": 71 | case "catch": 72 | case "finally": 73 | return " (try expr* catch-clause* finally-clause?)\n" + 74 | " catch-clause => (catch classname name expr*)\n" + 75 | "finally-clause => (finally expr*)\n" + 76 | "Special Form\n" + 77 | " Catches and handles Java exceptions."; 78 | case "var": 79 | return " (var symbol)\n" + 80 | "Special Form\n" + 81 | " The symbol must resolve to a var, and the Var object itself\n" + 82 | " (not its value) is returned. The reader macro #'x expands to (var x)."; 83 | } 84 | return null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/ClojureIndenterTest.java: -------------------------------------------------------------------------------- 1 | import freditor.Freditor; 2 | import org.junit.Test; 3 | 4 | import static org.junit.Assert.assertArrayEquals; 5 | 6 | public class ClojureIndenterTest { 7 | private static Freditor text(String s) { 8 | Freditor text = new Freditor(Flexer.instance, ClojureIndenter.instance, null); 9 | text.load(s); 10 | return text; 11 | } 12 | 13 | @Test 14 | public void square() { 15 | Freditor square = text(" ;first line\n (def square [x]\n (let [y\n" + "(* x x)]\n" + "y))"); 16 | int[] actual = ClojureIndenter.instance.corrections(square); 17 | int[] expected = {-3, -7, 1, 8, 4}; 18 | assertArrayEquals(expected, actual); 19 | } 20 | 21 | @Test 22 | public void tooManyClosingParens() { 23 | Freditor closing = text(" )\n a"); 24 | int[] actual = ClojureIndenter.instance.corrections(closing); 25 | int[] expected = {-1, -2}; 26 | assertArrayEquals(expected, actual); 27 | } 28 | 29 | @Test 30 | public void namespace() { 31 | Freditor text = text("(ns user\n(:require [clojure.string :as string]\n[clojure.test :refer [deftest is are run-tests]]))"); 32 | int[] actual = ClojureIndenter.instance.corrections(text); 33 | int[] expected = {0, 2, 12}; 34 | assertArrayEquals(expected, actual); 35 | } 36 | 37 | @Test 38 | public void newLineAfterKeyword() { 39 | Freditor text = text("(:count\nsession\n0)"); 40 | int[] actual = ClojureIndenter.instance.corrections(text); 41 | int[] expected = {0, 2, 2}; 42 | assertArrayEquals(expected, actual); 43 | } 44 | } 45 | --------------------------------------------------------------------------------