Skip to content

Conversation

@tachyons
Copy link

@tachyons tachyons commented Oct 27, 2025

Add GitLab Duo Language Server Configuration

Summary

This PR adds native support for the GitLab Duo Language Server to the Neovim LSP configuration collection, enabling AI-powered code suggestions from GitLab Duo directly in Neovim.

Motivation

GitLab Duo provides AI-assisted code completion similar to GitHub Copilot, but requires a specific LSP setup with OAuth device flow authentication. This configuration makes it easy for Neovim users to integrate GitLab Duo into their workflow without manual LSP setup.

Features

🔐 OAuth Device Flow Authentication

  • Full implementation of GitLab's OAuth device flow
  • Automatic token refresh before expiration
  • Secure token storage in Neovim's data directory
  • No manual PAT management required

🔄 Automatic Token Management

  • Detects expired tokens and refreshes automatically
  • Handles token validation errors from the server
  • Provides clear status messages and error handling

🎮 User Commands

  • :LspGitLabDuoSignIn - Start OAuth authentication flow
  • :LspGitLabDuoSignOut - Sign out and remove stored token
  • :LspGitLabDuoStatus - View authentication and token status

⌨️ Standard Keybindings

  • Tab - Accept suggestion
  • Alt/Option+[ - Previous suggestion
  • Alt/Option+] - Next suggestion

Implementation Details

Authentication Flow

  1. User runs :LspGitLabDuoSignIn
  2. Device code is copied to clipboard automatically
  3. Browser opens to GitLab verification URL
  4. User authorizes the application
  5. Token is automatically retrieved and stored
  6. LSP configuration is updated with the new token

Token Lifecycle

  • Access tokens are stored with refresh tokens and expiration times
  • Tokens are automatically refreshed 60 seconds before expiry
  • Failed refresh attempts prompt re-authentication
  • Token validation errors trigger automatic refresh attempts

LSP Handlers

  • $/gitlab/token/check - Handles token validation errors
  • $/gitlab/featureStateChange - Notifies about feature availability
  • Workspace configuration updates for token changes

Dependencies

Required:

Runtime:

  • Node.js and npm (for GitLab LSP server)
  • GitLab account with Duo Pro license
  • Internet connection for OAuth flow

Installation & Usage

Basic Setup

-- Enable the LSP
vim.lsp.enable('gitlab_duo')

-- Setup keybindings for inline completion
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local client = vim.lsp.get_client_by_id(args.data.client_id)
    local bufnr = args.buf
    
    if client.name == 'gitlab_duo' and 
        vim.lsp.inline_completion  and
       client:supports_method(vim.lsp.protocol.Methods.textDocument_inlineCompletion, bufnr) then
      vim.lsp.inline_completion.enable(true, { bufnr = bufnr })
      
      -- Tab to accept suggestion
      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 = bufnr })
      
      -- Navigate suggestions
      vim.keymap.set('i', '<M-[>', vim.lsp.inline_completion.select_prev, { buffer = bufnr })
      vim.keymap.set('i', '<M-]>', vim.lsp.inline_completion.select_next, { buffer = bufnr })
    end
  end
})

First-Time Authentication

:GitLabDuoSignIn

Follow the browser prompts to authorize the application.

Testing

Manual Testing Steps

  1. Install dependencies:

    -- With lazy.nvim
    { 'nvim-lua/plenary.nvim' }
  2. Load the configuration:

    vim.lsp.enable('gitlab_duo')
  3. Authenticate:

    :LspGitLabDuoSignIn
  4. Test suggestions:

    • Open a supported file (.lua, .py, .js, etc.)
    • Start typing code
    • Verify suggestions appear
    • Test Tab to accept
    • Test Alt+[ and Alt+] to cycle
  5. Test token management:

    :LspGitLabDuoStatus    " View authentication status
    :LspGitLabDuoSignOut   " Sign out
    :LspGitLabDuoSignIn    " Sign back in

Expected Behavior

  • ✅ OAuth flow completes successfully
  • ✅ Code suggestions appear while typing
  • ✅ Token refreshes automatically before expiry
  • ✅ Error messages are clear and actionable
  • ✅ Commands work in all supported file types

Configuration Options

Users can customize the GitLab instance URL via environment variable:

export GITLAB_URL="https://gitlab.example.com"

Default is https://gitlab.com.

Supported Languages

Ruby, Go, JavaScript, TypeScript, React, Rust, Lua, Python, Java, C, C++, PHP, C#, Kotlin, Swift, Scala, Vue, Svelte, HTML, CSS, SCSS, JSON, YAML

Documentation

Checklist

  • OAuth device flow implementation
  • Automatic token refresh
  • Error handling and user notifications
  • User commands for auth management
  • LSP handlers for GitLab-specific messages
  • Documentation and usage examples
  • Support for self-hosted GitLab instances

Screenshots

Screenshot 2025-10-27 at 1 17 41 PM Screenshot 2025-10-27 at 1 17 59 PM

Note: This configuration requires Neovim 0.12+ for native inline completion support.

@tachyons tachyons marked this pull request as draft October 27, 2025 07:55
This file contains the GitLab Duo Language Server configuration for Neovim,
including setup instructions, token management, and OAuth device flow.
@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,

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

Co-authored-by: Justin M. Keyes <[email protected]>
},
},
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.

Co-authored-by: Justin M. Keyes <[email protected]>
--- 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.

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.

3 participants