diff --git a/home/jashton.nix b/home/jashton.nix index bf1cc81..68775df 100644 --- a/home/jashton.nix +++ b/home/jashton.nix @@ -65,34 +65,126 @@ in custom_wvkbd + (pkgs.writeShellScriptBin "jroot" '' + d="$PWD" + while [ "$d" != "/" ]; do + if [ -f "$d/.classpath" ]; then printf '%s\n' "$d"; exit 0; fi + d="$(dirname "$d")" + done + echo "jroot: no .classpath found at or above $PWD" >&2 + exit 1 + '') + + (pkgs.writeShellScriptBin "jbuild" '' + set -euo pipefail + # Assign first: `cd "$(jroot)"` would swallow a jroot failure, because + # `cd ""` is a successful no-op and set -e never sees the error. + ROOT="$(jroot)" + cd "$ROOT" + mkdir -p bin + SOURCES="$(mktemp)" + trap 'rm -f "$SOURCES"' EXIT + find . -path ./bin -prune -o -name '.*' -prune -o -name '*.java' -print > "$SOURCES" + if [ ! -s "$SOURCES" ]; then + echo "jbuild: no .java files found under $ROOT" >&2 + exit 1 + fi + # bin is deliberately NOT on the compile classpath: every source is passed + # explicitly, so a stale .class from a deleted file would silently satisfy + # references that ought to fail. + javac -g --release 21 -encoding UTF-8 -d bin -cp "lib/*" @"$SOURCES" + '') + + (pkgs.writeShellScriptBin "jclean" '' + set -euo pipefail + ROOT="$(jroot)" + rm -rf "$ROOT/bin" + echo "cleaned $ROOT/bin" + '') + + (pkgs.writeShellScriptBin "jrun" '' + set -euo pipefail + if [ $# -lt 1 ]; then + echo "usage: jrun [args...]" >&2 + exit 1 + fi + ROOT="$(jroot)" + jbuild + cd "$ROOT" + MAIN="$1"; shift + exec java -cp "bin:lib/*" "$MAIN" "$@" + '') + + (pkgs.writeShellScriptBin "jtest" '' + set -euo pipefail + # sort -V so 1.10.2 beats 1.9.0; plain tail -n1 sorts those the wrong way. + JUNIT="$(ls -1 "$HOME"/dev/jars/junit-platform-console-standalone-*.jar 2>/dev/null | sort -V | tail -n1 || true)" + if [ -z "$JUNIT" ]; then + echo "jtest: no junit-platform-console-standalone jar in ~/dev/jars" >&2 + exit 1 + fi + ROOT="$(jroot)" + jbuild + cd "$ROOT" + if [ $# -eq 0 ]; then + exec java -jar "$JUNIT" execute --class-path "bin:lib/*" --scan-class-path --details=tree + fi + ARGS="" + for pkg in "$@"; do + ARGS="$ARGS --select-package $pkg" + done + exec java -jar "$JUNIT" execute --class-path "bin:lib/*" $ARGS --details=tree + '') + (pkgs.writeShellScriptBin "fkill" '' - if [ -z "$1" ]; then - echo "Need the name of one application" - echo "eg. fkill google-chrome" + set -eu + if [ $# -lt 1 ]; then + echo "usage: fkill " >&2 + echo "eg. fkill firefox" >&2 exit 1 fi - PROGRAM=$1 + PROGRAM="$1" - ps aux | grep $PROGRAM | awk '{print $2}' | xargs kill + if ! pgrep -x -- "$PROGRAM" >/dev/null 2>&1; then + echo "fkill: no process named '$PROGRAM'" >&2 + exit 1 + fi + + pgrep -a -x -- "$PROGRAM" + printf 'Kill these? [y/N] ' + read -r reply + case "$reply" in + [yY]*) pkill -x -- "$PROGRAM" ;; + *) echo "aborted" ;; + esac '') (pkgs.writeShellScriptBin "find-open-port" '' - if [ -z "$1" ]; then + set -u + if [ $# -lt 1 ]; then echo "Please provide an application name, project directory, or other identifier." - echo " Usage: find-open-port $HOME/dev/some-project" + echo ' Usage: find-open-port $HOME/dev/some-project' echo " Usage: find-open-port some-app" - + exit 1 fi - APP_NAME=$1 - REGISTRY_FILE=$HOME/.dotfiles/ports.registry + APP_NAME="$1" + REGISTRY_FILE="$HOME/.dotfiles/ports.registry" MIN_PORT=8000 MAX_PORT=8999 MAX_RETRIES=50 + mkdir -p "$(dirname "$REGISTRY_FILE")" + + if [ -f "$REGISTRY_FILE" ] && grep -q "^$APP_NAME : " "$REGISTRY_FILE"; then + echo "Already registered:" + grep "^$APP_NAME : " "$REGISTRY_FILE" + exit 0 + fi + echo "Searching for available port between $MIN_PORT and $MAX_PORT for $APP_NAME..." for ((i=1; i<=MAX_RETRIES; i++)); do @@ -163,4 +255,3 @@ in home.stateVersion = "26.05"; # DO NOT CHANGE } - diff --git a/home/programs/nvim.nix b/home/programs/nvim.nix index d5e7153..a866a0d 100644 --- a/home/programs/nvim.nix +++ b/home/programs/nvim.nix @@ -1,5 +1,8 @@ { config, pkgs, ... }: +let + javaDebug = pkgs.vscode-extensions.vscjava.vscode-java-debug; +in { programs.neovim = { enable = true; @@ -27,9 +30,8 @@ vim.opt.updatetime = 50 vim.opt.foldmethod = "indent" vim.opt.foldlevel = 99 - + vim.g.mapleader = " " - vim.keymap.set("n", "", ":") vim.keymap.set("n", "w", "w", { silent = true }) vim.keymap.set("n", "q", "q", { silent = true }) vim.keymap.set("n", "wq", "wq", { silent = true }) @@ -67,7 +69,7 @@ }) require("lualine").setup({ - ons = { theme = "auto" }, + options = { theme = "auto" }, }) require("neocord").setup({ @@ -86,7 +88,14 @@ }, }) - -- Java indentation must match Eclipse's formatter, not your 2-space default. + -- Indent is left off deliberately: treesitter's Java indent fights the + -- Eclipse-matching tab settings applied in the FileType autocmd below. + require("nvim-treesitter.configs").setup({ + highlight = { enable = true }, + indent = { enable = false }, + }) + + -- Java indentation must match Eclipse's formatter, not the 2-space default. vim.api.nvim_create_autocmd("FileType", { pattern = "java", callback = function() @@ -117,24 +126,41 @@ local capabilities = require("cmp_nvim_lsp").default_capabilities() - -- .project and .classpath come first so a repo containing several - -- assignments resolves to the assignment, not the repo root. - local root_markers = { ".project", ".classpath", "pom.xml", "build.gradle", ".git" } + local bundles = vim.fn.glob( + "${javaDebug}/share/vscode/extensions/vscjava.vscode-java-debug/server/com.microsoft.java.debug.plugin-*.jar", + true, true) + if #bundles == 0 then + vim.notify("java-debug bundle not found - Java debugging disabled", vim.log.levels.WARN) + end + + local java_home = os.getenv("JAVA_HOME") + if not java_home then + vim.notify("JAVA_HOME unset - jdtls will guess a runtime", vim.log.levels.WARN) + end + + -- Eclipse metadata wins over build files, build files over .git, and the + -- file's own directory is the last resort so bare folders still attach. + local function find_project_root() + local setup = require("jdtls.setup") + return setup.find_root({ ".project", ".classpath" }) + or setup.find_root({ "pom.xml", "build.gradle", "build.gradle.kts" }) + or setup.find_root({ ".git" }) + or vim.fn.expand("%:p:h") + end + + local dap_configured = false local function start_jdtls() local jdtls = require("jdtls") - local root_dir = require("jdtls.setup").find_root(root_markers) - if not root_dir or root_dir == "" then return end + local root_dir = find_project_root() local project_name = vim.fn.fnamemodify(root_dir, ":p:h:t") + .. "-" .. vim.fn.sha256(root_dir):sub(1, 8) local workspace = vim.fn.stdpath("cache") .. "/jdtls-workspaces/" .. project_name local java_settings = { configuration = { - updateBuildConfiguration = "interactive", - runtimes = { - { name = "JavaSE-21", path = os.getenv("JAVA_HOME"), default = true }, - }, + updateBuildConfiguration = "automatic", }, -- Keep a stray pom.xml from overriding the Eclipse project model. -- Flip these on if a course actually requires Maven or Gradle. @@ -142,10 +168,20 @@ maven = { enabled = false }, gradle = { enabled = false }, }, + project = { + outputPath = "bin", + referencedLibraries = { "lib/**/*.jar" }, + }, signatureHelp = { enabled = true }, eclipse = { downloadSources = true }, } + if java_home then + java_settings.configuration.runtimes = { + { name = "JavaSE-21", path = java_home, default = true }, + } + end + -- If the repo carries an exported Eclipse formatter profile, use it verbatim. local formatter = root_dir .. "/.settings/eclipse-formatter.xml" if vim.uv.fs_stat(formatter) then @@ -157,8 +193,14 @@ root_dir = root_dir, capabilities = capabilities, settings = { java = java_settings }, - init_options = { bundles = {} }, + init_options = { bundles = bundles }, on_attach = function(_, bufnr) + require("jdtls").setup_dap({ hotcodereplace = "auto" }) + if not dap_configured then + dap_configured = true + require("jdtls.dap").setup_dap_main_class_configs() + end + local map = function(k, f) vim.keymap.set("n", k, f, { buffer = bufnr, silent = true }) end map("gd", vim.lsp.buf.definition) map("gr", vim.lsp.buf.references) @@ -176,15 +218,25 @@ callback = start_jdtls, }) - require("neotest").setup({ - adapters = { - require("neotest-java")({ - junit_jar = vim.uv.os_homedir() .. "/dev/jars/junit-*.jar", - -- jvm_args = { "-Xmx512m" }, - incremental_build = true, - }), - }, - }) + local dap, dapui = require("dap"), require("dapui") + dapui.setup() + require("nvim-dap-virtual-text").setup() + + dap.listeners.after.event_initialized["dapui"] = function() dapui.open() end + dap.listeners.before.event_terminated["dapui"] = function() dapui.close() end + dap.listeners.before.event_exited["dapui"] = function() dapui.close() end + + vim.keymap.set("n", "db", dap.toggle_breakpoint) + vim.keymap.set("n", "dB", function() + dap.set_breakpoint(vim.fn.input("Condition: ")) + end) + vim.keymap.set("n", "dc", dap.continue) + vim.keymap.set("n", "dn", dap.step_over) + vim.keymap.set("n", "di", dap.step_into) + vim.keymap.set("n", "du", dap.step_out) + vim.keymap.set("n", "dr", dap.repl.toggle) + vim.keymap.set("n", "dq", dap.terminate) + vim.keymap.set({ "n", "v" }, "de", function() dapui.eval() end) ''; plugins = with pkgs.vimPlugins; [ @@ -197,11 +249,9 @@ { plugin = gitsigns-nvim; } { plugin = nvim-jdtls; } - { plugin = nvim-nio; } - { plugin = neotest; } - { plugin = neotest-java; } { plugin = nvim-dap; } { plugin = nvim-dap-ui; } + { plugin = nvim-nio; } { plugin = nvim-dap-virtual-text; } { plugin = nvim-cmp; } { plugin = cmp-nvim-lsp; } diff --git a/system/packages.nix b/system/packages.nix index c10bee3..fa44fc8 100644 --- a/system/packages.nix +++ b/system/packages.nix @@ -68,6 +68,7 @@ trap "rm -f '$ERRFILE'" EXIT echo "Building NixOS..." + cd $DOTFILES && git stage -A sudo nixos-rebuild switch --flake "$DOTFILES#$HOST" 2>&1 | tee "$ERRFILE" STATUS=''${PIPESTATUS[0]}