Skip to content

Conversation

@tachyons
Copy link

@tachyons tachyons commented Oct 27, 2025

Add GitLab Duo Language Server configuration

Summary

This PR adds support for the GitLab Duo Language Server, enabling AI-powered code suggestions through the Language Server Protocol.

What is GitLab Duo?

GitLab Duo is GitLab's AI-powered coding assistant that provides intelligent code completion and suggestions directly in your editor. The LSP implementation allows any LSP-compatible editor to integrate with GitLab Duo.

Key Features

  • OAuth Device Flow Authentication: Secure authentication using GitLab's OAuth device flow
  • Automatic Token Refresh: Handles token expiration and refresh automatically
  • Inline Code Completion: Native support for Neovim's inline completion API (requires Neovim 0.12+)
  • Multi-language Support: Works with 25+ programming languages including Python, JavaScript, TypeScript, Go, Rust, and more
  • User Commands: Convenient commands for authentication management:
    • :LspGitLabDuoSignIn - Authenticate with GitLab
    • :LspGitLabDuoSignOut - Sign out and remove tokens
    • :LspGitLabDuoStatus - View authentication status

Prerequisites

  • Node.js and npm installed
  • GitLab account with Duo Pro license
  • Neovim 0.12+ (for inline completion support)

Current Limitations

This initial implementation only supports GitLab.com. Support for self-managed GitLab instances will be added in a future iteration. The configuration is structured to make this addition straightforward when the OAuth application setup is documented for self-managed instances.

Usage

Basic Setup

vim.lsp.enable("gitlab_duo")

Enable Inline Completion

Add this to your config to enable inline completions with Tab acceptance:

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 == 'gitlab' and vim.lsp.inline_completion then
      vim.lsp.inline_completion.enable(true, { bufnr = args.buf })
      
      -- Tab to accept
      vim.keymap.set('i', '<Tab>', function()
        if vim.lsp.inline_completion.is_visible() then
          return vim.lsp.inline_completion.accept()
        else
          return '<Tab>'
        end
      end, { expr = true, buffer = args.buf })
    end
  end
})

Authentication Flow

  1. Run :LspGitLabDuoSignIn in any buffer with the LSP attached
  2. Your browser will open to GitLab's authorization page
  3. Authorize the application
  4. The LSP will automatically authenticate and start providing suggestions

Tokens are stored locally and refreshed automatically.

Implementation Details

  • Uses npx to run the GitLab LSP package from GitLab's npm registry
  • Implements OAuth device flow for secure authentication
  • Token storage in Neovim's data directory
  • Automatic token refresh with 60-second expiration buffer
  • Error handling for token validation and feature state changes
  • Telemetry disabled by default
  • Hardcoded to gitlab.com in this iteration

Testing

Tested on:

  • Neovim v0.12+
  • Multiple file types (Lua, Python, JavaScript, TypeScript, Go, Rust)
  • Token refresh flow
  • Authentication error handling

Future Enhancements

  • Support for self-managed GitLab instances with custom URLs
  • Configurable OAuth client credentials for self-managed setups

Related Links

Screenshot 2025-11-08 at 7 55 25 PM Screenshot 2025-11-08 at 7 55 46 PM

@tachyons tachyons marked this pull request as draft October 27, 2025 07:55
@tachyons tachyons force-pushed the patch-1 branch 2 times, most recently from ba27093 to e63b9ca Compare October 27, 2025 13:40
@tachyons tachyons marked this pull request as ready for review October 27, 2025 13:43

-- Configuration
local config = {
gitlab_url = vim.env.GITLAB_URL or 'https://gitlab.com',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just mentioning: users can provide arbitrary fields using vim.lsp.config():

vim.lsp.config('gitlab_duo', {
  gitlab_duo = {
    gitlab_url = '...',
  },
})

Then in callbacks such as cmd() in this config, which are passed a config object, you can access those fields:

cmd = function(..., config)
  local foo = config.gitlab_duo
  ...
end)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmk I couldn't get this working, cmd expects a table, not a function, right ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd can be a function. Example

cmd = function(dispatchers, config)
local local_cmd = { 'lake', 'serve', '--', config.root_dir }
return vim.lsp.rpc.start(local_cmd, dispatchers)
end,

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmk Thanks, cmd does not requires gitlab_uri, it is required in the workspace/didChangeConfiguration call. I couldn't find an easy way to pass this value around, we also need to make client_id configurable for self managed GitLab instances as the client_id will also be different. So I thought doing it in a separate iteration.

-- Configuration
local config = {
gitlab_url = vim.env.GITLAB_URL or 'https://gitlab.com',
client_id = '5f1f9933c9bff0a3e908007703f260bf1ff87bcdb91d38279a9f0d0ddecceadf',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a code comment that gives a reference about where this id came from. e.g. is it a personally generated thing or part of official gitlab docs.

@justinmk
Copy link
Member

This is a lot of code which is normally discouraged in this repo, but it looks pretty good and points to some common use-cases that we need to start thinking about in the stdlib. And for supporting LSP/AI providers that require sign-in, it's good to have self-contained examples (another is copilot.lua) of what is required to complete that full process.

So I'm fine with this.

},
},
settings = {
token = get_valid_token() or '',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only happens once, at module-load time. if the token expires what then? i think settings can be modified in an on_init handler.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmk We already do that on on_int by checking the /gitlab/token/check call. We use client.notify to update this value in LSP

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then just set this to '' ? It is misleading to just sprinkle a get_valid_token() call here, which again will have side effects like network calls at module-load time. It's better to do things in one place.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmk Thanks, I dropped method call from settings

--- 2. Follow the browser prompts to authorize
--- 3. Enable inline completion in LspAttach event (see example below)
---
--- **Inline Completion Example:**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it wasn't clear but I was hoping that all cases would be looked at. Not just the ones I mentioned.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmk No, I was planning to do it with the other things including making gitlab_url and application_id configurable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmk I couldn't get configuration support working properly with config block, So I dropped support for self managed instances from this iteration. Could you review rest of the changes ?

This file contains the GitLab Duo Language Server configuration for Neovim,
including setup instructions, token management, and OAuth device flow.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants