Building a Modern VIM Configuration from Scratch – Part 3: IDE Intelligence with LSP

In Part 1, we built a solid foundation. In Part 2, we added essential plugins.

Now we’re adding the final piece: IDE-level intelligence.

By the end of this article, you’ll have auto-completion, go-to-definition, real-time error checking, and code actions, all inside VIM. The same features that make VS Code popular, running in your terminal on z/OS.

What is LSP?

LSP (Language Server Protocol) is a game-changer. Instead of every editor implementing language support from scratch, LSP defines a standard protocol. One language server works with any editor that speaks LSP.

What this means for you:

  • Install one language server (like clangd for C/C++)
  • Get intelligent features in VIM, VS Code, Emacs, or any LSP-capable editor
  • Same quality as IDE plugins, but in your terminal

LSP features we’ll enable:

  • Intelligent auto-completion (not just text matching)
  • Go-to-definition (jump to where functions/types are defined)
  • Find references (where is this used?)
  • Hover documentation (see function signatures)
  • Real-time diagnostics (errors and warnings as you type)
  • Code actions (quick fixes, refactoring)
  • Rename symbol (across entire project)

Why CoC?

There are several ways to get LSP in VIM:

  • vim-lsp – Pure VimScript, lightweight
  • ALE – Asynchronous Lint Engine, good for linting
  • Native LSP – Built into Neovim and VIM 9+
  • CoC (Conquer of Completion) – Feature-rich, VS Code-like experience

I chose CoC because:

  • Most complete LSP implementation for VIM
  • Excellent out-of-the-box experience
  • Large ecosystem of extensions
  • Works reliably on z/OS
  • Active development and community

The trade-off: CoC requires Node.js. But if you’re doing modern development, you probably have Node.js anyway.

Prerequisites

You’ll need:

  • The configuration from Part 1 and Part 2
  • Node.js (v14.14 or higher)
  • VIM 8.1+

Checking Node.js

On z/OS, Node.js is typically installed at /usr/lpp/IBM/cnj. Check if it’s available:

node --version
# Should show v14.14 or higher (I have v20.19.2)

If node is not found, add it to your PATH in ~/.bashrc:

# Add Node.js to PATH (adjust path if different on your system)
export PATH="/usr/lpp/IBM/cnj/IBM/node-latest-os390-s390x/bin:$PATH"

Then reload: source ~/.bashrc

Checking VIM features

vim --version | grep -E '\+timers|\+job'
# Should show +timers and +job (not -timers or -job)

If you installed VIM via zopen (as in our Unix setup article), you should be fine.

Installing CoC

Add CoC to your plugin list in ~/.vimrc:

call plug#begin('~/.vim/plugged')
  " ... your existing plugins from Part 2 ...

  " LSP Support
  Plug 'neoclide/coc.nvim', {'branch': 'release'}
call plug#end()

Install the plugin:

# In VIM:
:PlugInstall

Wait for it to complete. CoC downloads its dependencies automatically.

Verifying the installation

Restart VIM and run:

:CocInfo

You should see something like:

vim version: 9.1
node version: v20.19.2
coc.nvim version: 0.0.82
platform: os390

If you see errors about Node.js, double-check your PATH settings.

CoC configuration

CoC needs keybindings to be useful. Add this configuration block to your ~/.vimrc after the plugin section:

Important: These mappings must come after your let mapleader = " " line!

" ============================================
" CoC mappings
" ============================================
" Tab completion navigation
inoremap <silent><expr> <TAB>
      \ coc#pum#visible() ? coc#pum#next(1) :
      \ CheckBackspace() ? "\<Tab>" :
      \ coc#refresh()
inoremap <expr><S-TAB> coc#pum#visible() ? coc#pum#prev(1) : "\<C-h>"

" Enter confirms completion
inoremap <silent><expr> <CR> coc#pum#visible() ? coc#pum#confirm()
                              \: "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>"

function! CheckBackspace() abort
  let col = col('.') - 1
  return !col || getline('.')[col - 1]  =~# '\s'
endfunction

" Hover documentation with K
nnoremap <silent> K :call ShowDocumentation()<CR>

function! ShowDocumentation()
  if CocAction('hasProvider', 'hover')
    call CocActionAsync('doHover')
  else
    call feedkeys('K', 'in')
  endif
endfunction

" Go-to Navigation
nnoremap <silent> gd <Plug>(coc-definition)
nnoremap <silent> gy <Plug>(coc-type-definition)
nnoremap <silent> gi <Plug>(coc-implementation)
nnoremap <silent> gr <Plug>(coc-references)

" Diagnostics Navigation
nnoremap <silent> <leader>dn <Plug>(coc-diagnostic-next)
nnoremap <silent> <leader>dp <Plug>(coc-diagnostic-prev)

" Code Actions & Fixes
nnoremap <silent> <leader>qf <Plug>(coc-fix-current)
nnoremap <silent> <leader>ac <Plug>(coc-codeaction)

" Rename Symbol
nnoremap <silent> <leader>rn <Plug>(coc-rename)

" Format
nnoremap <silent> <leader>fm <Plug>(coc-format)
xnoremap <leader>fm <Plug>(coc-format-selected)

" Highlight symbol under cursor
autocmd CursorHold * silent call CocActionAsync('highlight')

Let me explain what each section does:

Tab completion: When the completion menu is visible, Tab/Shift-Tab navigate through options. Otherwise, Tab inserts a tab character.

Enter confirms: Press Enter to accept the selected completion.

Hover documentation (K): Press K on any symbol to see its documentation/signature in a floating window.

Go-to navigation:

  • gd – Go to definition
  • gy – Go to type definition
  • gi – Go to implementation
  • gr – Find all references

Diagnostics navigation:

  • Space dn – Jump to next error/warning
  • Space dp – Jump to previous error/warning

Code actions:

  • Space qf – Apply quick fix for current line
  • Space ac – Show all available code actions
  • Space rn – Rename symbol under cursor
  • Space fm – Format code

A note on keybinding choices

You might wonder why I use <leader>dn instead of the common [g and ]g for diagnostic navigation.

On a German keyboard, [ and ] require AltGr+8/9 – impractical for frequent use. I chose <leader>dn (diagnostic next) and <leader>dp (diagnostic previous) because they’re easier to type and more memorable.

The point: Customize keybindings for YOUR keyboard and workflow. Don’t blindly copy configurations, understand them and make them yours.

CoC settings file

CoC has its own configuration file at ~/.vim/coc-settings.json. Create it with some sensible defaults:

{
  "suggest.noselect": true,
  "diagnostic.errorSign": "✘",
  "diagnostic.warningSign": "⚠",
  "diagnostic.infoSign": "ℹ",
  "diagnostic.hintSign": "➤",
  "coc.preferences.enableFloatHighlight": false,
  "suggest.floatConfig": {
    "border": true
  },
  "hover.floatConfig": {
    "border": true
  }
}

What these do:

  • suggest.noselect – Don’t auto-select first completion item
  • diagnostic.*Sign – Custom symbols in the sign column
  • the rest is for appearance’s sake

You can also edit this file from within VIM with :CocConfig.

Language server support

CoC itself is just the client. You need language servers for actual intelligence. CoC has extensions for many languages:

  • coc-clangd – C/C++ (our focus) – needs clangd as LSP server
  • coc-json – JSON – brings it’s own LSP server
  • coc-pyright – Python – needs pyright as LSP server

Install extensions with :CocInstall:

:CocInstall coc-clangd coc-json

Check installed extensions:

:CocList extensions

C/C++ support with clangd

For C and C++ development, clangd is the gold standard. It’s part of the LLVM project and provides excellent code intelligence.

A personal note on z/OS tooling

Getting modern development tools on z/OS has been a journey. I’ve been working on porting several LLVM-based tools to z/OS through the zopen community:

  • clang-format – Code formatting
  • clang-tidy – Static analysis
  • clangd – LSP server for C/C++
  • 🚧 HLASM LSP – High Level Assembler support (work in progress)

These ports make modern development workflows possible on z/OS. If you’re interested in the HLASM LSP, check out the Eclipse Che4z project.

Installing clangd via zopen

# Install clangd and related tools
zopen install -y clangd clang-tidy clang-format

# Verify installation
clangd --version
# Should show: clangd version 21.x.x or similar

The coc-clangd extension should automatically detect clangd in your PATH.

Current limitations on z/OS

Important: clangd on z/OS is functional but not yet fully optimized for the platform. You may encounter false positives – warnings or errors for code that compiles correctly.

Common issues:

  • _Packed keyword not recognized (common z/OS extension)
  • Some IBM-specific builtin types may show errors
  • Platform-specific headers might not be fully understood

The good news: Core functionality works well. Completion, go-to-definition, and most diagnostics are accurate. The false positives are annoying but don’t prevent productive use.

This is an area where community contributions could help improve z/OS support.

Project configuration with compile_commands.json

For clangd to fully understand your project, it needs to know how files are compiled. The standard way is a compile_commands.json file in your project root.
Basic IDE features will work out of the box.

With CMake

If you use CMake, this is easy:

# In your build directory:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..

# Create symlink in project root:
ln -s build/compile_commands.json .

CMake generates the file automatically with all compiler flags.

Manual creation

For smaller projects or non-CMake builds, create it manually:

[
  {
    "directory": "/path/to/project",
    "file": "main.c",
    "arguments": ["xlc", "-c", "-qascii", "-D_OPEN_SYS", "main.c"]
  },
  {
    "directory": "/path/to/project",
    "file": "utils.c",
    "arguments": ["xlc", "-c", "-qascii", "-D_OPEN_SYS", "utils.c"]
  }
]

Key points:

  • directory – Absolute path to compilation directory
  • file – Source file (relative to directory)
  • arguments – Full compiler command as array

Include all flags you use: -D defines, -I include paths, etc. This helps clangd understand your code correctly.

Testing the setup

Let’s verify everything works. Create a test file:

// test.c
#include <stdio.h>
#include <stdlib.h>                    // Warning: unused header -> quick fix available
#include <string.h>

// ============================================
// Test: Hover (K) - hover over types and functions
// ============================================

typedef struct {
    char name[50];
    int age;
    float salary;
} Employee;

// ============================================
// Test: Go-to-Definition (gd) / References (gr)
// ============================================

void print_employee(Employee *emp) {
    printf("Name: %s, Age: %d, Salary: %.2f\n",
           emp->name, emp->age, emp->salary);
}

int calculate_bonus(int base, float multiplier) {
    return (int)(base * multiplier);   // Warning: narrowing conversion
}

// ============================================
// Test: Diagnostics (Space dn / Space dp)
// ============================================

void function_with_errors() {
    int x = "this is wrong";           // Error: incompatible types
    int y = 10
    int z = undefined_var;             // Error: undeclared identifier
    printf("%d\n", x, y, z);           // Warning: too many arguments
}

// ============================================
// Test: Rename (Space rn)
// Try renaming 'counter' to something else
// ============================================

void counting_function() {
    int counter = 0;
    counter++;
    counter += 5;
    printf("Counter: %d\n", counter);
    counter = counter * 2;
}

// ============================================
// Test: Format (Space fm)
// This code is badly formatted
// ============================================

void unformatted_mess(){int a=1;int b=2;int c=3;
if(a==1){printf("one");} else {printf("not one");}
for(int i=0;i<10;i++){printf("%d",i);}}

// ============================================
// Test: Code Actions (Space ac)
// ============================================

int main() {
    Employee emp1;
    strcpy(emp1.name, "Mike");
    emp1.age = 30;
    emp1.salary = 50000.0;             // Warning: narrowing conversion

    // Test gd: jump to print_employee definition
    print_employee(&emp1);

    // Test gr: find all references to calculate_bonus
    int bonus = calculate_bonus(1000, 1.5);
    int bonus2 = calculate_bonus(2000, 1.2);

    printf("Bonus: %d, %d\n", bonus, bonus2);

    return 0;
}

Open it in VIM and test all your new stuff.

If all these work, congratulations! You have a working LSP setup.

Troubleshooting

CoC not starting

:CocInfo
# Check for errors

:messages
# See VIM messages for clues

Most common cause: Node.js not found. Verify node --version works in your shell.

No completions appearing

  • Check if clangd is running: :CocCommand clangd.showMemoryUsage
  • Verify clangd path: which clangd
  • Check for compile_commands.json in project root

Keybindings not working

Common causes:

  • CoC mappings placed before let mapleader, place them at the end of your other keybindings

Too many false errors

If clangd shows errors for valid z/OS code:

  • Add missing defines to compile_commands.json
  • Create a .clangd file in project root with additional flags
  • Accept that some z/OS-specific constructs won’t be understood (yet)

Complete keybinding reference

Here’s everything we’ve set up across all three parts:

Part 1 – Base:

  • Space / – Clear search highlight
  • Ctrl-j/k – Buffer navigation
  • Space bd – Close buffer
  • Space n – Toggle line numbers

Part 2 – Plugins:

  • Space f – FZF Files
  • Space b – FZF Buffers
  • Space h – FZF History
  • F2 – NERDTree toggle (if installed)
  • gcc – Toggle comment

Part 3 – CoC/LSP:

  • Tab/Shift-Tab – Navigate completion menu
  • Enter – Confirm completion
  • K – Hover documentation
  • gd – Go to definition
  • gy – Go to type definition
  • gi – Go to implementation
  • gr – Find references
  • Space dn – Next diagnostic
  • Space dp – Previous diagnostic
  • Space qf – Code fix
  • Space ac – Code action
  • Space rn – Rename symbol
  • Space fm – Format code

Remember: These are my choices. Feel free to change them to match your preferences!

What’s next

You now have a complete, modern development environment in VIM:

  • ✅ Solid foundation (~150 lines of understood config)
  • ✅ Essential plugins (colors, fuzzy finding, Git, editing helpers)
  • ✅ IDE-level intelligence (completion, navigation, diagnostics)
  • ✅ All working on z/OS

Possible future topics:

  • Integrating clang-format for automatic code formatting
  • Setting up clang-tidy for static analysis
  • HLASM LSP support (when ready)
  • CoC extensions for other languages

Key takeaways

What we learned:

  • LSP brings IDE features to any editor
  • CoC is a powerful LSP client for VIM
  • clangd provides C/C++ intelligence
  • compile_commands.json helps the language server understand your project
  • z/OS support is growing but needs community contribution

Core principles (same as Part 1 and 2):

  • Understand what you’re adding
  • Customize for your workflow
  • Start simple, add complexity as needed
  • Test each change

You’ve built a VIM configuration from scratch that rivals modern IDEs. More importantly, you understand every piece of it.

That’s the VIM way.

Related:

Questions or feedback? Drop a comment below!

Using LSP with other languages on z/OS? I’d love to hear about your setup!

2 thoughts on “Building a Modern VIM Configuration from Scratch – Part 3: IDE Intelligence with LSP”

Leave a Comment