diff --git a/init.lua b/init.lua index 4ebed13..5ca0efd 100644 --- a/init.lua +++ b/init.lua @@ -15,7 +15,8 @@ require("user.files_utils") require("user.commands") require("user.comment") require("user.trouble") -require("user.java") +require("user.android") +require("user.flutter") return require("packer").startup(function(use) -- Let packer manage itself diff --git a/jars/ktfmt.jar b/jars/ktfmt.jar new file mode 100644 index 0000000..9ca8572 Binary files /dev/null and b/jars/ktfmt.jar differ diff --git a/lua/user/android.lua b/lua/user/android.lua new file mode 100644 index 0000000..3cc96d5 --- /dev/null +++ b/lua/user/android.lua @@ -0,0 +1,86 @@ +-- lua/user/android.lua +-- Android development support for Neovim: +-- * exports the Android SDK env (so LSP, Gradle and :terminal jobs see it) +-- * :Android* commands + a* keymaps for build / run / test / logcat / emulator +-- +-- Java (nvim-jdtls) and Kotlin (JetBrains kotlin-lsp) are configured elsewhere; this +-- module is purely the build/run/device tooling around them. + +local sdk = vim.fn.expand("~/Android/Sdk") + +-- 1. Make the Android SDK visible to LSP, Gradle and terminal jobs launched from nvim. +vim.env.ANDROID_HOME = vim.env.ANDROID_HOME or sdk +vim.env.ANDROID_SDK_ROOT = vim.env.ANDROID_SDK_ROOT or sdk +do + local path = vim.env.PATH or "" + local extra = { + sdk .. "/platform-tools", -- adb + sdk .. "/emulator", -- emulator + sdk .. "/cmdline-tools/latest/bin",-- sdkmanager/avdmanager + vim.fn.expand("~/.local/bin"), -- the `android` CLI + } + for _, p in ipairs(extra) do + if vim.fn.isdirectory(p) == 1 and not string.find(path, p, 1, true) then + path = p .. ":" .. path + end + end + vim.env.PATH = path +end + +-- 2. Locate the Gradle project root (nearest ancestor with a gradlew wrapper). +local function gradle_root() + local start = vim.fn.expand("%:p:h") + if start == "" then start = vim.fn.getcwd() end + local found = vim.fs.find("gradlew", { upward = true, path = start }) + if found and found[1] then + return vim.fn.fnamemodify(found[1], ":h") + end + return vim.fn.getcwd() +end + +-- 3. Run a shell command in a bottom terminal split (live, scrollable output). +local function run_in_term(shell_cmd) + vim.cmd("botright 15split") + vim.cmd("terminal " .. shell_cmd) + vim.cmd("startinsert") +end + +local function run_gradle(args) + run_in_term(string.format("cd %s && ./gradlew %s", vim.fn.shellescape(gradle_root()), args)) +end + +local function run_cmd(c) + run_in_term(string.format("cd %s && %s", vim.fn.shellescape(gradle_root()), c)) +end + +-- 4. User commands. +local cmd = vim.api.nvim_create_user_command +cmd("AndroidBuild", function() run_gradle("assembleDebug") end, { desc = "Gradle assembleDebug" }) +cmd("AndroidClean", function() run_gradle("clean") end, { desc = "Gradle clean" }) +cmd("AndroidInstall", function() run_gradle("installDebug") end, { desc = "Gradle installDebug" }) +cmd("AndroidTest", function() run_gradle("testDebugUnitTest") end,{ desc = "Gradle unit tests" }) +cmd("AndroidRun", function() run_cmd("android run") end, { desc = "Deploy app (android run)" }) +cmd("AndroidDevices", function() run_cmd("adb devices -l") end, { desc = "List adb devices" }) +cmd("AndroidLogcat", function() run_cmd("adb logcat -c; adb logcat") end, { desc = "adb logcat (cleared)" }) +cmd("AndroidEmulators", function() run_cmd("android emulator list") end, { desc = "List AVDs" }) +cmd("AndroidEmulator", function(o) + if o.args ~= "" then + run_cmd("android emulator start " .. vim.fn.shellescape(o.args)) + else + -- Default: start the first available AVD. + run_cmd([[android emulator start "$("]] .. sdk .. [[/emulator/emulator" -list-avds | head -1)"]]) + end +end, { nargs = "?", desc = "Start an emulator (optional AVD name)" }) +cmd("AndroidStopEmulator", function() run_cmd("android emulator stop") end, { desc = "Stop emulator" }) + +-- 5. Keymaps (A* = Android). +-- NOTE: uppercase A so it does NOT clash with a (LSP code action). +local map = vim.keymap.set +map("n", "Ab", "AndroidBuild", { silent = true, desc = "Android: build (assembleDebug)" }) +map("n", "Ar", "AndroidRun", { silent = true, desc = "Android: run/deploy" }) +map("n", "Ai", "AndroidInstall", { silent = true, desc = "Android: install" }) +map("n", "At", "AndroidTest", { silent = true, desc = "Android: unit tests" }) +map("n", "Al", "AndroidLogcat", { silent = true, desc = "Android: logcat" }) +map("n", "Ad", "AndroidDevices", { silent = true, desc = "Android: devices" }) +map("n", "Ae", "AndroidEmulator", { silent = true, desc = "Android: start emulator" }) +map("n", "Ac", "AndroidClean", { silent = true, desc = "Android: clean" }) diff --git a/lua/user/cmp.lua b/lua/user/cmp.lua index 386ffbc..598257e 100644 --- a/lua/user/cmp.lua +++ b/lua/user/cmp.lua @@ -3,6 +3,13 @@ local cmp = require("cmp") cmp.setup({ + -- Throttle LSP completion requests. + -- Default: no debounce → every keystroke fires a compiler pass (CPU heat). + performance = { + debounce = 300, -- wait 300ms after typing stops before requesting + throttle = 500, -- max one completion request every 500ms + max_view_entries = 30, + }, snippet = { expand = function(args) require("luasnip").lsp_expand(args.body) diff --git a/lua/user/flutter.lua b/lua/user/flutter.lua new file mode 100644 index 0000000..5bdfef0 --- /dev/null +++ b/lua/user/flutter.lua @@ -0,0 +1,63 @@ +-- Flutter-specific Neovim configuration +-- Keymaps for Flutter development workflow + +local function flutter_root() + return vim.fn.finddir("android", vim.fn.getcwd() .. ";") ~= "" + or vim.fn.findfile("pubspec.yaml", vim.fn.getcwd() .. ";") ~= "" +end + +-- Create Flutter-specific user commands +vim.api.nvim_create_user_command("FlutterRun", function() + vim.cmd("split | terminal flutter run") +end, {}) + +vim.api.nvim_create_user_command("FlutterClean", function() + vim.cmd("terminal flutter clean") +end, {}) + +vim.api.nvim_create_user_command("FlutterPubGet", function() + vim.cmd("terminal flutter pub get") +end, {}) + +vim.api.nvim_create_user_command("FlutterPubOutdated", function() + vim.cmd("terminal flutter pub outdated") +end, {}) + +vim.api.nvim_create_user_command("FlutterPubUpgrade", function() + vim.cmd("terminal flutter pub upgrade") +end, {}) + +vim.api.nvim_create_user_command("FlutterAnalyze", function() + vim.cmd("terminal flutter analyze") +end, {}) + +vim.api.nvim_create_user_command("FlutterTest", function() + vim.cmd("terminal flutter test") +end, {}) + +vim.api.nvim_create_user_command("FlutterBuildApk", function() + vim.cmd("terminal flutter build apk") +end, {}) + +vim.api.nvim_create_user_command("FlutterDevices", function() + vim.cmd("terminal flutter devices") +end, {}) + +-- Dart filetype-specific settings +vim.api.nvim_create_autocmd("FileType", { + pattern = "dart", + callback = function() + vim.opt_local.expandtab = true + vim.opt_local.tabstop = 2 + vim.opt_local.shiftwidth = 2 + vim.opt_local.softtabstop = 2 + vim.opt_local.commentstring = "// %s" + vim.opt_local.colorcolumn = "80" + end, +}) + +-- Open Flutter DevTools in browser +vim.api.nvim_set_keymap("n", "fd", ":!flutter devtools", { noremap = true, silent = false, desc = "Flutter: DevTools" }) + +-- Flutter Run in a new tmux split (if tmux available) +vim.api.nvim_set_keymap("n", "fe", ":terminal flutter emulators", { noremap = true, silent = false, desc = "Flutter: List Emulators" }) diff --git a/lua/user/lsp.lua b/lua/user/lsp.lua index 356f307..a606e89 100644 --- a/lua/user/lsp.lua +++ b/lua/user/lsp.lua @@ -31,3 +31,4 @@ require("user.lsp_servers.ts_ls") require("user.lsp_servers.marksman") require("user.lsp_servers.kotlin_ls") require("user.lsp_servers.graphql") +require("user.lsp_servers.dartls") diff --git a/lua/user/lsp_servers/dartls.lua b/lua/user/lsp_servers/dartls.lua new file mode 100644 index 0000000..74c8ed2 --- /dev/null +++ b/lua/user/lsp_servers/dartls.lua @@ -0,0 +1,79 @@ +-- Dart/Flutter LSP configuration +-- dartls is bundled with the Dart SDK (which ships with Flutter) + +local function get_dart_sdk_path() + -- Try Flutter's bundled Dart SDK first + local flutter_dart = vim.fn.expand("~/development/flutter/bin/cache/dart-sdk/bin/dart") + if vim.fn.executable(flutter_dart) == 1 then + return flutter_dart + end + -- Fall back to system dart + return "dart" +end + +vim.lsp.config("dartls", { + cmd = { get_dart_sdk_path(), "language-server", "--protocol=lsp" }, + filetypes = { "dart" }, + root_markers = { "pubspec.yaml", "pubspec.yml" }, + + -- Dart-specific settings + settings = { + dart = { + -- Enable Flutter integration + completeFunctionCalls = true, + showTodos = true, + runPubGetOnPubspecChanges = true, + updateImportsOnRename = true, + closingLabels = true, + -- Flutter-specific + enableSdkFormatter = true, + lineLength = 80, + -- Analysis + enableSnippets = true, + }, + }, +}) + +-- Dart/Flutter-specific LSP attach handlers +vim.api.nvim_create_autocmd("LspAttach", { + callback = function(args) + local client = vim.lsp.get_client_by_id(args.data.client_id) + if client and client.name == "dartls" then + local bufnr = args.buf + + -- Flutter hot reload + vim.keymap.set("n", "fr", function() + vim.lsp.buf_request(bufnr, "dart/reanalyze", nil) + vim.cmd("write") + vim.cmd("!flutter hot-reload 2>/dev/null || flutter run --hot") + end, { buffer = bufnr, desc = "Flutter: Hot Reload" }) + + -- Flutter hot restart + vim.keymap.set("n", "fR", function() + vim.cmd("write") + vim.cmd("!flutter run --restart 2>/dev/null") + end, { buffer = bufnr, desc = "Flutter: Hot Restart" }) + + -- Organize imports + vim.keymap.set("n", "fo", function() + vim.lsp.buf.execute_command({ command = "dart.organizeImports", arguments = { vim.fn.expand("%:p") } }) + end, { buffer = bufnr, desc = "Dart: Organize Imports" }) + + -- Fix all auto-fixable issues + vim.keymap.set("n", "fa", function() + vim.lsp.buf.code_action({ + context = { only = { "source.fixAll" } }, + apply = true, + }) + end, { buffer = bufnr, desc = "Dart: Fix All" }) + + -- Sort members + vim.keymap.set("n", "fs", function() + vim.lsp.buf.execute_command({ command = "dart.sortMembers", arguments = { vim.fn.expand("%:p") } }) + end, { buffer = bufnr, desc = "Dart: Sort Members" }) + end + end, +}) + +-- Enable the server +vim.lsp.enable("dartls") diff --git a/lua/user/lsp_servers/kotlin_ls.lua b/lua/user/lsp_servers/kotlin_ls.lua index f332d61..534c79e 100644 --- a/lua/user/lsp_servers/kotlin_ls.lua +++ b/lua/user/lsp_servers/kotlin_ls.lua @@ -1,19 +1,83 @@ +-- Kotlin LSP = fwcd kotlin-language-server (installed via Mason). +-- +-- NOTE: the JetBrains `kotlin-lsp` (intellij-server) was tried but it imports +-- Android Gradle modules with an EMPTY set of source sets, so it resolves +-- nothing for Android (hover/definition/references all return empty). fwcd +-- resolves the Gradle compile classpath itself and DOES navigate Android +-- projects, so we use it here. (kotlin-lsp remains fine for plain Kotlin/JVM.) +-- +-- CPU/Memory notes for Android projects: +-- - Formatting disabled in LSP capabilities — ktfmt in-process spawns a SECOND +-- Kotlin compiler, doubling memory (OOMs even at 4GB). Formatting is handled +-- externally by null-ls running ktfmt as a separate process. +-- - Linting enabled — runs on the same compiler already loaded for completions, +-- so no extra memory cost. Provides error diagnostics (red squiggles). +-- - Semantic tokens disabled — extra compilation passes, not worth the CPU. +-- - 500ms debounce reduces compilation frequency during typing. +-- - Persistent tmpdir so the SQLite compilation cache survives across sessions. +local bin = vim.fn.expand("~/.local/share/nvim/mason/bin/kotlin-language-server") + +if vim.fn.executable(bin) == 0 then + vim.notify("kotlin-language-server not found — run :MasonInstall kotlin-language-server", + vim.log.levels.WARN) + return +end + +-- Build capabilities, then strip formatting to prevent ktfmt OOMs. +local capabilities = require("cmp_nvim_lsp").default_capabilities() +capabilities.textDocument.formatting = nil +capabilities.textDocument.rangeFormatting = nil +capabilities.textDocument.onTypeFormatting = nil + -- 1. Configuration vim.lsp.config('kotlin_language_server', { - cmd = { "kotlin-language-server" }, + cmd = { bin }, filetypes = { "kotlin" }, - -- Kotlin projects are usually defined by build files or git - root_markers = { "settings.gradle", "settings.gradle.kts", "pom.xml", ".git" }, - -- flags in lspconfig setup are now part of the general config table + -- Kotlin/Android projects are rooted at a Gradle build file or git repo. + -- vim.lsp.config expects callback form: function(bufnr, on_dir). + root_dir = function(bufnr, on_dir) + local fname = vim.api.nvim_buf_get_name(bufnr) + local root = vim.fs.root(fname, { + "settings.gradle.kts", + "settings.gradle", + "build.gradle.kts", + "build.gradle", + "pom.xml", + ".git", + }) + on_dir(root or vim.fs.dirname(fname)) + end, + + capabilities = capabilities, + + -- Strip formatting from the server's advertised capabilities. + -- client-side capabilities only prevent the server from expecting format + -- requests; server-side capabilities are what vim.lsp.buf.format() actually + -- checks. Without this, format-on-save routes through the LSP's ktfmt → + -- second Kotlin compiler → OOM. + on_attach = function(client) + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + end, + + -- 6GB heap — Android classpath is large (163 deps + 320 source files). + -- Persistent tmpdir so the compilation cache survives across sessions. + -- Use Android Studio's JBR 21 — optimised GC/JIT for Kotlin workloads. + cmd_env = { + KOTLIN_LANGUAGE_SERVER_OPTS = "-Xmx6G -Xms512m -Djava.io.tmpdir=/home/prabhat/.cache/kotlin-ls-fwcd", + JAVA_HOME = "/opt/android-studio/jbr", + }, + init_options = { - debounceTextChanges = 150, + debounceTextChanges = 500, }, settings = { kotlin = { linting = { enabled = true }, completion = { enabled = true }, + semanticTokens = { enabled = false }, }, }, }) diff --git a/lua/user/null-ls.lua b/lua/user/null-ls.lua index 49b72fa..eae4ce1 100644 --- a/lua/user/null-ls.lua +++ b/lua/user/null-ls.lua @@ -16,6 +16,18 @@ local sources = { extra_args = { "--aosp" }, -- optional: use AOSP style formatting --command = "google-java-format", -- specify command if not in PATH }), + -- Kotlin formatting via ktfmt — runs as separate JVM, no OOM risk. + -- Custom formatter (not in null-ls builtins for this version). + { + name = "ktfmt", + method = null_ls.methods.FORMATTING, + filetypes = { "kotlin" }, + generator = null_ls.generator({ + command = "java", + args = { "-jar", "/home/prabhat/.config/nvim/jars/ktfmt.jar", "-" }, + to_stdin = true, + }), + }, -- Add other formatters based on your needs } diff --git a/lua/user/treesitter.lua b/lua/user/treesitter.lua index 0cea3f5..25a7f57 100644 --- a/lua/user/treesitter.lua +++ b/lua/user/treesitter.lua @@ -10,5 +10,6 @@ require("nvim-treesitter").setup({ "javascript", "kotlin", "go", + "dart", }, })