How can I get Vim Features inside GDScript code editor ?

Features like Motion, Jumps, Registers, Global Commands, Substitution

  • I'm just getting into nvim myself so I have wondered the same.

    I think you have two options.

    1. Use a plugin to add vim features inside of Godot.
    2. Edit the scripts using vim as an external code editor.

    Option 1:
    You could try plugins like this:

    Option 2:
    Look into resources like these to help you get vim setup as an external code editor for Godot.

    Resource 1: https://github.com/habamax/vim-godot

    Resource 2:

I'm just getting into nvim myself so I have wondered the same.

I think you have two options.

  1. Use a plugin to add vim features inside of Godot.
  2. Edit the scripts using vim as an external code editor.

Option 1:
You could try plugins like this:

Option 2:
Look into resources like these to help you get vim setup as an external code editor for Godot.

Resource 1: https://github.com/habamax/vim-godot

Resource 2:

Thanks
External Editor seems the best.
Though it does not allow drag & drop to create $Node Path

a year later

I use this dap config, and lsp config. you don't need special plugins for any functionality, just a bit of trial and error.

return {
  'mfussenegger/nvim-dap',
  dependencies = {
    'rcarriga/nvim-dap-ui',
    'nvim-neotest/nvim-nio',
    'theHamsta/nvim-dap-virtual-text',
    'nvim-treesitter/nvim-treesitter',
  },
  lazy = false,
  keys = {
    {
      '<F5>',
      mode = { 'n' },
      function()
        require('dap').continue()
      end,
      desc = 'F5 dap ~ [c]ontinue',
    },
    {
      '<F10>',
      mode = { 'n' },
      function()
        require('dap').step_over()
      end,
      desc = 'dap ~ step [o]ver',
    },
    {
      '<F11>',
      mode = { 'n' },
      function()
        require('dap').step_into()
      end,
      desc = 'F11 dap ~ step into',
    },
    {
      '<S-F11>',
      mode = { 'n' },
      function()
        require('dap').step_out()
      end,
      desc = 'Shift F11 dap ~ Step [O]ut',
    },
    {
      '<Leader>b',
      mode = { 'n' },
      function()
        require('dap').toggle_breakpoint()
      end,
      desc = 'dap ~ toggle [b]reakpoint',
    },
  },
  config = function()
    local dap = require 'dap'
    local dapui = require 'dapui'

    -- Configure dapui
    dapui.setup {
      layouts = {
        {
          elements = {
            { id = 'scopes', size = 0.25 },
            'breakpoints',
            'stacks',
            'watches',
          },
          size = 40,
          position = 'left',
        },
        {
          elements = {
            'repl',
            'console',
          },
          size = 0.25,
          position = 'bottom',
        },
      },
    }

    -- Automatically open/close dapui
    dap.listeners.before.attach.dapui_config = function()
      dapui.open()
    end
    dap.listeners.before.launch.dapui_config = function()
      dapui.open()
    end
    dap.listeners.before.event_terminated.dapui_config = function()
      dapui.close()
    end
    dap.listeners.before.event_exited.dapui_config = function()
      dapui.close()
    end

    -- Helper function to find Godot project file and directory
    local function find_godot_project()
      -- Create cache to avoid repeated lookups
      if not _G.godot_project_cache then
        _G.godot_project_cache = {}
      end

      -- Start with current working directory
      local current_dir = vim.fn.getcwd()

      -- Check cache first
      if _G.godot_project_cache[current_dir] then
        return _G.godot_project_cache[current_dir].file_path, _G.godot_project_cache[current_dir].dir_path
      end

      -- Function to check if a directory contains a project.godot file
      local function has_project_file(dir)
        local project_file = dir .. '/project.godot'
        local stat = vim.uv.fs_stat(project_file)
        if stat and stat.type == 'file' then
          return project_file, dir
        else
          return nil, nil
        end
      end

      -- Check current directory first
      local project_file, project_dir = has_project_file(current_dir)
      if project_file then
        _G.godot_project_cache[current_dir] = { file_path = project_file, dir_path = project_dir }
        vim.notify('Found Godot project at: ' .. project_file, vim.log.levels.INFO)
        return project_file, project_dir
      end

      -- Search in parent directories up to a reasonable limit
      local max_depth = 5
      local dir = current_dir

      for i = 1, max_depth do
        -- Get parent directory
        local parent = vim.fn.fnamemodify(dir, ':h')

        -- Stop if we've reached the root
        if parent == dir then
          break
        end

        dir = parent

        -- Check if this directory has a project.godot file
        local project_file, project_dir = has_project_file(dir)
        if project_file then
          _G.godot_project_cache[current_dir] = { file_path = project_file, dir_path = project_dir }
          vim.notify('Found Godot project in parent directory: ' .. project_file, vim.log.levels.INFO)
          return project_file, project_dir
        end
      end

      -- Search in immediate subdirectories (first level only)
      local handle = vim.uv.fs_scandir(current_dir)
      if handle then
        while true do
          local name, type = vim.uv.fs_scandir_next(handle)
          if not name then
            break
          end

          -- Only check directories
          if type == 'directory' then
            local subdir = current_dir .. '/' .. name
            local project_file, project_dir = has_project_file(subdir)
            if project_file then
              _G.godot_project_cache[current_dir] = { file_path = project_file, dir_path = project_dir }
              vim.notify('Found Godot project in subdirectory: ' .. project_file, vim.log.levels.INFO)
              return project_file, project_dir
            end
          end
        end
      end

      -- If still not found, ask the user
      local input_dir = vim.fn.input('Godot project directory: ', current_dir, 'dir')

      -- Validate the input path
      if input_dir ~= '' then
        local project_file, project_dir = has_project_file(input_dir)
        if project_file then
          _G.godot_project_cache[current_dir] = { file_path = project_file, dir_path = project_dir }
          return project_file, project_dir
        end
      end

      vim.notify('No valid Godot project found. Using current directory.', vim.log.levels.WARN)
      return current_dir .. '/project.godot', current_dir
    end

    -- Function to debug and print the full command that will be executed
    local function debug_command(executable, args)
      local full_command = executable
      for _, arg in ipairs(args) do
        -- Properly quote arguments with spaces
        if arg:find ' ' then
          full_command = full_command .. ' "' .. arg .. '"'
        else
          full_command = full_command .. ' ' .. arg
        end
      end

      local debug_msg = 'Executing: ' .. full_command
      vim.notify(debug_msg, vim.log.levels.INFO)
      vim.notify(debug_msg)

      -- Debug environment info
      vim.notify('Current working directory: ' .. vim.fn.getcwd())
      vim.notify('HOME env: ' .. (os.getenv 'HOME' or 'not set'))
      vim.notify('DISPLAY env: ' .. (os.getenv 'DISPLAY' or 'not set'))
      vim.notify('XDG_SESSION_TYPE env: ' .. (os.getenv 'XDG_SESSION_TYPE' or 'not set'))

      return args
    end

    -- Path to the Godot executable
    local godot_executable = '/usr/lib/godot-mono/godot.linuxbsd.editor.x86_64.mono'

    -- Get important environment variables
    local function get_env_vars()
      return {
        -- Graphics-related variables (crucial for GUI apps)
        DISPLAY = os.getenv 'DISPLAY' or ':0',
        WAYLAND_DISPLAY = os.getenv 'WAYLAND_DISPLAY',
        XDG_SESSION_TYPE = os.getenv 'XDG_SESSION_TYPE',
        XAUTHORITY = os.getenv 'XAUTHORITY',

        -- Audio-related variables
        PULSE_SERVER = os.getenv 'PULSE_SERVER',

        -- User-related variables
        HOME = os.getenv 'HOME',
        USER = os.getenv 'USER',
        LOGNAME = os.getenv 'LOGNAME',

        -- Path-related variables
        PATH = os.getenv 'PATH',
        LD_LIBRARY_PATH = os.getenv 'LD_LIBRARY_PATH',

        -- Locale variables
        LANG = os.getenv 'LANG' or 'en_US.UTF-8',
        LC_ALL = os.getenv 'LC_ALL',

        -- XDG variables
        XDG_RUNTIME_DIR = os.getenv 'XDG_RUNTIME_DIR',
        XDG_DATA_HOME = os.getenv 'XDG_DATA_HOME',
        XDG_CONFIG_HOME = os.getenv 'XDG_CONFIG_HOME',

        -- Other potentially relevant variables
        SHELL = os.getenv 'SHELL',
        TERM = os.getenv 'TERM',
        DBUS_SESSION_BUS_ADDRESS = os.getenv 'DBUS_SESSION_BUS_ADDRESS',
      }
    end

    -- Standard GDScript adapter for non-C# projects
    dap.adapters.godot = {
      type = 'server',
      host = '127.0.0.1',
      port = 6006,
    }

    dap.configurations.gdscript = {
      {
        type = 'godot',
        request = 'launch',
        name = 'Launch Scene',
        project = '${workspaceFolder}',
        launch_scene = true,
      },
    }

    -- Direct launch approach using netcoredbg to start Godot-Mono
    dap.adapters.coreclr = {
      type = 'executable',
      command = '/home/jamie/.local/share/nvim/mason/bin/netcoredbg',
      args = {
        '--interpreter=vscode',
        '--',
        godot_executable,
      },
    }

    dap.configurations.cs = {
      -- Launch Godot editor with project - simple approach
      {
        type = 'coreclr',
        request = 'launch',
        name = 'Simple Editor Launch',
        cwd = function()
			local project_file,project_dir = find_godot_project()
			vim.notify("cwd " .. project_dir)
			return project_dir
        end,
        env = get_env_vars(), -- Pass environment variables
        args = function()
					local project_file,project_dir = find_godot_project()
					return {" --editor " .. project_file}
				end,
      },
    }

    -- Add visual indicators for breakpoints
    vim.fn.sign_define('DapBreakpoint', { text = '●', texthl = 'DapBreakpoint', linehl = '', numhl = '' })
    vim.fn.sign_define('DapBreakpointCondition', { text = '◆', texthl = 'DapBreakpointCondition', linehl = '', numhl = '' })
    vim.fn.sign_define('DapLogPoint', { text = '◆', texthl = 'DapLogPoint', linehl = '', numhl = '' })
    vim.fn.sign_define('DapStopped', { text = '→', texthl = 'DapStopped', linehl = 'DapStopped', numhl = 'DapStopped' })
    vim.fn.sign_define('DapBreakpointRejected', { text = '●', texthl = 'DapBreakpointRejected', linehl = '', numhl = '' })
  end,
}
local keymap = require 'keymaps'

return {
  {
    'williamboman/mason.nvim',
    priority = 100, -- Make sure mason loads first
    config = function()
      local mason_ok, mason = pcall(require, 'mason')
      if not mason_ok then
        print 'Failed to load mason'
        return
      end
      mason.setup {
        check_outdated_packages_on_open = true,
        border = 'none',
        log_level = vim.log.levels.DEBUG,
        ui = {
          icons = {
            package_installed = '✓',
            package_pending = '➜',
            package_uninstalled = '✗',
          },
        },
        registries = {
          'github:nvim-java/mason-registry',
          'github:mason-org/mason-registry',
        },
        install = {
          connection_timeout = 3600,
        },
        max_concurrent_installers = 2,
      }
    end,
  },
  {
    'neovim/nvim-lspconfig',
    dependencies = {
      'williamboman/mason-lspconfig.nvim',
      'WhoIsSethDaniel/mason-tool-installer.nvim',
      'nvim-java/nvim-java',
      'Hoffs/omnisharp-extended-lsp.nvim',
      'kevinhwang91/nvim-ufo',
      'kevinhwang91/promise-async',
      'folke/lazydev.nvim',
      'Bilal2453/luvit-meta',
      'hrsh7th/nvim-cmp',
    },

    config = function()
      local capabilities = vim.lsp.protocol.make_client_capabilities()
      capabilities.textDocument.foldingRange = {
        dynamicRegistration = false,
        lineFoldingOnly = true,
      }
      capabilities = vim.tbl_deep_extend('force', capabilities, require('cmp_nvim_lsp').default_capabilities())

      -- Set up enhanced hover UI
      local hover_config = {
        border = 'rounded',
        max_width = 80,
        max_height = 30,
      }

      -- Configure the LSP handlers for better UI
      vim.lsp.handlers['textDocument/hover'] = vim.lsp.with(vim.lsp.handlers.hover, hover_config)

      vim.lsp.handlers['textDocument/signatureHelp'] = vim.lsp.with(vim.lsp.handlers.signature_help, hover_config)

      -- Common on_attach function to use with all LSP servers
      local on_attach = function(client, bufnr)
        -- Set updatetime for faster hover/signature response
        vim.opt.updatetime = 300

        -- Keep existing functionality but prefer lspsaga if available
        local has_saga, _ = pcall(require, 'lspsaga')

        -- Regular LSP keymaps as fallback
        if not has_saga then
          keymap.buf_map(bufnr, 'n', '<leader>la', vim.lsp.buf.code_action, { desc = 'LSP code actions' })
          keymap.buf_map(bufnr, 'n', '<leader>lh', vim.lsp.buf.hover, { desc = 'Show hover documentation' })
          keymap.buf_map(bufnr, 'n', '<leader>ln', vim.lsp.buf.rename, { desc = 'Rename symbol' })
          keymap.buf_map(bufnr, 'n', '<leader>ld', vim.lsp.buf.definition, { desc = 'Go to definition' })
          keymap.buf_map(bufnr, 'n', '<leader>lD', vim.lsp.buf.declaration, { desc = 'Go to declaration' })
          keymap.buf_map(bufnr, 'n', '<leader>li', vim.lsp.buf.implementation, { desc = 'Go to implementation' })
          keymap.buf_map(bufnr, 'n', '<leader>lr', vim.lsp.buf.references, { desc = 'Find references' })
          keymap.buf_map(bufnr, 'n', '<leader>lt', vim.lsp.buf.type_definition, { desc = 'Go to type definition' })
          keymap.buf_map(bufnr, 'i', '<C-k>', vim.lsp.buf.signature_help, { desc = 'Show signature help' })
          keymap.buf_map(bufnr, 'n', '<leader>lf', function()
            vim.lsp.buf.format { timeout_ms = 2000 }
          end, { desc = 'Format code' })
        else
          -- Keep format command using LSP directly since lspsaga doesn't provide this
          keymap.buf_map(bufnr, 'n', '<leader>lf', function()
            vim.lsp.buf.format { timeout_ms = 2000 }
          end, { desc = 'Format code' })
        end

      end

      -- GDScript setup
      require('lspconfig').gdscript.setup {
        cmd = { 'nc', 'localhost', '6005' },
        filetypes = { 'gdscript', 'gd' },
        root_dir = require('lspconfig').util.root_pattern('project.godot', '.git'),
        capabilities = capabilities,
        on_attach = function(client, bufnr)
          client.server_capabilities.documentFormattingProvider = false
          client.server_capabilities.documentRangeFormattingProvider = false

          vim.bo[bufnr].expandtab = true
          vim.bo[bufnr].shiftwidth = 4
          vim.bo[bufnr].tabstop = 4
          vim.bo[bufnr].softtabstop = 4

          -- Call common on_attach for standard LSP features
          on_attach(client, bufnr)
        end,
      }

      -- Lua LSP setup
      require('lspconfig').lua_ls.setup {
        settings = {
          Lua = {
            completion = { callSnippet = 'Replace' },
          },
        },
        capabilities = capabilities,
        on_attach = on_attach,
      }

      -- OmniSharp setup with enhanced capabilities for C#
      local pid = vim.fn.getpid()
      require('lspconfig').omnisharp.setup {
        cmd = {
          '/home/jamie/.local/share/nvim/mason/bin/omnisharp',
          '--languageserver',
          '--hostPID',
          tostring(pid),
        },
        handlers = {
          ['textDocument/definition'] = require('omnisharp_extended').handler,
          ['textDocument/implementation'] = require('omnisharp_extended').handler,
        },
        capabilities = capabilities,
        on_attach = function(client, bufnr)
          -- Call common LSP setup first
          on_attach(client, bufnr)

          -- OmniSharp specific settings
          client.server_capabilities.documentFormattingProvider = true
          client.server_capabilities.hoverProvider = true
          client.server_capabilities.documentHighlightProvider = true

          -- Use extended OmniSharp for improved functionality
          keymap.buf_map(bufnr, 'n', 'gd', "<cmd>lua require('omnisharp_extended').lsp_definition()<cr>")
          keymap.buf_map(bufnr, 'n', '<leader>D', "<cmd>lua require('omnisharp_extended').lsp_type_definition()<cr>")
          keymap.buf_map(bufnr, 'n', 'gr', "<cmd>lua require('omnisharp_extended').lsp_references()<cr>")
          keymap.buf_map(bufnr, 'n', 'gi', "<cmd>lua require('omnisharp_extended').lsp_implementation()<cr>")
          -- C# specific settings
          vim.bo[bufnr].expandtab = true
          vim.bo[bufnr].shiftwidth = 4
          vim.bo[bufnr].tabstop = 4
        end,
      }
    end,
  },
}

    budtard Thank you! I can confirm this worked on my setup (Godot-Mono, Arch-based Linux, Lazyvim) after everything else I tried failed.