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:
:PlugInstallWait for it to complete. CoC downloads its dependencies automatically.
Verifying the installation
Restart VIM and run:
:CocInfoYou should see something like:
vim version: 9.1
node version: v20.19.2
coc.nvim version: 0.0.82
platform: os390If 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 definitiongy– Go to type definitiongi– Go to implementationgr– Find all references
Diagnostics navigation:
Space dn– Jump to next error/warningSpace dp– Jump to previous error/warning
Code actions:
Space qf– Apply quick fix for current lineSpace ac– Show all available code actionsSpace rn– Rename symbol under cursorSpace 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 itemdiagnostic.*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 servercoc-json– JSON – brings it’s own LSP servercoc-pyright– Python – needs pyright as LSP server- …
Install extensions with :CocInstall:
:CocInstall coc-clangd coc-jsonCheck installed extensions:
:CocList extensionsC/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 similarThe 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:
_Packedkeyword 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 directoryfile– 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 cluesMost 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
.clangdfile 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 highlightCtrl-j/k– Buffer navigationSpace bd– Close bufferSpace n– Toggle line numbers
Part 2 – Plugins:
Space f– FZF FilesSpace b– FZF BuffersSpace h– FZF HistoryF2– NERDTree toggle (if installed)gcc– Toggle comment
Part 3 – CoC/LSP:
Tab/Shift-Tab– Navigate completion menuEnter– Confirm completionK– Hover documentationgd– Go to definitiongy– Go to type definitiongi– Go to implementationgr– Find referencesSpace dn– Next diagnosticSpace dp– Previous diagnosticSpace qf– Code fixSpace ac– Code actionSpace rn– Rename symbolSpace 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”