-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Background
This is not a feature request. I'm not even entirely sure its a desirable feature, but I might as well post this since I did spend some time on experimenting with it while trying to get clangd to behave properly.
As is documented, clangd supposedly implements its own custom LSP commands. Among them is one to switch between header file and source file. Presumably because their own implementation of textDocument/definition
is failing to meet expectations, regardless of whether using a compile_commands.json
database or not.
While not a fix, the correct path is likely often to use another tool than ale. Some kind of dedicated interactive LSP debugger able to send arbitrary methods to a language server for analyzing its response. Pointers towards existing such tools are welcome and to be considered being on-topic replies.
Example
Consider this personal pet project of mine, having these files:
hello.cpp
#include "greet.hpp"
int main() {
say_it();
return 0;
}
greet.hpp
#pragma once
void say_it();
greet.hpp
#include <iostream>
#include "greet.hpp"
void say_it() {
std::cout << "Hello C++!" << std::endl;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(HelloWorld LANGUAGES CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_executable(hello
hello.cpp
greet.cpp
)
add_custom_command(TARGET hello PRE_BUILD
COMMENT "Ensure clangd file is up to date."
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_BINARY_DIR}/compile_commands.json ${CMAKE_SOURCE_DIR}/
)
Changes to ale
To make things interesting, lets patch ale. This is of course not a pull request in disguise, but code as basis for further discussion. A lot of stuff is obviously wrong. Not only the fact that ale#lsp#HasCapability()
is forced to always return 1
.
Patching ale (dirty hack, brokenly attempting to implement this stuff)
diff --git a/autoload/ale/definition.vim b/autoload/ale/definition.vim
index e58a5642..300744dd 100644
--- a/autoload/ale/definition.vim
+++ b/autoload/ale/definition.vim
@@ -229,6 +229,8 @@ function! s:OnReady(line, column, options, capability, linter, lsp_details) abor
let l:message = ale#lsp#message#TypeDefinition(l:buffer, a:line, a:column)
elseif a:capability is# 'implementation'
let l:message = ale#lsp#message#Implementation(l:buffer, a:line, a:column)
+ elseif a:capability is# 'switchsourceheader'
+ let l:message = ale#lsp#message#SwitchSourceHeader(l:buffer)
else
" XXX: log here?
return
@@ -272,6 +274,12 @@ function! ale#definition#GoToImpl(options) abort
endfor
endfunction
+function! ale#definition#SwitchSourceHeader(options) abort
+ for l:linter in ale#lsp_linter#GetEnabled(bufnr(''))
+ call s:GoToLSPDefinition(l:linter, a:options, 'switchsourceheader')
+ endfor
+endfunction
+
function! ale#definition#GoToCommandHandler(command, ...) abort
let l:options = {}
@@ -299,6 +307,8 @@ function! ale#definition#GoToCommandHandler(command, ...) abort
call ale#definition#GoToType(l:options)
elseif a:command is# 'implementation'
call ale#definition#GoToImpl(l:options)
+ elseif a:command is# 'switchsourceheader'
+ call ale#definition#SwitchSourceHeader(l:options)
else
call ale#definition#GoTo(l:options)
endif
diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim
index 07b073f8..7ba5b124 100644
--- a/autoload/ale/lsp.vim
+++ b/autoload/ale/lsp.vim
@@ -490,6 +490,9 @@ function! s:SendInitMessage(conn) abort
\ 'typeDefinition': {
\ 'dynamicRegistration': v:false,
\ },
+ \ 'switchSourceHeader': {
+ \ 'dynamicRegistration': v:false,
+ \ },
\ 'implementation': {
\ 'dynamicRegistration': v:false,
\ 'linkSupport': v:false,
@@ -837,6 +840,7 @@ endfunction
" Check if an LSP has a given capability.
function! ale#lsp#HasCapability(conn_id, capability) abort
+ return 1
let l:conn = get(s:connections, a:conn_id, {})
if empty(l:conn)
diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim
index 72ed7d59..36fc43f3 100644
--- a/autoload/ale/lsp/message.vim
+++ b/autoload/ale/lsp/message.vim
@@ -126,6 +126,14 @@ function! ale#lsp#message#Definition(buffer, line, column) abort
\}]
endfunction
+" https://clangd.llvm.org/extensions#switch-between-sourceheader
+function! ale#lsp#message#SwitchSourceHeader(buffer) abort
+ return [0, 'textDocument/switchSourceHeader', {
+ \ 'TextDocumentIdentifier':
+ \ ale#util#ToURI(expand('#' . a:buffer . ':p')),
+ \}]
+endfunction
+
function! ale#lsp#message#TypeDefinition(buffer, line, column) abort
return [0, 'textDocument/typeDefinition', {
\ 'textDocument': {
diff --git a/plugin/ale.vim b/plugin/ale.vim
index ba702956..5b0101b0 100644
--- a/plugin/ale.vim
+++ b/plugin/ale.vim
@@ -297,6 +297,8 @@ command! -bar -nargs=* ALEGoToTypeDefinition :call ale#definition#GoToCommandHan
" Go to implementation for tsserver and LSP
command! -bar -nargs=* ALEGoToImplementation :call ale#definition#GoToCommandHandler('implementation', <f-args>)
+command! -bar -nargs=* ALESwitchSourceHeader :call ale#definition#GoToCommandHandler('switchsourceheader', <f-args>)
+
" Repeat a previous selection in the preview window
command! -bar ALERepeatSelection :call ale#preview#RepeatSelection()
Preparing and entering vim
You could try building and running this project to get a friendly greeting, and also to ensure compile_commands.json
gets generated alright. Then opening up the main file in a vim with ale patched in accordance with above:
mkdir build && cd build && cmake .. && make
./hello
cd ..
gvim hello.cpp
Now it is time to start saving your LSP logs if following along interactively. I have ALESaveLSPLog
for this and it should be contributed somewhere in the comments of some other issue, using call ch_logfile('/tmp/chlogfile')
+ StopAllLSPs
ought to work just as well.
Communication of course starts out with some initial messages between ale and clangd: (some verbose obviously uninteresting things are stripped out of the initial message, and a few fully unrelated messages are completely left out.)
The outgoing initialize message contains the added capability, but I'm not sure its named correctly or has valid arguments.
id 4: initialize, ale → clangd
{
"method": "initialize",
"jsonrpc": "2.0",
"id": 4,
"params": {
"initializationOptions": {},
"rootUri": "file:///tmp/hello",
"capabilities": {
…
"textDocument": {
…
"switchSourceHeader": {
"dynamicRegistration": false
},
…
}
},
"rootPath": "/tmp/hello",
"processId": 46392
}
}
The response does not appear to mention the capability, as far as I can see. That could be due to an invalid initialization request, or due to the feature missing. Likely the first, given the exact details of the response when attempting to use it.
id 4: full response, ale ← clangd
{
"id": 4,
"jsonrpc": "2.0",
"result": {
"capabilities": {
"astProvider": true,
"callHierarchyProvider": true,
"clangdInlayHintsProvider": true,
"codeActionProvider": {
"codeActionKinds": [
"quickfix",
"refactor",
"info"
]
},
"compilationDatabase": {
"automaticReload": true
},
"completionProvider": {
"resolveProvider": false,
"triggerCharacters": [
".",
"<",
">",
":",
"\"",
"/",
"*"
]
},
"declarationProvider": true,
"definitionProvider": true,
"documentFormattingProvider": true,
"documentHighlightProvider": true,
"documentLinkProvider": {
"resolveProvider": false
},
"documentOnTypeFormattingProvider": {
"firstTriggerCharacter": "\n",
"moreTriggerCharacter": []
},
"documentRangeFormattingProvider": true,
"documentSymbolProvider": true,
"executeCommandProvider": {
"commands": [
"clangd.applyFix",
"clangd.applyTweak"
]
},
"foldingRangeProvider": true,
"hoverProvider": true,
"implementationProvider": true,
"inlayHintProvider": true,
"memoryUsageProvider": true,
"referencesProvider": true,
"renameProvider": true,
"selectionRangeProvider": true,
"semanticTokensProvider": {
"full": {
"delta": true
},
"legend": {
"tokenModifiers": [
"declaration",
"definition",
"deprecated",
"deduced",
"readonly",
"static",
"abstract",
"virtual",
"dependentName",
"defaultLibrary",
"usedAsMutableReference",
"usedAsMutablePointer",
"constructorOrDestructor",
"userDefined",
"functionScope",
"classScope",
"fileScope",
"globalScope"
],
"tokenTypes": [
"variable",
"variable",
"parameter",
"function",
"method",
"function",
"property",
"variable",
"class",
"interface",
"enum",
"enumMember",
"type",
"type",
"unknown",
"namespace",
"typeParameter",
"concept",
"type",
"macro",
"modifier",
"operator",
"comment"
]
},
"range": false
},
"signatureHelpProvider": {
"triggerCharacters": [
"(",
")",
"{",
"}",
"<",
">",
","
]
},
"standardTypeHierarchyProvider": true,
"textDocumentSync": {
"change": 2,
"openClose": true,
"save": true
},
"typeDefinitionProvider": true,
"typeHierarchyProvider": true,
"workspaceSymbolProvider": true
},
"serverInfo": {
"name": "clangd",
"version": "clangd version 16.0.6 unix x86_64-portbld-freebsd14.2"
}
}
}
A couple of expected uninteresting messages gets sent before the channel gets idle.
initialized, ale → clangd
{
"method": "initialized",
"jsonrpc": "2.0",
"params": {}
}
testDocument/didOpen, ale → clangd
{
"method": "textDocument/didOpen",
"jsonrpc": "2.0",
"params": {
"textDocument": {
"uri": "file:///tmp/hello/hello.cpp",
"version": 2,
"languageId": "cpp",
"text": "#include \"greet.hpp\"\n\nint main() {\n say_it();\n return 0;\n}\n"
}
}
}
Use existing command, with disappointment
Navigate to say_it()
and call ALEGoToDefinition
. Here everyone probably expects to end up on its definition in greet.cpp
, but the way clangd works is that where you end up depends on its internal state. For the first few minutes, even on a trivial project like this one, you end up at the declaration in greet.hpp
. Do you see why this debugging session happened yet‽
id 5: textDocument/definition, ale → clangd
{
"method": "textDocument/definition",
"jsonrpc": "2.0",
"id": 5,
"params": {
"textDocument": {
"uri": "file:///tmp/hello/hello.cpp"
},
"position": {
"character": 4,
"line": 3
}
}
}
id 5: response, ale ← clangd
{
"id": 5,
"jsonrpc": "2.0",
"result": [
{
"range": {
"end": {
"character": 11,
"line": 2
},
"start": {
"character": 5,
"line": 2
}
},
"uri": "file:///tmp/hello/greet.hpp"
}
]
}
textDocument/didOpen, ale → clangd
{
"method": "textDocument/didOpen",
"jsonrpc": "2.0",
"params": {
"textDocument": {
"uri": "file:///tmp/hello/greet.hpp",
"version": 3,
"languageId": "cpp",
"text": "#pragma once\n\nvoid say_it();\n"
}
}
}
Attempt new command, ends in failure
Now comes our actual command. I've tried to make the request look like to Protocol Extension describes it, but another pair of eyes might spot something I'm missing.
id 7: textDocument/switchSourceHeader, ale → clangd
{
"method": "textDocument/switchSourceHeader",
"jsonrpc": "2.0",
"id": 7,
"params": {
"TextDocumentIdentifier": "file:///tmp/hello/greet.hpp"
}
}
To me, the response reads as that clangd understands the command but has issues with the argument.
id 7: response, ale ← clangd
{
"error": {
"code": -32602,
"message": "failed to decode textDocument/switchSourceHeader request: missing value at (root).uri"
},
"id": 7,
"jsonrpc": "2.0"
}
Ending comment
When reaching this point of debugging, clangd had caught up with my project and started to place me at the definition rather than the declaration. Which means I kind of lost interest, but I post this here just in case it'll help either myself or someone else in the future.