Post

Bonus: opencode.nvim

Bonus: opencode.nvim

Disclaimer: Dieser Artikel ist keine offizielle Anleitung der Informatikdienste. Vorausgesetzt werden grundlegende Kenntnisse im Bereich KI und idealerweise auch KI-Agenten. Ich empfehle, KI-Tools nur für Aufgaben einzusetzen, die man sich auch selbst zutrauen würde.

Only let the AI do what you would feel comfortable doing yourself!

Wiederholung: Opencode ist ein Open-Source-Coding-Agent. Er ist als terminalbasiertes Interface, Desktop-App oder IDE-Erweiterung verfügbar – ich verwende hier nur das Terminal-Interface. Die Idee ist simpel: Du gibst dem Agenten eine Aufgabe, er liest deinen Code, schlägt Änderungen vor, führt Befehle aus – und du schaust dabei zu (oder greifst ein, wenn er mal wieder etwas zu kreativ wird).


Bisher haben wir Opencode als Container in Docker direkt aus der Shell ausgeführt. Für alle, die ihren Arbeitsalltag im Terminal verbringen und Neovim als einzig wahren Editor anerkennen (ihr wisst, wer ihr seid) gibt es jetzt noch ein Extra: Es gibt das Plugin opencode.nvim, das opencode direkt in Neovim integriert. Ich installiere es zunächst in seiner Standard-Konfiguration mit lazy.

opencode mit Port starten

Beim genaueren Lesen des READMEs fällt auf: opencode muss mit der --port-Option gestartet werden. Genauer gesagt brauchen wir auch noch --hostname 0.0.0.0, da der Container sonst nicht auf Anfragen von aussen reagiert. Der Alias wird entsprechend erweitert:

1
2
3
4
5
6
7
alias occ='docker run --rm -it \
  -p 4096:4096 \
  --env-file "$HOME/.opencode.env" \
  -v "$HOME/.config/opencode/opencode.json:/home/coder/.config/opencode/opencode.json:ro" \
  -v "$(pwd):/workspace:rw" \
  ghcr.io/grvlbit/opencode-container:latest opencode --port 4096 --hostname 0.0.0.0'

Plugin-Konfiguration: Server URL

Damit Neovim eine laufende opencode-Instanz findet, setzen wir die Server-URL in der Plugin-Konfiguration explizit. Mit dieser Konfiguration verbindet sich opencode.nvim mit einer bereits gestarteten opencode-Instanz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "nickjvandyke/opencode.nvim",
  version = "*",
  config = function()
    ---@type opencode.Opts
    vim.g.opencode_opts = {
      server = {
        url = "http://localhost:4096",
      },
    }
    vim.o.autoread = true

    vim.keymap.set({ "n", "x" }, "<leader>oa", function() require("opencode").ask("@this: ") end,           { desc = "Ask opencode…" })
    vim.keymap.set({ "n", "x" }, "<leader>os", function() require("opencode").select() end,                  { desc = "Select opencode…" })
    vim.keymap.set({ "n", "x" }, "go",  function() return require("opencode").operator("@this ") end,        { desc = "Add range to opencode", expr = true })
    vim.keymap.set("n",          "goo", function() return require("opencode").operator("@this ") .. "_" end, { desc = "Add line to opencode", expr = true })
    vim.keymap.set("n", "<leader>ou", function() require("opencode").command("session.half.page.up") end,   { desc = "Scroll opencode up" })
    vim.keymap.set("n", "<leader>od", function() require("opencode").command("session.half.page.down") end, { desc = "Scroll opencode down" })
  end,
},

Das Pfad-Problem

Wenn wir mit goo im Normal-Mode eine Zeile zum opencode-Kontext hinzufügen, passiert das standardmässig mit dem vollen Pfad auf dem Host-System. Diesen kennt opencode im Container jedoch nicht – die Dateien sind dort unter /workspace eingebunden. Wir brauchen eine kleine Hilfsfunktion, die den absoluten Host-Pfad durch den Container-Pfad ersetzt:

1
2
3
4
5
local function remap_path(value)
  if not value then return nil end
  return (value:gsub(vim.pesc(vim.fn.getcwd()), "/workspace"))
end

Diese Funktion wird dann für alle Kontext-Funktionen verwendet:

1
2
3
4
5
6
7
8
9
10
11
contexts = {
  ["@this"]        = function(ctx) return remap_path(require("opencode.context.builtins").this(ctx)) end,
  ["@buffer"]      = function(ctx) return remap_path(require("opencode.context.builtins").buffer(ctx)) end,
  ["@buffers"]     = function(_)   return remap_path(require("opencode.context.builtins").buffers()) end,
  ["@visible"]     = function(_)   return remap_path(require("opencode.context.builtins").visible_text()) end,
  ["@diagnostics"] = function(ctx) return remap_path(require("opencode.context.builtins").diagnostics(ctx)) end,
  ["@quickfix"]    = function(_)   return remap_path(require("opencode.context.builtins").quickfix()) end,
  ["@diff"]        = function(_)   return require("opencode.context.builtins").git_diff() end,
  ["@marks"]       = function(_)   return remap_path(require("opencode.context.builtins").marks()) end,
},

Fertig? Fast.

Falls der opencode-Container beim Start von Neovim noch nicht läuft, erscheint folgende Fehlermeldung im Terminal:

1
2
zsh: command not found: opencode

Standardmässig erwartet opencode.nvim, dass opencode direkt auf dem Host als Executable installiert ist. Von unserem Container weiss das Plugin noch nichts. Das lässt sich über die start-Option in der Plugin-Konfiguration lösen – wir sagen dem Plugin einfach selbst, wie es opencode starten soll:

1
2
3
4
5
6
7
8
9
start = function()
  local win = require('snacks.terminal').get(opencode_cmd, { create = false })
  if win then
    win:show()
  else
    require('snacks.terminal').open(opencode_cmd, snacks_terminal_opts)
  end
end,

Vollständige opencode.nvim Konfiguration

Hier nochmals die komplette Konfiguration auf einen Blick:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
{
  "nickjvandyke/opencode.nvim",
  version = "*",
  config = function()
    -- Host-Pfad → Container-Pfad
    local function remap_path(value)
      if not value then return nil end
      return (value:gsub(vim.pesc(vim.fn.getcwd()), "/workspace"))
    end

    ---@type snacks.terminal.Opts
    local snacks_terminal_opts = {
      win = {
        position = 'right',
        enter = false,
      },
    }

    local opencode_cmd = "zsh -i -c 'occ'"

    ---@type opencode.Opts
    vim.g.opencode_opts = {
      server = {
        -- occ startet opencode im Docker-Container mit Port 4096
        url = "http://localhost:4096",
        start = function()
          local win = require('snacks.terminal').get(opencode_cmd, { create = false })
          if win then
            win:show()
          else
            require('snacks.terminal').open(opencode_cmd, snacks_terminal_opts)
          end
        end,
      },
      contexts = {
        ["@this"]        = function(ctx) return remap_path(require("opencode.context.builtins").this(ctx)) end,
        ["@buffer"]      = function(ctx) return remap_path(require("opencode.context.builtins").buffer(ctx)) end,
        ["@buffers"]     = function(_)   return remap_path(require("opencode.context.builtins").buffers()) end,
        ["@visible"]     = function(_)   return remap_path(require("opencode.context.builtins").visible_text()) end,
        ["@diagnostics"] = function(ctx) return remap_path(require("opencode.context.builtins").diagnostics(ctx)) end,
        ["@quickfix"]    = function(_)   return remap_path(require("opencode.context.builtins").quickfix()) end,
        ["@diff"]        = function(_)   return require("opencode.context.builtins").git_diff() end,
        ["@marks"]       = function(_)   return remap_path(require("opencode.context.builtins").marks()) end,
      },
    }

    vim.o.autoread = true

    vim.keymap.set({ "n", "x" }, "<leader>oa", function() require("opencode").ask("@this: ") end,           { desc = "Ask opencode…" })
    vim.keymap.set({ "n", "x" }, "<leader>os", function() require("opencode").select() end,                  { desc = "Select opencode…" })
    vim.keymap.set({ "n", "x" }, "go",  function() return require("opencode").operator("@this ") end,        { desc = "Add range to opencode", expr = true })
    vim.keymap.set("n",          "goo", function() return require("opencode").operator("@this ") .. "_" end, { desc = "Add line to opencode", expr = true })
    vim.keymap.set("n", "<leader>ou", function() require("opencode").command("session.half.page.up") end,   { desc = "Scroll opencode up" })
    vim.keymap.set("n", "<leader>od", function() require("opencode").command("session.half.page.down") end, { desc = "Scroll opencode down" })
  end,
},

Hinweis: Ich verwende snacks.nvim für das Terminalfenster. Anpassungen für andere Terminal-Plugins sollten sich in Grenzen halten.

This post is licensed under CC BY 4.0 by the author.