diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2026-02-21 12:29:54 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2026-02-21 12:29:54 -0800 |
| commit | 170f5c496054c9c5c2df9b8a1c5c8df7643ea004 (patch) | |
| tree | 1ed217130939a04234761d54ba2ec607866f51ad /core/src/main/java/coffee/liz/abstractionengine/app/ui | |
| parent | d6a294e14604aecd9046cbc19dc1837ca694fbb0 (diff) | |
| download | the-abstraction-engine-java-main.tar.gz the-abstraction-engine-java-main.zip | |
Diffstat (limited to 'core/src/main/java/coffee/liz/abstractionengine/app/ui')
8 files changed, 249 insertions, 35 deletions
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java index 11a4b0e..1affd43 100644 --- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java @@ -3,78 +3,214 @@ package coffee.liz.abstractionengine.app.ui.editor; import coffee.liz.abstractionengine.app.Theme; import coffee.liz.abstractionengine.app.ui.editor.vim.VimEngine; import coffee.liz.abstractionengine.app.utils.FontHelper; +import coffee.liz.lambda.LambdaDriver; +import coffee.liz.lambda.ast.LambdaProgram; +import coffee.liz.lambda.ast.SourceCode; +import coffee.liz.lambda.format.Formatter; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.GlyphLayout; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Align; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.TableUtils; import com.kotcrab.vis.ui.util.highlight.Highlighter; import com.kotcrab.vis.ui.widget.VisLabel; +import com.kotcrab.vis.ui.widget.VisSelectBox; +import com.kotcrab.vis.ui.widget.VisTable; +import com.kotcrab.vis.ui.widget.VisTextButton; import com.kotcrab.vis.ui.widget.VisTextField.VisTextFieldStyle; import com.kotcrab.vis.ui.widget.VisWindow; +import lombok.Getter; + +import java.util.function.Consumer; public class LambdaEditor extends VisWindow { private static final int FONT_SIZE = 15; private static final float WINDOW_ALPHA = 0.92f; - private static final String DEFAULT_TEXT = "-- Church numerals\n" + "let zero = λf.λx.x;\n" - + "let one = λf.λx.f x;\n" + "let succ = λn.λf.λx.f (n f x);\n" + "let add = λm.λn.λf.λx.m f (n f x);\n" - + "\n" + "succ (add one zero)\n"; - private final BitmapFont font; + private final Consumer<SourceCode> onSave; + private final boolean vimMode; + private final VisLabel statusLabel; + private final VisSelectBox<SourceCode.Syntax> syntaxSwitch; + @Getter + private final EditorTextArea editorTextArea; + private SourceCode.Syntax syntax; + private boolean updatingSyntaxSwitch = false; - public LambdaEditor(final boolean vimMode) { + public LambdaEditor(final boolean vimMode, final SourceCode initialSource, final Consumer<SourceCode> onSave) { super("λ-editor"); + this.vimMode = vimMode; + this.onSave = onSave != null ? onSave : _ -> { }; + this.syntax = syntaxOf(initialSource); + this.font = FontHelper.initFont(FONT_SIZE, FontHelper.MONO, 1f); + this.statusLabel = new VisLabel(vimMode ? "-- NORMAL --" : "-- INSERT --"); + this.syntaxSwitch = new VisSelectBox<>(); TableUtils.setSpacingDefaults(this); columnDefaults(0).left(); - this.font = FontHelper.initFont(FONT_SIZE, FontHelper.MONO, 1f); - setResizable(true); addCloseButton(); - addEditor(vimMode); - setSize(400, 300); - setColor(1, 1, 1, WINDOW_ALPHA); - centerWindow(); - } - - private void addEditor(final boolean vimMode) { final VisTextFieldStyle style = new VisTextFieldStyle(VisUI.getSkin().get("textArea", VisTextFieldStyle.class)); style.font = font; final GlyphLayout layout = new GlyphLayout(font, "000 "); final float gutterWidth = layout.width; - final EditorTextArea textArea = new EditorTextArea(DEFAULT_TEXT, style, gutterWidth); - textArea.setHighlighter(createHighlighter()); + this.editorTextArea = new EditorTextArea(sourceOf(initialSource), style, gutterWidth); + editorTextArea.setHighlighter(createHighlighter(syntax)); - final ScrollPane scrollPane = textArea.createCompatibleScrollPane(); - scrollPane.setFadeScrollBars(false); + bindInput(vimMode); + buildLayout(); + bindSyntaxSwitch(); + syntaxSwitch.setSelected(syntax); - final VisLabel statusLabel = new VisLabel(vimMode ? "-- NORMAL --" : "-- INSERT --"); - statusLabel.setAlignment(Align.left); + setSize(560, 380); + setColor(1, 1, 1, WINDOW_ALPHA); + } - if (vimMode) { - final VimEngine controller = new VimEngine(textArea, statusLabel::setText); - textArea.setInputStrategy(controller); - statusLabel.setText(controller.getStatusText()); + private void bindInput(final boolean vimEnabled) { + statusLabel.setAlignment(Align.left); + if (!vimEnabled) { + return; } + final VimEngine controller = new VimEngine(editorTextArea, statusLabel::setText, this::executeExCommand); + editorTextArea.setInputStrategy(controller); + statusLabel.setText(controller.getStatusText()); + } + + private void buildLayout() { + final ScrollPane scrollPane = editorTextArea.createCompatibleScrollPane(); + scrollPane.setFadeScrollBars(false); + + final VisTextButton saveButton = new VisTextButton(vimMode ? "Save (:w)" : "Save"); + saveButton.addListener(new ChangeListener() { + @Override + public void changed(final ChangeEvent event, final com.badlogic.gdx.scenes.scene2d.Actor actor) { + saveCurrentTerm(); + } + }); + + final VisTable controls = new VisTable(); + controls.add(statusLabel).growX().left(); + controls.add(syntaxSwitch).padLeft(8f); + controls.add(saveButton).padLeft(8f); add(scrollPane).grow().row(); - add(statusLabel).growX().padTop(6f); + add(controls).growX().padTop(6f); + } + + private void bindSyntaxSwitch() { + syntaxSwitch.setItems(SourceCode.Syntax.ARROW, SourceCode.Syntax.LAMBDA); + syntaxSwitch.addListener(new ChangeListener() { + @Override + public void changed(final ChangeEvent event, final com.badlogic.gdx.scenes.scene2d.Actor actor) { + if (updatingSyntaxSwitch) { + return; + } + applySyntaxSwitch(syntaxSwitch.getSelected()); + } + }); + } + + private void applySyntaxSwitch(final SourceCode.Syntax nextSyntax) { + if (nextSyntax == syntax) { + return; + } + try { + final LambdaProgram program = LambdaDriver.parse(sourceFromEditor()); + final String formatted = Formatter.emit(program, nextSyntax); + syntax = nextSyntax; + editorTextArea.setText(formatted); + editorTextArea.syncCursorState(); + editorTextArea.setHighlighter(createHighlighter(nextSyntax)); + setStatus("Converted to " + nextSyntax.name().toLowerCase() + " syntax"); + } catch (final RuntimeException parseException) { + setStatus("Cannot switch syntax: parse error"); + updatingSyntaxSwitch = true; + syntaxSwitch.setSelected(syntax); + updatingSyntaxSwitch = false; + } + } + + private void executeExCommand(final String command) { + final String normalized = command == null ? "" : command.trim(); + switch (normalized) { + case "w" -> saveCurrentTerm(); + case "wq" -> { + if (saveCurrentTerm()) { + remove(); + } + } + case "q" -> remove(); + default -> setStatus("Unknown command :" + normalized); + } + } + + private boolean saveCurrentTerm() { + try { + final LambdaProgram program = LambdaDriver.parse(sourceFromEditor()); + final String formatted = Formatter.emit(program, syntax); + final SourceCode updatedTerm = sourceCodeForSyntax(formatted, syntax); + editorTextArea.setText(formatted); + editorTextArea.syncCursorState(); + onSave.accept(updatedTerm); + setStatus("Saved"); + return true; + } catch (final RuntimeException parseException) { + setStatus("Save failed: parse error"); + return false; + } + } + + private SourceCode sourceFromEditor() { + return sourceCodeForSyntax(editorTextArea.getText(), syntax); } - private static Highlighter createHighlighter() { + private static SourceCode sourceCodeForSyntax(final String source, final SourceCode.Syntax syntax) { + return switch (syntax) { + case ARROW -> SourceCode.ofArrow(source); + case LAMBDA -> SourceCode.ofLambda(source); + }; + } + + private static SourceCode.Syntax syntaxOf(final SourceCode sourceCode) { + return switch (sourceCode) { + case SourceCode.Arrow ignored -> SourceCode.Syntax.ARROW; + case SourceCode.Lambda ignored -> SourceCode.Syntax.LAMBDA; + }; + } + + private static String sourceOf(final SourceCode sourceCode) { + return switch (sourceCode) { + case SourceCode.Arrow(String source) -> source; + case SourceCode.Lambda(String source) -> source; + }; + } + + private void setStatus(final String text) { + if (text == null || text.isBlank()) { + return; + } + statusLabel.setText(text); + } + + private static Highlighter createHighlighter(final SourceCode.Syntax syntax) { final Highlighter highlighter = new Highlighter(); highlighter.regex(Theme.MUTED, "--[^\\n]*"); highlighter.regex(Theme.PRIMARY, "\\blet\\b"); - highlighter.regex(Theme.SECONDARY, "[λ\\\\]"); - highlighter.regex(Theme.SECONDARY, "\\."); highlighter.regex(Theme.BORDER_LIGHT, "[=;]"); highlighter.regex(Theme.BORDER_LIGHT, "[()]"); + switch (syntax) { + case ARROW -> highlighter.regex(Theme.SECONDARY, "->"); + case LAMBDA -> { + highlighter.regex(Theme.SECONDARY, "[λ\\\\]"); + highlighter.regex(Theme.SECONDARY, "\\."); + } + } return highlighter; } } diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java index ab5f203..d2fc843 100644 --- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java @@ -9,6 +9,7 @@ import coffee.liz.abstractionengine.app.ui.editor.RenderState; import coffee.liz.abstractionengine.app.ui.editor.EditorTextArea; import coffee.liz.abstractionengine.app.ui.editor.vim.buffer.TextAreaBuffer; import coffee.liz.abstractionengine.app.ui.editor.vim.context.KeyContext; +import coffee.liz.abstractionengine.app.ui.editor.vim.handler.CommandModeHandler; import coffee.liz.abstractionengine.app.ui.editor.vim.handler.HandleResult; import coffee.liz.abstractionengine.app.ui.editor.vim.handler.InsertModeHandler; import coffee.liz.abstractionengine.app.ui.editor.vim.handler.ModeHandler; @@ -21,6 +22,7 @@ import coffee.liz.abstractionengine.app.ui.editor.vim.model.UndoManager; import coffee.liz.abstractionengine.app.ui.editor.vim.navigation.TextNavigation; import coffee.liz.abstractionengine.app.ui.editor.vim.state.InsertState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.ModeState; +import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.SearchState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualLineState; @@ -39,38 +41,55 @@ public class VimEngine implements InputStrategy { private final ModeHandler<VisualState> visualModeHandler; private final ModeHandler<VisualLineState> visualLineModeHandler; private final ModeHandler<SearchState> searchModeHandler; + private final ModeHandler<CommandState> commandModeHandler; private ModeState state; public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink, final VimConfig config) { - this(new TextAreaBuffer(textArea), statusSink, config); + this(new TextAreaBuffer(textArea), statusSink, null, config); + } + + public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink, + final Consumer<String> commandSink, final VimConfig config) { + this(new TextAreaBuffer(textArea), statusSink, commandSink, config); + } + + public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink, + final Consumer<String> commandSink) { + this(new TextAreaBuffer(textArea), statusSink, commandSink, VimConfig.defaults()); } public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink) { - this(textArea, statusSink, VimConfig.defaults()); + this(textArea, statusSink, null, VimConfig.defaults()); } public VimEngine(final EditorTextArea textArea) { - this(textArea, null, VimConfig.defaults()); + this(textArea, null, null, VimConfig.defaults()); } public VimEngine(final EditorBuffer buffer, final Consumer<String> statusSink, final VimConfig config) { + this(buffer, statusSink, null, config); + } + + public VimEngine(final EditorBuffer buffer, final Consumer<String> statusSink, final Consumer<String> commandSink, + final VimConfig config) { this.config = config != null ? config : VimConfig.defaults(); this.context = new KeyContext(buffer, statusSink, new TextNavigation(), - new UndoManager(this.config.maxUndoStates()), new Register(), this.config); + new UndoManager(this.config.maxUndoStates()), new Register(), this.config, commandSink); this.statusFormatter = new StatusFormatter(); this.normalModeHandler = new NormalModeHandler(); this.insertModeHandler = new InsertModeHandler(); this.visualModeHandler = new VisualModeHandler(); this.visualLineModeHandler = new VisualLineModeHandler(); this.searchModeHandler = new SearchModeHandler(); + this.commandModeHandler = new CommandModeHandler(); this.state = NormalState.DEFAULT; context.recordUndoState(); publishStatus(); } public VimEngine(final EditorBuffer buffer, final Consumer<String> statusSink) { - this(buffer, statusSink, VimConfig.defaults()); + this(buffer, statusSink, null, VimConfig.defaults()); } @Override @@ -160,6 +179,7 @@ public class VimEngine implements InputStrategy { case VisualState visualState -> visualModeHandler.onKeyDown(visualState, context); case VisualLineState visualLineState -> visualLineModeHandler.onKeyDown(visualLineState, context); case SearchState searchState -> searchModeHandler.onKeyDown(searchState, context); + case CommandState commandState -> commandModeHandler.onKeyDown(commandState, context); }; } @@ -171,6 +191,7 @@ public class VimEngine implements InputStrategy { case VisualLineState visualLineState -> visualLineModeHandler.onKeyTyped(visualLineState, character, context); case SearchState searchState -> searchModeHandler.onKeyTyped(searchState, character, context); + case CommandState commandState -> commandModeHandler.onKeyTyped(commandState, character, context); }; } diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java index 285bbde..e269b58 100644 --- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java @@ -33,6 +33,7 @@ public class KeyContext { private final UndoManager undoManager; private final Register register; private final VimConfig config; + private final Consumer<String> commandExecutor; @Getter private String statusText; @@ -48,13 +49,14 @@ public class KeyContext { public KeyContext(final EditorBuffer editorBuffer, final Consumer<String> statusUpdater, final TextNavigation navigation, final UndoManager undoManager, final Register register, - final VimConfig config) { + final VimConfig config, final Consumer<String> commandExecutor) { this.editorBuffer = editorBuffer; this.statusUpdater = statusUpdater; this.navigation = navigation; this.undoManager = undoManager; this.register = register; this.config = config; + this.commandExecutor = commandExecutor; this.statusText = ""; this.lastSearchQuery = ""; this.lastSearchDirection = SearchDirection.FORWARD; @@ -66,6 +68,12 @@ public class KeyContext { this.currentKeyEvent = new KeyEvent(Input.Keys.UNKNOWN, Set.of()); } + public void executeCommand(final String command) { + if (commandExecutor != null) { + commandExecutor.accept(command == null ? "" : command); + } + } + public EditorBuffer editorBuffer() { return editorBuffer; } diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/CommandModeHandler.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/CommandModeHandler.java new file mode 100644 index 0000000..fea2f8a --- /dev/null +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/CommandModeHandler.java @@ -0,0 +1,38 @@ +package coffee.liz.abstractionengine.app.ui.editor.vim.handler; + +import coffee.liz.abstractionengine.app.ui.editor.vim.context.KeyContext; +import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState; +import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState; +import com.badlogic.gdx.Input; + +public class CommandModeHandler implements ModeHandler<CommandState> { + @Override + public HandleResult onKeyDown(final CommandState state, final KeyContext context) { + if (context.currentKeyEvent().keycode() == Input.Keys.ESCAPE) { + return HandleResult.handled(NormalState.DEFAULT); + } + return HandleResult.handled(); + } + + @Override + public HandleResult onKeyTyped(final CommandState state, final char character, final KeyContext context) { + if (character == '\r' || character == '\n') { + context.executeCommand(state.command().toString()); + return HandleResult.handled(NormalState.DEFAULT); + } + + if (character == '\b') { + if (state.command().length() > 0) { + state.command().setLength(state.command().length() - 1); + } + return HandleResult.handled(state); + } + + if (character < 32 || character == 127) { + return HandleResult.handled(state); + } + + state.command().append(character); + return HandleResult.handled(state); + } +} diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java index 95dc2bc..3fc24d3 100644 --- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java @@ -6,6 +6,7 @@ import coffee.liz.abstractionengine.app.ui.editor.vim.search.SearchDirection; import coffee.liz.abstractionengine.app.ui.editor.vim.state.InsertState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.PendingOperator; +import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.SearchState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualLineState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualState; @@ -296,6 +297,7 @@ public class NormalModeHandler implements ModeHandler<NormalState> { return HandleResult.handled(new NormalState(0, PendingOperator.NONE, state.preferredColumn())); }); map.put('u', (state, context) -> HandleResult.handled(context.undoToNormalState())); + map.put(':', (state, context) -> HandleResult.handled(new CommandState(new StringBuilder()))); return map; } diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/CommandState.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/CommandState.java new file mode 100644 index 0000000..e0112e0 --- /dev/null +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/CommandState.java @@ -0,0 +1,7 @@ +package coffee.liz.abstractionengine.app.ui.editor.vim.state; + +public record CommandState(StringBuilder command) implements ModeState { + public CommandState { + command = command == null ? new StringBuilder() : command; + } +} diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java index e878dd7..20ce01e 100644 --- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java @@ -1,6 +1,6 @@ package coffee.liz.abstractionengine.app.ui.editor.vim.state; -public sealed interface ModeState permits InsertState, NormalState, SearchState, VisualLineState, VisualState { +public sealed interface ModeState permits CommandState, InsertState, NormalState, SearchState, VisualLineState, VisualState { default PendingOperator pendingOperator() { return PendingOperator.NONE; } diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java index 5ff2654..d38f9d6 100644 --- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java +++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java @@ -3,6 +3,7 @@ package coffee.liz.abstractionengine.app.ui.editor.vim.status; import coffee.liz.abstractionengine.app.ui.editor.vim.VimConfig; import coffee.liz.abstractionengine.app.ui.editor.vim.state.InsertState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.ModeState; +import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState; import coffee.liz.abstractionengine.app.ui.editor.vim.state.PendingOperator; import coffee.liz.abstractionengine.app.ui.editor.vim.state.SearchState; @@ -17,6 +18,7 @@ public class StatusFormatter { case VisualState ignored -> config.visualStatusLabel(); case VisualLineState ignored -> config.visualLineStatusLabel(); case SearchState searchState -> '/' + searchState.query().toString(); + case CommandState commandState -> ':' + commandState.command().toString(); }; } |
