Skip to content

Conversation

@tachyons
Copy link
Contributor

@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 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
Contributor 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
Contributor 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.

Copy link
Member

Choose a reason for hiding this comment

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

I couldn't find an easy way to pass this value around

You can set arbitrary properties on the client or config.

-- 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.

This file contains the GitLab Duo Language Server configuration for Neovim,
including setup instructions, token management, and OAuth device flow.
Comment on lines +55 to +56
-- This is a oauth application created from tachyons-gitlab account with `api` scope
client_id = '00bb391f527d2e77b3467b0b6b900151cc6a28dcfb18fa1249871e43bc3e5832',
Copy link
Member

Choose a reason for hiding this comment

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

That seems kind of strange. Do users need to generate their own client_id? Is this config dependent on your account?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@justinmk There is no need to create a client_id per user. Alternatively one of the maintainer of this repo can create a GitLab application and add the client_id here.

Copy link
Member

@justinmk justinmk Nov 14, 2025

Choose a reason for hiding this comment

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

Makes no difference to me, just trying to understand, and ensure that we document stuff and can answer questions in the future.

@justinmk justinmk merged commit adbfc92 into neovim:master Nov 14, 2025
6 checks passed
Comment on lines +22 to +29
--- vim.api.nvim_create_autocmd('LspAttach', {
--- callback = function(args)
--- local bufnr = args.buf
--- local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
---
--- if vim.lsp.inline_completion and
--- client:supports_method(vim.lsp.protocol.Methods.textDocument_inlineCompletion, bufnr) then
--- vim.lsp.inline_completion.enable(true, { bufnr = bufnr })
Copy link
Member

@justinmk justinmk Nov 14, 2025

Choose a reason for hiding this comment

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

I should have asked this for Copilot too, but ... why don't we just do this when this config is enabled? Why should the user need to manually add this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@justinmk I used copilot.lua as the reference implication, that is why I followed the same pattern

Copy link
Member

Choose a reason for hiding this comment

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

I think we should change both imo

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