30 Commits

Author SHA1 Message Date
Myriad-Dreamin
1f8168577b wip 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
60cab39f92 dev: make server public 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
67bedd1ae0 dev: index nodes 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
7ab8ad9513 fix: crash 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
c11178fd77 dev: builtin lib 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
150f47c590 hardcode clang 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
b7987fded3 dev: workaround cc1 compilation 2026-01-26 17:45:00 +08:00
Myriad-Dreamin
44a4bd4107 dev: index notify 2026-01-26 17:44:47 +08:00
Myriad-Dreamin
044e4c4b27 feat: implement serialize for optional 2026-01-26 17:44:47 +08:00
Myriad-Dreamin
68eb63ba04 ActiveFile 2026-01-26 17:44:47 +08:00
Myriad-Dreamin
39d3648fdd feat: add method to check indices size 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
6640c05d66 build: cannot build cpptrace 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
69ac764ef2 feat: split copy_headers for external builds 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
04c6ca5337 fix: correct server ref impl 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
2408978d2d fix: correct plugin_paths decl 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
74f75f107f fix: compile error 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
0e2e487bc9 feat: introduce any type alias for llvm::json::Value 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
6be48bccd2 feat: implement dummy workspace/executeCommand 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
4262734d21 dev: simplify protocol def 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
ed0d7db3db dev: simplify protocol def 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
824b305f93 dev: simplify protocol def 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
f89793c66d dev: more lifetime hooks 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
4425fb5244 dev: simplify 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
25a85a3b8e feat: implement apis 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
0f95344abe fix: typo 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
3511915886 dev: remove useless comments 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
6e50451c43 fix: typo 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
99d9363b95 fix: paths 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
7533d4d15e docs: compile and laod plugin sections 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
a118c16e96 feat: add minimal support to clice server plugins 2026-01-26 01:01:39 +08:00
35 changed files with 1274 additions and 72 deletions

0
.codex Normal file
View File

View File

@@ -150,6 +150,28 @@ add_custom_target(
DEPENDS ${CONFIG_GENERATED_FILE}
)
add_library(clice_core_deps INTERFACE)
target_include_directories(clice_core_deps INTERFACE
"${PROJECT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_BINARY_DIR}/generated"
)
target_link_libraries(clice_core_deps INTERFACE
clice_options
libuv::libuv
spdlog::spdlog
tomlplusplus::tomlplusplus
roaring::roaring
flatbuffers
llvm-libs
)
add_library(clice_builtin_api INTERFACE)
target_link_libraries(clice_builtin_api INTERFACE clice_core_deps)
include("${PROJECT_SOURCE_DIR}/cmake/builtin-libraries.cmake")
clice_include_builtin_library_modules()
file(GLOB_RECURSE CLICE_SOURCES CONFIGURE_DEPENDS
"${PROJECT_SOURCE_DIR}/src/AST/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Async/*.cpp"
@@ -162,20 +184,8 @@ file(GLOB_RECURSE CLICE_SOURCES CONFIGURE_DEPENDS
)
add_library(clice-core STATIC "${CLICE_SOURCES}")
add_dependencies(clice-core generate_flatbuffers_schema generate_config)
target_include_directories(clice-core PUBLIC
"${PROJECT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_BINARY_DIR}/generated"
)
target_link_libraries(clice-core PUBLIC
clice_options
libuv::libuv
spdlog::spdlog
tomlplusplus::tomlplusplus
roaring::roaring
flatbuffers
llvm-libs
)
target_link_libraries(clice-core PUBLIC clice_core_deps)
clice_finalize_builtin_libraries(TARGET clice-core)
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")
target_link_libraries(clice PRIVATE clice-core)

View File

@@ -0,0 +1,158 @@
include_guard(GLOBAL)
set(
CLICE_BUILTIN_LIBRARY_MODULES
"${CLICE_BUILTIN_LIBRARY_MODULES}"
CACHE STRING
"Semicolon-separated list of CMake modules that register extra builtin clice libraries"
)
function(clice_add_builtin_library)
set(options)
set(oneValueArgs NAME ENTRYPOINT)
set(multiValueArgs SOURCES INCLUDE_DIRECTORIES LINK_LIBRARIES COMPILE_DEFINITIONS COMPILE_OPTIONS)
cmake_parse_arguments(CBL "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(CBL_UNPARSED_ARGUMENTS)
message(
FATAL_ERROR
"clice_add_builtin_library got unexpected arguments: ${CBL_UNPARSED_ARGUMENTS}"
)
endif()
if(NOT CBL_NAME)
message(FATAL_ERROR "clice_add_builtin_library requires NAME")
endif()
if(NOT CBL_SOURCES)
message(FATAL_ERROR "clice_add_builtin_library(${CBL_NAME}) requires SOURCES")
endif()
if(NOT CBL_ENTRYPOINT)
message(FATAL_ERROR "clice_add_builtin_library(${CBL_NAME}) requires ENTRYPOINT")
endif()
if(NOT TARGET clice_builtin_api)
message(
FATAL_ERROR
"clice_builtin_api must be defined before calling clice_add_builtin_library(${CBL_NAME})"
)
endif()
set(target "clice_builtin_${CBL_NAME}")
if(TARGET "${target}")
message(FATAL_ERROR "builtin library target '${target}' already exists")
endif()
add_library("${target}" OBJECT)
target_sources("${target}" PRIVATE ${CBL_SOURCES})
target_link_libraries("${target}" PUBLIC clice_builtin_api)
add_dependencies("${target}" generate_flatbuffers_schema generate_config)
if(CBL_INCLUDE_DIRECTORIES)
target_include_directories("${target}" PRIVATE ${CBL_INCLUDE_DIRECTORIES})
endif()
if(CBL_LINK_LIBRARIES)
target_link_libraries("${target}" PUBLIC ${CBL_LINK_LIBRARIES})
endif()
if(CBL_COMPILE_DEFINITIONS)
target_compile_definitions("${target}" PRIVATE ${CBL_COMPILE_DEFINITIONS})
endif()
if(CBL_COMPILE_OPTIONS)
target_compile_options("${target}" PRIVATE ${CBL_COMPILE_OPTIONS})
endif()
set_property(GLOBAL APPEND PROPERTY CLICE_BUILTIN_LIBRARY_TARGETS "${target}")
set_property(GLOBAL APPEND PROPERTY CLICE_BUILTIN_LIBRARY_ENTRYPOINTS "${CBL_ENTRYPOINT}")
endfunction()
function(clice_include_builtin_library_modules)
foreach(module_path IN LISTS CLICE_BUILTIN_LIBRARY_MODULES)
cmake_path(
ABSOLUTE_PATH module_path
BASE_DIRECTORY "${PROJECT_SOURCE_DIR}"
NORMALIZE
OUTPUT_VARIABLE module_abs_path
)
if(NOT EXISTS "${module_abs_path}")
message(
FATAL_ERROR
"builtin library module '${module_path}' does not exist: ${module_abs_path}"
)
endif()
include("${module_abs_path}")
endforeach()
endfunction()
function(clice_finalize_builtin_libraries)
set(options)
set(oneValueArgs TARGET REGISTRATION_SOURCE)
cmake_parse_arguments(CBL "${options}" "${oneValueArgs}" "" ${ARGN})
if(CBL_UNPARSED_ARGUMENTS)
message(
FATAL_ERROR
"clice_finalize_builtin_libraries got unexpected arguments: ${CBL_UNPARSED_ARGUMENTS}"
)
endif()
if(NOT CBL_TARGET)
message(FATAL_ERROR "clice_finalize_builtin_libraries requires TARGET")
endif()
if(NOT TARGET "${CBL_TARGET}")
message(FATAL_ERROR "target '${CBL_TARGET}' does not exist")
endif()
if(NOT CBL_REGISTRATION_SOURCE)
set(CBL_REGISTRATION_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/generated/builtin-libraries.cpp")
endif()
get_property(builtin_targets GLOBAL PROPERTY CLICE_BUILTIN_LIBRARY_TARGETS)
get_property(builtin_entrypoints GLOBAL PROPERTY CLICE_BUILTIN_LIBRARY_ENTRYPOINTS)
if(NOT builtin_targets)
set(builtin_targets "")
endif()
if(NOT builtin_entrypoints)
set(builtin_entrypoints "")
endif()
set(registration_source_content "#include \"Server/Plugin.h\"\n\nnamespace clice {\n\n")
foreach(entrypoint IN LISTS builtin_entrypoints)
string(APPEND registration_source_content "::clice::PluginInfo ${entrypoint}();\n")
endforeach()
string(APPEND registration_source_content "\nvoid register_builtin_server_plugins(ServerPluginBuilder& builder) {\n")
foreach(entrypoint IN LISTS builtin_entrypoints)
string(
APPEND
registration_source_content
" ${entrypoint}().register_server_callbacks(builder);\n"
)
endforeach()
string(APPEND registration_source_content "}\n\n} // namespace clice\n")
get_filename_component(registration_source_dir "${CBL_REGISTRATION_SOURCE}" DIRECTORY)
file(MAKE_DIRECTORY "${registration_source_dir}")
file(CONFIGURE OUTPUT "${CBL_REGISTRATION_SOURCE}" CONTENT "${registration_source_content}" @ONLY)
target_sources("${CBL_TARGET}" PRIVATE "${CBL_REGISTRATION_SOURCE}")
if(builtin_targets)
foreach(builtin_target IN LISTS builtin_targets)
target_sources("${CBL_TARGET}" PRIVATE $<TARGET_OBJECTS:${builtin_target}>)
endforeach()
target_link_libraries("${CBL_TARGET}" PRIVATE ${builtin_targets})
endif()
endfunction()

View File

@@ -66,16 +66,20 @@ set(FLATBUFFERS_BUILD_GRPC OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
# cpptrace
FetchContent_Declare(
cpptrace
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
GIT_TAG v1.0.4
GIT_SHALLOW TRUE
)
set(CPPTRACE_DISABLE_CXX_20_MODULES ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(libuv spdlog tomlplusplus croaring flatbuffers)
FetchContent_MakeAvailable(libuv spdlog tomlplusplus croaring flatbuffers cpptrace)
if(CLICE_ENABLE_TEST)
# cpptrace
FetchContent_Declare(
cpptrace
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
GIT_TAG v1.0.4
GIT_SHALLOW TRUE
)
set(CPPTRACE_DISABLE_CXX_20_MODULES ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(cpptrace)
endif()
if(WIN32)
target_compile_definitions(uv_a PRIVATE _CRT_SECURE_NO_WARNINGS)

View File

@@ -69,6 +69,7 @@ Optional build options:
| CLICE_ENABLE_TEST | OFF | Build clice unit tests |
| CLICE_USE_LIBCXX | OFF | Build clice with libc++ (adds `-std=libc++`); if enabled, ensure the LLVM libs are also built with libc++ |
| CLICE_CI_ENVIRONMENT | OFF | Enable the `CLICE_CI_ENVIRONMENT` macro; some tests only run in CI |
| CLICE_BUILTIN_LIBRARY_MODULES | "" | Semicolon-separated list of CMake modules that register builtin libraries compiled into `clice`; see [Builtin Libraries](./builtin-library.md) |
### XMake

View File

@@ -0,0 +1,101 @@
# Builtin Libraries
Builtin libraries are compiled directly into the `clice` binary instead of being loaded later with `--plugin-path`.
This is useful when:
- your plugin sources live outside the `clice` source tree
- you want the builtin to be part of the default executable
- you need extra include directories, compile definitions, or link dependencies during the main build
## CMake Entry Point
`clice` now exposes a small helper module at [cmake/builtin-libraries.cmake](/cmake/builtin-libraries.cmake).
Extra builtin libraries are registered through the cache variable `CLICE_BUILTIN_LIBRARY_MODULES`.
Each value in `CLICE_BUILTIN_LIBRARY_MODULES` must be a CMake file. During configure, `clice` includes those files, and each file calls `clice_add_builtin_library(...)`.
## Minimal Module
Create a CMake file in your external project, for example `/path/to/my-plugin/clice-builtin.cmake`:
```cmake
clice_add_builtin_library(
NAME my_plugin
SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/MyPlugin.cpp"
INCLUDE_DIRECTORIES
"${CMAKE_CURRENT_LIST_DIR}/include"
ENTRYPOINT
clice_get_my_plugin_server_plugin_info
)
```
Then configure `clice` with:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/my-plugin/clice-builtin.cmake"
```
To load multiple modules, pass a semicolon-separated CMake list:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/a.cmake;/path/to/b.cmake"
```
## `clice_add_builtin_library`
The helper accepts the following arguments:
| Argument | Required | Description |
| --- | --- | --- |
| `NAME` | Yes | Logical name used to create an internal object target |
| `SOURCES` | Yes | Source files compiled into `clice`; absolute paths and out-of-tree paths are supported |
| `ENTRYPOINT` | Yes | Unique function name in namespace `clice` that returns `::clice::PluginInfo` |
| `INCLUDE_DIRECTORIES` | No | Extra include directories for this builtin only |
| `LINK_LIBRARIES` | No | Extra libraries or targets needed by this builtin |
| `COMPILE_DEFINITIONS` | No | Extra compile definitions for this builtin |
| `COMPILE_OPTIONS` | No | Extra compile options for this builtin |
## Entrypoint Requirements
Builtin libraries share a single final executable, so each builtin must use its own unique entrypoint function inside namespace `clice`.
Dynamic plugins use:
```cpp
clice_get_server_plugin_info()
```
Builtin libraries should use a unique name such as:
```cpp
clice_get_my_plugin_server_plugin_info()
```
For example:
```cpp
#include "Server/Plugin.h"
namespace clice {
::clice::PluginInfo clice_get_my_plugin_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION,
"MyPlugin",
"v0.0.1",
CLICE_PLUGIN_DEF_HASH,
[](clice::ServerPluginBuilder& builder) {
// register callbacks here
},
};
}
} // namespace clice
```
`clice` generates the static registration glue automatically, so once the module is included, no additional edits to `src/clice.cc` are required.

View File

@@ -0,0 +1,78 @@
You can implement a clice server plugin to extend clice's functionality.
## Use case
When you use `clice` as LSP backend of LLM agents, e.g. claude code, you can add plugin to provide some extra features.
## Writing a plugin
When a plugin is loaded by the server, it will call `clice_get_server_plugin_info` to obtain information about this plugin and about how to register its customization points.
This function needs to be implemented by the plugin, see the example below:
```c++
extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK
clice_get_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.1", CLICE_PLUGIN_DEF_HASH,
[](ServerPluginBuilder builder) { ... }
};
}
```
See [PluginProtocol.h](/include/Server/PluginProtocol.h) for more details.
## Compiling a plugin
The plugin must be compiled with the same dependencies and compiler options as clice, otherwise it will cause undefined behavior. [config/llvm-manifest.json](/config/llvm-manifest.json) defines the build information used by clice.
## Loading plugins
For security reasons, clice does not allow loading plugins through configuration files, but must specify the plugin path through command line options.
When `clice` starts, it will load all plugins specified in the command line. You can specify the plugin path through the `--plugin-path` option.
```shell
$ clice --plugin-path /path/to/my-plugin.so
```
## Getting content of `CLICE_PLUGIN_DEF_HASH`
There are two values to return in the `clice_get_server_plugin_info` function.
- `CLICE_PLUGIN_API_VERSION` is used to ensure compability of the `clice_get_server_plugin_info` function between the plugin and the server.
- `CLICE_PLUGIN_DEF_HASH` is used to ensure the consistency of the C++ declarations between the plugin and the server.
To debug the content of `CLICE_PLUGIN_DEF_HASH`, you can run following command:
```shell
$ git clone https://github.com/clice-io/clice.git
$ cd clice
$ git checkout `clice --version --git-describe`
$ python scripts/plugin-def.py content
```
You will get a C source code file, content of which is like this:
```cpp
#if 0
// begin of config/llvm-manifest.json
[
{
"version": "21.1.4+r1",
"filename": "arm64-macos-clang-debug-asan.tar.xz",
"sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5",
"lto": false,
"asan": true,
"platform": "macosx",
"build_type": "Debug"
},
...
]
...
#endif
```
## Builtin libraries
If you want to compile a plugin directly into the `clice` binary instead of loading it dynamically, see [Builtin Libraries](./builtin-library.md).

View File

@@ -69,6 +69,7 @@ cmake -B build -G Ninja \
| CLICE_ENABLE_TEST | OFF | 是否构建 clice 的单元测试 |
| CLICE_USE_LIBCXX | OFF | 是否使用 libc++ 来构建 clice添加 `-std=libc++`),如果开启,请确保 LLVM 库也是使用 libc++ 编译的 |
| CLICE_CI_ENVIRONMENT | OFF | 是否打开 `CLICE_CI_ENVIRONMENT` 这个宏,有些测试在 CI 环境才会执行 |
| CLICE_BUILTIN_LIBRARY_MODULES | "" | 以分号分隔的 CMake 模块列表,用于注册会被编译进 `clice` 的 builtin library详见 [Builtin Libraries](./builtin-library.md) |
### XMake

View File

@@ -0,0 +1,101 @@
# Builtin Libraries
Builtin library 会被直接编译进 `clice` 可执行文件,而不是在运行时通过 `--plugin-path` 动态加载。
这种方式适合以下场景:
- 插件源码位于 `clice` 源码树之外
- 希望该 builtin 默认随 `clice` 可执行文件一起发布
- 需要为该 builtin 单独补充 include 路径、编译宏或链接依赖
## CMake 入口
`clice` 现在提供了一个辅助模块:[cmake/builtin-libraries.cmake](/cmake/builtin-libraries.cmake)。
额外的 builtin library 通过缓存变量 `CLICE_BUILTIN_LIBRARY_MODULES` 注册。
`CLICE_BUILTIN_LIBRARY_MODULES` 中的每一项都必须是一个 CMake 文件。配置阶段 `clice``include()` 这些文件,而每个文件都需要调用 `clice_add_builtin_library(...)`
## 最小示例
你可以在外部项目中创建一个 CMake 文件,例如 `/path/to/my-plugin/clice-builtin.cmake`
```cmake
clice_add_builtin_library(
NAME my_plugin
SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/MyPlugin.cpp"
INCLUDE_DIRECTORIES
"${CMAKE_CURRENT_LIST_DIR}/include"
ENTRYPOINT
clice_get_my_plugin_server_plugin_info
)
```
然后在配置 `clice` 时传入:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/my-plugin/clice-builtin.cmake"
```
如果要加载多个模块,可以传入以分号分隔的 CMake 列表:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/a.cmake;/path/to/b.cmake"
```
## `clice_add_builtin_library`
该辅助函数支持以下参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `NAME` | 是 | 逻辑名称,会用于创建内部 object target |
| `SOURCES` | 是 | 要编译进 `clice` 的源文件,支持绝对路径和源码树外路径 |
| `ENTRYPOINT` | 是 | `clice` 命名空间中的唯一函数名,返回值类型为 `::clice::PluginInfo` |
| `INCLUDE_DIRECTORIES` | 否 | 仅对当前 builtin 生效的额外头文件目录 |
| `LINK_LIBRARIES` | 否 | 当前 builtin 额外需要的库或 target |
| `COMPILE_DEFINITIONS` | 否 | 当前 builtin 额外需要的编译宏 |
| `COMPILE_OPTIONS` | 否 | 当前 builtin 额外需要的编译选项 |
## Entrypoint 要求
所有 builtin library 最终都会被链接进同一个可执行文件,因此每个 builtin 都必须在 `clice` 命名空间中使用唯一的入口函数名。
动态插件通常使用:
```cpp
clice_get_server_plugin_info()
```
builtin library 应该改用类似下面的唯一名字:
```cpp
clice_get_my_plugin_server_plugin_info()
```
例如:
```cpp
#include "Server/Plugin.h"
namespace clice {
::clice::PluginInfo clice_get_my_plugin_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION,
"MyPlugin",
"v0.0.1",
CLICE_PLUGIN_DEF_HASH,
[](clice::ServerPluginBuilder& builder) {
// 在这里注册回调
},
};
}
} // namespace clice
```
`clice` 会自动生成静态注册代码,因此只要模块被包含进来,就不需要再手动修改 `src/clice.cc`

View File

@@ -0,0 +1,78 @@
你可以在 clice 中实现一个 server plugin 来扩展 clice 的功能。
## 用例
当你使用 `clice` 作为 LLM 代理的 LSP 后端时,比如 claude code你可以添加插件来提供一些额外功能。
## 编写插件
当一个插件被服务器加载时,它会调用 `clice_get_server_plugin_info` 来获取关于这个插件的信息以及如何注册它的定制点。
这个函数需要由插件实现,请参考下面的示例:
```cpp
extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK
clice_get_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.1", CLICE_PLUGIN_DEF_HASH,
[](ServerPluginBuilder builder) { ... }
};
}
```
请参考 [PluginProtocol.h](/include/Server/PluginProtocol.h) 了解更多细节。
## 编译插件
插件必须使用与 clice 一致的依赖和编译器选项来编译,否则会导致 undefined behavior。[config/llvm-manifest.json](/config/llvm-manifest.json) 中定义了 clice 使用的构建信息。
## 加载插件
为了安全考虑clice 不允许通过配置文件来加载插件,而必须通过命令行选项来指定插件的路径。
`clice` 启动时,它会加载所有在命令行中指定的插件。你可以通过 `--plugin-path` 选项来指定插件的路径。
```shell
$ clice --plugin-path /path/to/my-plugin.so
```
## 获取 `CLICE_PLUGIN_DEF_HASH` 的内容
`clice_get_server_plugin_info` 函数中需要返回两个值。
- `CLICE_PLUGIN_API_VERSION` 用于确保插件和服务器之间的 `clice_get_server_plugin_info` 函数的一致性。
- `CLICE_PLUGIN_DEF_HASH` 用于确保插件和服务器之间的 C++ 声明的一致性。
要调试 `CLICE_PLUGIN_DEF_HASH` 的内容,你可以运行以下命令:
```shell
$ git clone https://github.com/clice-io/clice.git
$ cd clice
$ git checkout `clice --version --git-describe`
$ python scripts/plugin-def.py content > /tmp/plugin-proto.h
```
你将会得到一个 C 源码格式的文件,内容大致如下:
```cpp
#if 0
// begin of config/llvm-manifest.json
[
{
"version": "21.1.4+r1",
"filename": "arm64-macos-clang-debug-asan.tar.xz",
"sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5",
"lto": false,
"asan": true,
"platform": "macosx",
"build_type": "Debug"
},
...
]
...
#endif
```
## Builtin libraries
如果你希望把插件直接编译进 `clice` 可执行文件,而不是在运行时动态加载,请参考 [Builtin Libraries](./builtin-library.md)。

View File

@@ -37,6 +37,10 @@ public:
}
}
void unset() {
ready = false;
}
void clear() {
ready = false;
awaiters.clear();

View File

@@ -98,7 +98,7 @@ public:
static std::optional<std::uint32_t> get_option_id(llvm::StringRef argument);
/// FIXME: bad interface design ...
std::vector<const char*> files();
std::vector<llvm::StringRef> files();
/// FIXME: remove this api?
auto save_string(llvm::StringRef string) -> llvm::StringRef;

View File

@@ -6,6 +6,8 @@
#include <string>
#include <vector>
#include "llvm/Support/JSON.h"
namespace clice::proto {
using integer = std::int32_t;
@@ -15,6 +17,8 @@ using decimal = double;
using string = std::string;
using any = llvm::json::Value;
template <typename T>
using array = std::vector<T>;

View File

@@ -0,0 +1,17 @@
#pragma once
#include "../Basic.h"
namespace clice::proto {
struct ExecuteCommandParams {
string command;
array<any> arguments;
};
struct TextDocumentParams {
/// The text document.
TextDocumentIdentifier textDocument;
};
} // namespace clice::proto

View File

@@ -11,6 +11,7 @@
#include "Feature/DocumentHighlight.h"
#include "Feature/DocumentLink.h"
#include "Feature/DocumentSymbol.h"
#include "Feature/ExecuteCommand.h"
#include "Feature/FoldingRange.h"
#include "Feature/Formatting.h"
#include "Feature/Hover.h"

View File

@@ -44,6 +44,11 @@ public:
if(it2 != project_index.indices.end()) {
auto path = project_index.path_pool.path(it2->second);
it->second = index::MergedIndex::load(path);
} else {
std::println(stderr,
"failed to load project index for path_id: {} {}",
path_id,
project_index.indices.size());
}
return it->second;
@@ -67,6 +72,14 @@ public:
/// TODO: Types ...
bool empty() const {
return project_index.indices.empty();
}
size_t size() const {
return project_index.indices.size();
}
private:
CompilationDatabase& database;
@@ -87,6 +100,10 @@ private:
std::deque<std::uint32_t> waitings;
async::Event update_event;
async::Event finish_event;
size_t finish_cnt = 0;
};
} // namespace clice

59
include/Server/Plugin.h Normal file
View File

@@ -0,0 +1,59 @@
#pragma once
#include <expected>
#include "PluginProtocol.h"
#include "llvm/ADT/StringRef.h"
// clang-format off
/// Run `python scripts/plugin-def.py update` to update the hash.
#define CLICE_PLUGIN_DEF_HASH "sha256:332cd65741dea17d5168dd1ab564ac73ce230f14fc1e34170438d895eb11881c"
// clang-format on
namespace clice {
/// The hash of the definitions exposed to server plugins.
constexpr std::string_view plugin_definition_hash = CLICE_PLUGIN_DEF_HASH;
class Server;
struct ServerPluginBuilder;
/// A loaded server plugin.
///
/// An instance of this class wraps a loaded server plugin and gives access to its interface.
class Plugin {
public:
/// Attempts to load a server plugin from a given file.
///
/// Returns an error if either the library cannot be found or loaded,
/// there is no public entry point, or the plugin implements the wrong API
/// version.
static std::expected<Plugin, std::string> load(const std::string& file_path);
/// Gets the file path of the loaded plugin.
llvm::StringRef file_path() const;
/// Gets the name of the loaded plugin.
llvm::StringRef name() const;
/// Gets the version of the loaded plugin.
llvm::StringRef version() const;
/// Registers the server callbacks for the loaded plugin.
void register_server_callbacks(ServerPluginBuilder& builder) const;
public:
struct Self;
Plugin(Self* self) : self(self) {}
Self* operator->() {
return self;
}
protected:
Self* self;
};
} // namespace clice

View File

@@ -0,0 +1,113 @@
#pragma once
/// The API version of the clice plugin.
/// Update this version when you change:
/// - The definition of struct `PluginInfo`.
/// - The definition of function `clice_get_server_plugin_info`.
/// Note: you don't have to update this version if you only change other APIs, which is guaranteed
/// by the `PluginInfo::definition_hash`.
#define CLICE_PLUGIN_API_VERSION 1
#include <cstdint>
#include "Async/Async.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/Support/Compiler.h"
#include "llvm/Support/JSON.h"
namespace clice {
class Server;
struct ServerPluginBuilder;
/// Defines the library APIs that loads a plugin.
extern "C" {
/// A C-compatible struct that contains information about the plugin.
struct PluginInfo {
/// The clice API version of the plugin.
uint32_t api_version;
/// The name of the plugin.
const char* name;
/// The version of the plugin.
const char* version;
/// The plugin definition hash.
const char* definition_hash;
/// Registers the server callbacks for the loaded plugin.
void (*register_server_callbacks)(ServerPluginBuilder& builder);
};
/// The public entry point for a server plugin.
///
/// When a plugin is loaded by the server, it will call this entry point to
/// obtain information about this plugin and about how to register its customization points.
/// This function needs to be implemented by the plugin, see the example below:
///
/// ```cpp
/// #include "Server/Plugin.h"
/// extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK
/// clice_get_server_plugin_info() {
/// return {
/// CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.0.1", CLICE_PLUGIN_DEF_HASH,
/// [](clice::ServerPluginBuilder& builder) { ... }
/// };
/// }
/// ```
PluginInfo LLVM_ATTRIBUTE_WEAK clice_get_server_plugin_info();
}
struct ServerRef {
public:
struct Self;
ServerRef(Server* self) : self(self) {}
Server* operator->() const {
return self;
}
Server& server() const;
protected:
Server* self;
};
/// Defines the library APIs to register callbacks for a plugin.
struct ServerPluginBuilder {
public:
ServerPluginBuilder(ServerRef server_ref) : server_ref(server_ref) {}
/// Gets a reference to the server.
auto get_server_ref() const -> ServerRef {
return server_ref;
}
#define CliceServerPluginAPI(METHOD, ...) void METHOD(void* plugin_data, __VA_ARGS__)
using lifecycle_hook_t = async::Task<> (*)(ServerRef server, void* plugin_data);
/// Registers a callback to be called when the server is initialized.
CliceServerPluginAPI(on_initialize, lifecycle_hook_t callback);
/// Registers a callback to be called when the server is initialized.
CliceServerPluginAPI(on_initialized, lifecycle_hook_t callback);
/// Registers a callback to be called when the server is shutdown.
CliceServerPluginAPI(on_shutdown, lifecycle_hook_t callback);
/// Registers a callback to be called when the server is exiting.
CliceServerPluginAPI(on_exit, lifecycle_hook_t callback);
/// Registers a callback to be called when the server's configuration is changed.
CliceServerPluginAPI(on_did_change_configuration, lifecycle_hook_t callback);
using command_handler_t =
async::Task<llvm::json::Value> (*)(ServerRef server,
void* plugin_data,
llvm::ArrayRef<llvm::json::Value> arguments);
/// Registers a callback to be called when a command is received from the LSP client.
CliceServerPluginAPI(register_commmand_handler,
llvm::StringRef command,
command_handler_t callback);
#undef CliceServerPluginAPI
protected:
ServerRef server_ref;
};
} // namespace clice

View File

@@ -3,6 +3,7 @@
#include "Config.h"
#include "Convert.h"
#include "Indexer.h"
#include "Plugin.h"
#include "Async/Async.h"
#include "Compiler/Command.h"
#include "Compiler/Diagnostic.h"
@@ -10,6 +11,8 @@
#include "Feature/DocumentLink.h"
#include "Protocol/Protocol.h"
#include <llvm/ADT/FunctionExtras.h>
namespace clice {
struct OpenFile {
@@ -76,10 +79,10 @@ public:
}
/// Try get OpenFile from manager, default construct one if not exists.
[[nodiscard]] ActiveFile& get_or_add(llvm::StringRef path);
[[nodiscard]] ActiveFile get_or_add(llvm::StringRef path);
/// Add a OpenFile to the manager.
ActiveFile& add(llvm::StringRef path, OpenFile file);
ActiveFile add(llvm::StringRef path, OpenFile file);
[[nodiscard]] bool contains(llvm::StringRef path) const {
return index.contains(path);
@@ -94,7 +97,7 @@ public:
}
private:
ActiveFile& lru_put_impl(llvm::StringRef path, OpenFile file);
ActiveFile lru_put_impl(llvm::StringRef path, OpenFile file);
private:
/// The maximum size of the cache.
@@ -167,7 +170,7 @@ private:
async::Task<> on_exit(proto::ExitParams params);
private:
public:
/// Load the cache info from disk.
void load_cache_info();
@@ -192,6 +195,8 @@ private:
private:
using Result = async::Task<json::Value>;
auto on_execute_command(proto::ExecuteCommandParams params) -> Result;
auto on_completion(proto::CompletionParams params) -> Result;
auto on_hover(proto::HoverParams params) -> Result;
@@ -218,7 +223,7 @@ private:
auto on_inlay_hint(proto::InlayHintParams params) -> Result;
private:
public:
/// The current request id.
std::uint32_t server_request_id = 0;
std::uint32_t client_request_id = 0;
@@ -241,6 +246,20 @@ private:
config::Config config;
Indexer indexer;
public:
friend struct ServerPluginBuilder;
using lifecycle_hook_t = llvm::unique_function<async::Task<>()>;
using command_handler_t = llvm::unique_function<async::Task<llvm::json::Value>(
llvm::ArrayRef<llvm::json::Value> arguments)>;
std::vector<lifecycle_hook_t> initialize_hooks;
std::vector<lifecycle_hook_t> initialized_hooks;
std::vector<lifecycle_hook_t> shutdown_hooks;
std::vector<lifecycle_hook_t> exit_hooks;
std::vector<lifecycle_hook_t> did_change_configuration_hooks;
llvm::StringMap<command_handler_t> command_handlers;
std::vector<Plugin> plugins;
};
} // namespace clice

3
include/Server/Utility.h Normal file
View File

@@ -0,0 +1,3 @@
#pragma once
#define bail(...) std::unexpected(std::format(__VA_ARGS__))

View File

@@ -149,7 +149,7 @@ inline std::string toPath(llvm::StringRef uri) {
llvm::SmallString<128> result;
if(auto err = fs::real_path(decoded, result)) {
std::println("Failed to get real path: {}, Input is {}\n", err.message(), decoded);
std::println(stderr, "Failed to get real path: {}, Input is {}\n", err.message(), decoded);
std::abort();
}

View File

@@ -70,6 +70,20 @@ struct Serde<std::nullopt_t> {
}
};
template <typename T>
struct Serde<std::optional<T>> {
static json::Value serialize(const std::optional<T>& v) {
return v ? json::serialize(*v) : json::Value(nullptr);
}
static std::optional<T> deserialize(const json::Value& value) {
if(value.kind() == json::Value::Null) {
return std::nullopt;
}
return json::deserialize<T>(value);
}
};
template <>
struct Serde<bool> {
static json::Value serialize(bool v) {

View File

@@ -22,6 +22,24 @@ def normalize_mode(value: str) -> str:
)
def copy_headers(project_root, install_prefix):
print("\nCopying internal Sema headers...")
clang_sema_dir = project_root / "clang/lib/Sema"
install_sema_dir = install_prefix / "include/clang/Sema"
install_sema_dir.mkdir(parents=True, exist_ok=True)
headers_to_copy = ["CoroutineStmtBuilder.h", "TypeLocBuilder.h", "TreeTransform.h"]
for header in headers_to_copy:
src = clang_sema_dir / header
dst = install_sema_dir / header
if src.exists():
shutil.copy(src, dst)
print(f" Copied {header}")
else:
print(f" Warning: {header} not found in source.")
def main():
parser = argparse.ArgumentParser(
description="Build LLVM with specific configurations."
@@ -293,21 +311,7 @@ def main():
print("Build failed!")
sys.exit(1)
print("\nCopying internal Sema headers...")
clang_sema_dir = project_root / "clang/lib/Sema"
install_sema_dir = install_prefix / "include/clang/Sema"
install_sema_dir.mkdir(parents=True, exist_ok=True)
headers_to_copy = ["CoroutineStmtBuilder.h", "TypeLocBuilder.h", "TreeTransform.h"]
for header in headers_to_copy:
src = clang_sema_dir / header
dst = install_sema_dir / header
if src.exists():
shutil.copy(src, dst)
print(f" Copied {header}")
else:
print(f" Warning: {header} not found in source.")
copy_headers(project_root, install_prefix)
def human_readable(num: int) -> str:
for unit in ["B", "KB", "MB", "GB"]:

98
scripts/plugin-def.py Normal file
View File

@@ -0,0 +1,98 @@
import hashlib
import re
from pathlib import Path
import sys
clice_apis = []
# all of the files in `include/`, other than `include/Server/Plugin.h`
for path in Path("include/").glob("**/*.h"):
if path.name == "Plugin.h":
continue
clice_apis.append(path)
clice_apis = [
# the dependencies of the clice.
Path("config/llvm-manifest.json"),
# the clice C/C++ sources.
*sorted(clice_apis),
]
def fatal(message: str):
print(f"error: {message}", file=sys.stderr)
sys.exit(1)
def sha256sum(paths: list[Path]) -> str:
digest = hashlib.sha256()
for path in paths:
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def clice_api_content():
content = []
content.append("#if 0")
for path in clice_apis:
with path.open("r") as file:
content.append(f"// begin of {path}")
content.append(file.read())
content.append(f"// end of {path}")
content.append("#endif")
return "\n".join(content)
def plugin_def_hash():
hash_val = sha256sum(clice_apis)
return f"sha256:{hash_val}"
def update():
hash_val = plugin_def_hash()
with open("include/Server/Plugin.h", "r") as file:
content = file.read()
content = re.sub(
r"#define CLICE_PLUGIN_DEF_HASH .*",
f'#define CLICE_PLUGIN_DEF_HASH "{hash_val}"',
content,
)
with open("include/Server/Plugin.h", "w") as file:
file.write(content)
def check():
hash_val = plugin_def_hash()
with open("include/Server/Plugin.h", "r") as file:
content = file.read()
match = re.search(r'#define CLICE_PLUGIN_DEF_HASH "(.*)"', content)
if match is None:
fatal("plugin def hash not found in include/Server/Plugin.h")
if match.group(1) != hash_val:
fatal(
f"plugin def hash mismatch in include/Server/Plugin.h, expected: {hash_val}, actual: {match.group(1)}"
)
print(f"plugin def hash is up to date: {hash_val}")
def main():
if len(sys.argv) > 1:
if sys.argv[1] == "update":
update()
check()
elif sys.argv[1] == "check":
check()
elif sys.argv[1] == "content":
print(clice_api_content())
else:
fatal(
f"invalid command: {sys.argv[1]}, expected: update, check",
)
else:
fatal("no command provided, expected: update, check")
if __name__ == "__main__":
main()

View File

@@ -643,11 +643,24 @@ std::vector<UpdateInfo> CompilationDatabase::load_compile_database(llvm::StringR
llvm::BumpPtrAllocator local;
llvm::StringSaver saver(local);
llvm::SmallVector<const char*, 32> agrs;
auto hasCC1 = false;
for(auto& argument: *arguments) {
if(argument.kind() == json::Value::String) {
agrs.emplace_back(saver.save(*argument.getAsString()).data());
if(argument.getAsString() == "-cc1") {
hasCC1 = true;
break;
}
}
}
if(hasCC1) {
std::println(stderr, "cannot handle arguments: {}", *file);
for(auto& arg: *arguments) {
std::print(stderr, "{} ", arg);
}
std::println(stderr, "");
continue;
}
item.info = self->save_compilation_info(*file, *directory, agrs);
} else if(command) {
item.info = self->save_compilation_info(*file, *directory, *command);
@@ -762,10 +775,10 @@ std::optional<std::uint32_t> CompilationDatabase::get_option_id(llvm::StringRef
}
}
std::vector<const char*> CompilationDatabase::files() {
std::vector<const char*> result;
std::vector<llvm::StringRef> CompilationDatabase::files() {
std::vector<llvm::StringRef> result;
for(auto& [file, _]: self->files) {
result.emplace_back(self->strings.get(file).data());
result.emplace_back(self->strings.get(file));
}
return result;
}

View File

@@ -287,7 +287,7 @@ CompilerFamily driver_family(llvm::StringRef driver) {
std::vector<const char*> query_toolchain(const QueryParams& params) {
auto arguments = params.arguments;
llvm::StringRef driver = arguments[0];
llvm::StringRef driver = "clang"; // arguments[0];
/// Note: The name used to invoke the compiler driver affects its behavior.
/// For example, `/usr/bin/clang++` is often a symbolic link to

View File

@@ -249,7 +249,7 @@ async::Task<bool> Server::build_pch(std::string file, std::string content) {
auto info = database.lookup(file, options);
auto bound = compute_preamble_bound(content);
auto& open_file = opening_files.get_or_add(file);
auto open_file = opening_files.get_or_add(file);
/// Check update ...
if(open_file->pch && !check_pch_update(content, bound, info, *open_file->pch)) {
@@ -347,7 +347,7 @@ async::Task<> Server::build_ast(std::string path, std::string content) {
}
async::Task<std::shared_ptr<OpenFile>> Server::add_document(std::string path, std::string content) {
auto& openFile = opening_files.get_or_add(path);
auto openFile = opening_files.get_or_add(path);
openFile->version += 1;
openFile->content = content;

19
src/Server/Implement.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "Server/Plugin.h"
#include "Server/Server.h"
namespace clice {
struct ServerRef::Self {
public:
Self(Server* server_instance) : server_instance(server_instance) {}
Server& server() const {
return *server_instance;
}
Server* server_instance;
};
} // namespace clice

View File

@@ -80,13 +80,26 @@ async::Task<> Indexer::schedule_next() {
co_await workings[i];
workings[i].release().destroy();
finish_cnt += 1;
finish_event.set();
}
}
async::Task<> Indexer::index_all() {
waitings.clear();
workings.clear();
update_event.clear();
finish_event.clear();
finish_cnt = 0;
for(auto& file: database.files()) {
waitings.push_back(project_index.path_pool.path_id(file));
}
auto to_finish = waitings.size();
if(to_finish == 0) {
co_return;
}
auto max_count = std::max(std::thread::hardware_concurrency(), 4u);
@@ -100,6 +113,11 @@ async::Task<> Indexer::index_all() {
task.dispose();
}
while(finish_cnt < to_finish) {
co_await finish_event;
finish_event.unset();
}
co_return;
}

View File

@@ -1,3 +1,4 @@
#include "Server/Plugin.h"
#include "Server/Server.h"
#include <llvm/Support/FileSystem.h>
@@ -46,6 +47,11 @@ async::Task<json::Value> Server::on_initialize(proto::InitializeParams params) {
}
}
/// Run initialize hooks.
for(auto& hook: initialize_hooks) {
co_await hook();
}
/// Load cache info.
load_cache_info();
@@ -108,18 +114,41 @@ async::Task<json::Value> Server::on_initialize(proto::InitializeParams params) {
}
async::Task<> Server::on_initialized(proto::InitializedParams) {
// emit indexing notification
co_await notify("clice/willIndex", {});
/// Run initialized hooks.
for(auto& hook: initialized_hooks) {
co_await hook();
}
indexer.load_from_disk();
co_await indexer.index_all();
// emit indexed notification
co_await notify("clice/didIndex", {});
co_return;
}
async::Task<json::Value> Server::on_shutdown(proto::ShutdownParams params) {
/// Run shutdown hooks.
for(auto& hook: shutdown_hooks) {
co_await hook();
}
co_return json::Value(nullptr);
}
async::Task<> Server::on_exit(proto::ExitParams params) {
/// Run exit hooks.
for(auto& hook: exit_hooks) {
co_await hook();
}
save_cache_info();
indexer.save_to_disk();
async::stop();
co_return;
}

163
src/Server/Plugin.cpp Normal file
View File

@@ -0,0 +1,163 @@
#include "Server/Plugin.h"
#include "Implement.h"
#include "Server/Utility.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/Support/DynamicLibrary.h"
namespace clice {
Server& ServerRef::server() const {
return *self;
}
struct Plugin::Self {
/// The file path of the plugin.
std::string file_path;
/// The dynamic library data of the plugin.
llvm::sys::DynamicLibrary library;
/// The name of the plugin.
std::string name;
/// The version of the plugin.
std::string version;
/// Registers the server callbacks for the loaded plugin.
void (*register_server_callbacks)(ServerPluginBuilder& builder);
};
std::expected<Plugin, std::string> Plugin::load(const std::string& file_path) {
std::string err;
auto library = llvm::sys::DynamicLibrary::getPermanentLibrary(file_path.c_str(), &err);
if(!library.isValid()) {
return bail("Could not load library '{}': {}", file_path, err);
}
Plugin P{
/// We currently never destroy plugins, so this is not a memory leak.
new Self{file_path, library}
};
/// `clice_get_server_plugin_info` should be resolved to the definition from the plugin
/// we are currently loading.
intptr_t get_details_fn = (intptr_t)library.getAddressOfSymbol("clice_get_server_plugin_info");
if(!get_details_fn) {
return bail(
"The symbol `clice_get_server_plugin_info` is not found in '{}'. Is this a clice server plugin?",
file_path);
}
auto info = reinterpret_cast<decltype(clice_get_server_plugin_info)*>(get_details_fn)();
/// First, we check whether the plugin is compatible with the clice plugin API.
if(info.api_version != CLICE_PLUGIN_API_VERSION) {
return bail("Wrong API version on plugin '{}'. Got version {}. Supported version is {}.",
file_path,
info.api_version,
CLICE_PLUGIN_API_VERSION);
}
/// Then, we safely get definition hash from the plugin, and check if it is consistent with
/// the expected hash. This ensures that the plugin has consistent declarations with the
/// server.
std::string definition_hash = info.definition_hash;
if(plugin_definition_hash.size() != definition_hash.size()) {
return bail("Wrong definition hash size on plugin '{}'. Got {}, expected {} ({}).",
file_path,
definition_hash.size(),
plugin_definition_hash.size(),
plugin_definition_hash);
}
/// If there is any non-printable character in the definition hash, this is likely a bug in the
/// plugin. We cannot even print the `definition_hash` in this case.
if(std::ranges::any_of(definition_hash, [](char c) { return !std::isprint(c); })) {
return bail("Corrupt definition hash on plugin '{}'. This is likely a bug in the plugin.",
file_path);
}
if(definition_hash != CLICE_PLUGIN_DEF_HASH) {
return bail("Wrong definition hash on plugin '{}'. Got '{}', expected '{}'.",
file_path,
definition_hash,
CLICE_PLUGIN_DEF_HASH);
}
/// A plugin must implement the `register_server_callbacks` function.
if(!info.register_server_callbacks) {
return bail("Empty `register_server_callbacks` function in plugin '{}'.", file_path);
}
P->name = info.name;
P->version = info.version;
P->register_server_callbacks = info.register_server_callbacks;
return P;
}
llvm::StringRef Plugin::file_path() const {
return self->file_path;
}
llvm::StringRef Plugin::name() const {
return self->name;
}
llvm::StringRef Plugin::version() const {
return self->version;
}
/// Registers the server callbacks for the loaded plugin.
void Plugin::register_server_callbacks(ServerPluginBuilder& builder) const {
self->register_server_callbacks(builder);
}
using command_handler_t =
async::Task<llvm::json::Value> (*)(ServerRef server,
const llvm::ArrayRef<llvm::json::Value>& arguments);
void ServerPluginBuilder::on_initialize(void* plugin_data, lifecycle_hook_t callback) {
auto server = server_ref;
server_ref.server().initialize_hooks.push_back(
[=]() -> async::Task<> { co_await callback(server, plugin_data); });
}
void ServerPluginBuilder::on_initialized(void* plugin_data, lifecycle_hook_t callback) {
auto server = server_ref;
server_ref.server().initialized_hooks.push_back(
[=]() -> async::Task<> { co_await callback(server, plugin_data); });
}
void ServerPluginBuilder::on_shutdown(void* plugin_data, lifecycle_hook_t callback) {
auto server = server_ref;
server_ref.server().shutdown_hooks.push_back(
[=]() -> async::Task<> { co_await callback(server, plugin_data); });
}
void ServerPluginBuilder::on_exit(void* plugin_data, lifecycle_hook_t callback) {
auto server = server_ref;
server_ref.server().exit_hooks.push_back(
[=]() -> async::Task<> { co_await callback(server, plugin_data); });
}
void ServerPluginBuilder::on_did_change_configuration(void* plugin_data,
lifecycle_hook_t callback) {
auto server = server_ref;
server_ref.server().did_change_configuration_hooks.push_back(
[=]() -> async::Task<> { co_await callback(server, plugin_data); });
}
void ServerPluginBuilder::register_commmand_handler(void* plugin_data,
llvm::StringRef command,
command_handler_t callback) {
auto server = server_ref;
auto [_, inserted] = server_ref.server().command_handlers.try_emplace(
command,
[=](llvm::ArrayRef<llvm::json::Value> arguments) -> async::Task<llvm::json::Value> {
return callback(server, plugin_data, arguments);
});
if(!inserted) {
LOG_ERROR("Command handler already registered for command '{}'.", command);
}
}
} // namespace clice

View File

@@ -1,11 +1,15 @@
#include "Server/Server.h"
#include "Compiler/CompilationUnit.h"
#include "Protocol/Basic.h"
#include "Support/FileSystem.h"
#include "Support/Logging.h"
#include <clang/AST/Decl.h>
namespace clice {
ActiveFileManager::ActiveFile& ActiveFileManager::lru_put_impl(llvm::StringRef path,
OpenFile file) {
ActiveFileManager::ActiveFile ActiveFileManager::lru_put_impl(llvm::StringRef path, OpenFile file) {
/// If the file is not in the chain, create a new OpenFile.
if(items.size() >= capability) {
/// If the size exceeds the maximum size, remove the last element.
@@ -21,7 +25,7 @@ ActiveFileManager::ActiveFile& ActiveFileManager::lru_put_impl(llvm::StringRef p
return items.front().second;
}
ActiveFileManager::ActiveFile& ActiveFileManager::get_or_add(llvm::StringRef path) {
ActiveFileManager::ActiveFile ActiveFileManager::get_or_add(llvm::StringRef path) {
auto iter = index.find(path);
if(iter == index.end()) {
return lru_put_impl(path, OpenFile{});
@@ -32,7 +36,7 @@ ActiveFileManager::ActiveFile& ActiveFileManager::get_or_add(llvm::StringRef pat
return iter->second->second;
}
ActiveFileManager::ActiveFile& ActiveFileManager::add(llvm::StringRef path, OpenFile file) {
ActiveFileManager::ActiveFile ActiveFileManager::add(llvm::StringRef path, OpenFile file) {
auto iter = index.find(path);
if(iter == index.end()) {
return lru_put_impl(path, std::move(file));
@@ -102,6 +106,8 @@ Server::Server() : indexer(database, config, kind) {
register_callback<&Server::on_shutdown>("shutdown");
register_callback<&Server::on_exit>("exit");
register_callback<&Server::on_execute_command>("workspace/executeCommand");
register_callback<&Server::on_did_open>("textDocument/didOpen");
register_callback<&Server::on_did_change>("textDocument/didChange");
register_callback<&Server::on_did_save>("textDocument/didSave");
@@ -184,4 +190,18 @@ async::Task<> Server::on_receive(json::Value value) {
co_return;
}
async::Task<json::Value> Server::on_execute_command(proto::ExecuteCommandParams params) {
auto& command = params.command;
auto& arguments = params.arguments;
auto it = command_handlers.find(command);
if(it == command_handlers.end()) {
LOG_ERROR("Command handler not found for command '{}'.", command);
co_return json::Value{};
}
auto& handler = it->second;
co_return co_await handler(arguments);
}
} // namespace clice

View File

@@ -6,6 +6,7 @@
#include "spdlog/sinks/ringbuffer_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/stdout_sinks.h"
#include "llvm/Support/raw_ostream.h"
namespace clice::logging {
@@ -34,26 +35,43 @@ void stderr_logger(std::string_view name, const Options& options) {
}
void file_loggger(std::string_view name, std::string_view dir, const Options& options) {
auto now = std::chrono::system_clock::now();
auto filename = std::format("{:%Y-%m-%d_%H-%M-%S}.log", now);
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path::join(dir, filename));
if(options.replay_console && ringbuffer_sink) {
sink->set_level(options.level);
sink->set_pattern(pattern);
for(auto& log: ringbuffer_sink->last_raw()) {
sink->log(log);
}
ringbuffer_sink.reset();
if(auto err = fs::create_directories(dir)) {
spdlog::error("Failed to create log directory {}: {}", std::string(dir), err.message());
return;
}
auto logger = std::make_shared<spdlog::logger>(std::string(name), std::move(sink));
auto now = std::chrono::system_clock::now();
auto filename = std::format("{:%Y-%m-%d_%H-%M-%S}.log", now);
auto filepath = path::join(dir, filename);
{
std::error_code err;
llvm::raw_fd_ostream test(filepath, err, fs::OF_Append);
if(err) {
spdlog::error("Failed to open log file {}: {}", filepath, err.message());
return;
}
}
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(filepath);
auto console_sink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>(options.color);
std::array<spdlog::sink_ptr, 2> sinks = {file_sink, console_sink};
auto logger = std::make_shared<spdlog::logger>(std::string(name), sinks.begin(), sinks.end());
logger->set_level(options.level);
logger->set_pattern(pattern);
logger->flush_on(Level::trace);
spdlog::set_default_logger(std::move(logger));
if(options.replay_console && ringbuffer_sink) {
file_sink->set_level(options.level);
file_sink->set_pattern(pattern);
for(auto& log: ringbuffer_sink->last_raw()) {
file_sink->log(log);
}
ringbuffer_sink.reset();
}
}
} // namespace clice::logging

View File

@@ -1,3 +1,5 @@
#include "Server/Implement.h"
#include "Server/Plugin.h"
#include "Server/Server.h"
#include "Server/Version.h"
#include "Support/Format.h"
@@ -11,6 +13,12 @@
namespace cl = llvm::cl;
using namespace clice;
namespace clice {
void register_builtin_server_plugins(ServerPluginBuilder& builder);
}
namespace {
static cl::OptionCategory category("clice options");
@@ -73,6 +81,31 @@ cl::opt<logging::Level> log_level{
cl::desc("The log level, default is info"),
};
cl::list<std::string> plugin_paths{
"plugin-path",
cl::cat(category),
cl::value_desc("path1,path2,path3,..."),
cl::desc("The server plugins to load"),
cl::CommaSeparated,
};
/// Loads plugins intermediatly.
std::vector<Plugin> load_plugins(Server& instance) {
ServerPluginBuilder builder{ServerRef{&instance}};
std::vector<Plugin> plugins;
for(auto& plugin_path: plugin_paths) {
auto plugin_instance = Plugin::load(plugin_path);
if(!plugin_instance) {
LOG_FATAL("Failed to load plugin {}: {}", plugin_path, plugin_instance.error());
}
plugin_instance->register_server_callbacks(builder);
plugins.push_back(std::move(plugin_instance.value()));
}
::clice::register_builtin_server_plugins(builder);
// The llvm::sys::DynamicLibrary will be unloaded when the program exits.
return plugins;
}
} // namespace
int main(int argc, const char** argv) {
@@ -105,6 +138,10 @@ int main(int argc, const char** argv) {
/// The global server instance.
static Server instance;
/// Loads plugins intermediatly before the server starts.
static auto plugins = load_plugins(instance);
auto loop = [&](json::Value value) -> async::Task<> {
co_await instance.on_receive(value);
};

View File

@@ -32,12 +32,12 @@ TEST_CASE(LruAlgorithm) {
ASSERT_EQ(actives.size(), 0U);
auto& first = actives.add("first", OpenFile{.version = 1});
auto first = actives.add("first", OpenFile{.version = 1});
ASSERT_EQ(actives.size(), 1U);
ASSERT_TRUE(actives.contains("first"));
ASSERT_EQ(first->version, 1U);
auto& second = actives.add("second", OpenFile{.version = 2});
auto second = actives.add("second", OpenFile{.version = 2});
ASSERT_EQ(actives.size(), 1U);
}
@@ -82,7 +82,7 @@ TEST_CASE(IteratorCheck) {
std::string fpath = std::format("{}", i);
OpenFile object{.version = i};
auto& inseted = manager.add(fpath, std::move(object));
auto inseted = manager.add(fpath, std::move(object));
std::optional new_added_entry = manager.get_or_add(fpath);
ASSERT_TRUE(new_added_entry.has_value());
auto new_added = std::move(new_added_entry).value();