1 Commits

Author SHA1 Message Date
Myriad-Dreamin
183b90d572 Add OpenSpec proposal for folding range pipeline split 2026-04-24 00:13:04 +08:00
98 changed files with 1301 additions and 5583 deletions

View File

@@ -1,40 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: ""
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,20 +0,0 @@
name: "Setup Pixi"
description: "setup pixi"
inputs:
environments:
description: "The pixi environments to install (e.g. default, docs, test)"
required: false
default: "default"
runs:
using: "composite"
steps:
- name: Setup Pixi
uses: prefix-dev/setup-pixi@v0.9.3
with:
pixi-version: v0.67.0
environments: ${{ inputs.environments }}
activate-environment: true
cache: true
locked: true

View File

@@ -1,44 +0,0 @@
name: benchmark
on:
workflow_dispatch:
jobs:
benchmark:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
- name: Build scan_benchmark
run: |
pixi run cmake-config RelWithDebInfo ON -- -DCLICE_ENABLE_BENCHMARK=ON
cmake --build build/RelWithDebInfo --target scan_benchmark
- name: Clone LLVM
run: git clone --depth 1 https://github.com/llvm/llvm-project.git
- name: Generate CDB
run: |
cmake -B llvm-build -G Ninja \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_TOOLCHAIN_FILE="$(pwd)/cmake/toolchain.cmake" \
-DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lld;lldb;mlir;polly;flang;bolt" \
-DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind" \
llvm-project/llvm
- name: Run benchmark
run: ./build/RelWithDebInfo/bin/scan_benchmark --runs 20 llvm-build/compile_commands.json
- name: Stop sccache server
if: runner.os == 'Windows'
run: pixi run -- sccache --stop-server || true

View File

@@ -1,446 +0,0 @@
name: build llvm
on:
workflow_dispatch:
inputs:
llvm_version:
description: "LLVM version to build (e.g., 21.1.8)"
required: true
type: string
skip_upload:
description: "Skip upload and PR creation (build-only mode)"
required: false
type: boolean
default: false
skip_pr:
description: "Skip PR creation (upload only, no PR)"
required: false
type: boolean
default: false
pull_request:
# if you want to run this workflow, change the branch name to main,
# if you want to turn off it, change it to non existent branch.
branches: [main-turn-off]
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
# Native builds
- os: windows-2025
llvm_mode: RelWithDebInfo
lto: OFF
- os: windows-2025
llvm_mode: RelWithDebInfo
lto: ON
- os: ubuntu-24.04
llvm_mode: Debug
lto: OFF
- os: ubuntu-24.04
llvm_mode: RelWithDebInfo
lto: OFF
- os: ubuntu-24.04
llvm_mode: RelWithDebInfo
lto: ON
- os: macos-15
llvm_mode: Debug
lto: OFF
- os: macos-15
llvm_mode: RelWithDebInfo
lto: OFF
- os: macos-15
llvm_mode: RelWithDebInfo
lto: ON
# Cross-compilation builds
# macOS x64 (from arm64 macos-15)
- os: macos-15
llvm_mode: RelWithDebInfo
lto: OFF
target_triple: x86_64-apple-darwin
- os: macos-15
llvm_mode: RelWithDebInfo
lto: ON
target_triple: x86_64-apple-darwin
# Linux aarch64 (from x64 ubuntu-24.04)
- os: ubuntu-24.04
llvm_mode: RelWithDebInfo
lto: OFF
target_triple: aarch64-linux-gnu
pixi_env: cross-linux-aarch64
- os: ubuntu-24.04
llvm_mode: RelWithDebInfo
lto: ON
target_triple: aarch64-linux-gnu
pixi_env: cross-linux-aarch64
# Windows arm64 (from x64 windows-2025)
- os: windows-2025
llvm_mode: RelWithDebInfo
lto: OFF
target_triple: aarch64-pc-windows-msvc
pixi_env: cross-windows-arm64
- os: windows-2025
llvm_mode: RelWithDebInfo
lto: ON
target_triple: aarch64-pc-windows-msvc
pixi_env: cross-windows-arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Free Disk Space
if: runner.os == 'Linux'
uses: jlumbroso/free-disk-space@main
- name: Increase Swap Space
if: runner.os == 'Linux'
run: |
echo "===== Initial Status ====="
sudo swapon --show
free -h
echo "===== Creating Swap File ====="
sudo swapoff -a
sudo fallocate -l 16G /mnt/swapfile
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
echo "===== Final Status ====="
sudo swapon --show
free -h
df -h
- uses: ./.github/actions/setup-pixi
with:
environments: ${{ matrix.pixi_env || 'package' }}
- name: Clone llvm-project
shell: bash
run: |
VERSION="${{ inputs.llvm_version || '21.1.8' }}"
echo "Cloning LLVM ${VERSION}..."
git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm
- name: Validate distribution components
shell: bash
run: |
python3 scripts/validate-llvm-components.py \
--llvm-src=.llvm \
--components-file=scripts/llvm-components.json
- name: Build LLVM (install-distribution)
shell: bash
run: |
ENV="${{ matrix.pixi_env || 'package' }}"
EXTRA_ARGS=""
if [[ -n "${{ matrix.target_triple }}" ]]; then
EXTRA_ARGS="--target-triple=${{ matrix.target_triple }}"
fi
pixi run -e "$ENV" build-llvm \
--llvm-src=.llvm \
--mode="${{ matrix.llvm_mode }}" \
--lto="${{ matrix.lto }}" \
--build-dir=build \
${EXTRA_ARGS}
- name: Build clice using installed LLVM
if: ${{ !matrix.target_triple }}
shell: bash
run: |
pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \
"-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \
"-DLLVM_INSTALL_PATH=.llvm/build-install"
pixi run cmake-build ${{ matrix.llvm_mode }}
- name: Build clice using installed LLVM (cross-compile)
if: ${{ matrix.target_triple }}
shell: bash
run: |
ENV="${{ matrix.pixi_env || 'package' }}"
pixi run -e "$ENV" cmake-config ${{ matrix.llvm_mode }} ON -- \
"-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \
"-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" \
"-DLLVM_INSTALL_PATH=.llvm/build-install"
pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }}
- name: Verify cross-compiled binary architecture
if: ${{ matrix.target_triple && runner.os != 'Windows' }}
shell: bash
run: |
BINARY="build/${{ matrix.llvm_mode }}/bin/clice"
echo "Binary info:"
file "$BINARY"
case "${{ matrix.target_triple }}" in
aarch64-linux-gnu) file "$BINARY" | grep -q "aarch64" ;;
x86_64-apple-darwin) file "$BINARY" | grep -q "x86_64" ;;
esac
- name: Upload cross-compiled clice for functional test
if: ${{ matrix.target_triple && matrix.lto == 'OFF' }}
uses: actions/upload-artifact@v4
with:
name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }}
path: |
build/${{ matrix.llvm_mode }}/bin/
build/${{ matrix.llvm_mode }}/lib/
if-no-files-found: error
retention-days: 1
- name: Run tests
if: ${{ !matrix.target_triple }}
shell: bash
run: pixi run test ${{ matrix.llvm_mode }}
# Prune is only supported for native builds (requires linking clice to test).
# Cross-compiled targets reuse the native prune manifest of the same OS.
- name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO)
if: (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'))
shell: bash
run: |
MANIFEST="pruned-libs-${{ matrix.os }}.json"
echo "LLVM_PRUNED_MANIFEST=${MANIFEST}" >> "${GITHUB_ENV}"
python3 scripts/prune-llvm-bin.py \
--action discover \
--install-dir ".llvm/build-install/lib" \
--build-dir "build/${{ matrix.llvm_mode }}" \
--max-attempts 60 \
--sleep-seconds 60 \
--manifest "${MANIFEST}"
- name: Upload pruned-libs manifest
if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'
uses: actions/upload-artifact@v4
with:
name: llvm-pruned-libs-${{ matrix.os }}
path: ${{ env.LLVM_PRUNED_MANIFEST }}
if-no-files-found: error
compression-level: 0
- name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only)
if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
MANIFEST="pruned-libs-${{ matrix.os }}.json"
python3 scripts/prune-llvm-bin.py \
--action apply \
--manifest "${MANIFEST}" \
--install-dir ".llvm/build-install/lib" \
--build-dir "build/${{ matrix.llvm_mode }}" \
--gh-run-id "${{ github.run_id }}" \
--gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \
--gh-download-dir "artifacts" \
--max-attempts 60 \
--sleep-seconds 60
# For cross-compiled LTO builds, apply the native prune manifest.
# The unused library set is arch-independent (same API surface).
- name: Apply pruned-libs manifest (cross-compile + LTO)
if: matrix.target_triple && matrix.lto == 'ON'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
MANIFEST="pruned-libs-${{ matrix.os }}.json"
python3 scripts/prune-llvm-bin.py \
--action apply \
--manifest "${MANIFEST}" \
--install-dir ".llvm/build-install/lib" \
--build-dir "build/${{ matrix.llvm_mode }}" \
--gh-run-id "${{ github.run_id }}" \
--gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \
--gh-download-dir "artifacts" \
--max-attempts 60 \
--sleep-seconds 60
- name: Package LLVM install directory
shell: bash
run: |
MODE_TAG="releasedbg"
if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then
MODE_TAG="debug"
fi
# Determine arch/platform/toolchain from target triple or runner OS
if [[ -n "${{ matrix.target_triple }}" ]]; then
case "${{ matrix.target_triple }}" in
x86_64-apple-darwin)
ARCH="x64"; PLATFORM="macos"; TOOLCHAIN="clang" ;;
aarch64-linux-gnu)
ARCH="aarch64"; PLATFORM="linux"; TOOLCHAIN="gnu" ;;
aarch64-pc-windows-msvc)
ARCH="aarch64"; PLATFORM="windows"; TOOLCHAIN="msvc" ;;
esac
else
ARCH="x64"
PLATFORM="linux"
TOOLCHAIN="gnu"
if [[ "${{ matrix.os }}" == windows-* ]]; then
PLATFORM="windows"
TOOLCHAIN="msvc"
elif [[ "${{ matrix.os }}" == macos-* ]]; then
ARCH="arm64"
PLATFORM="macos"
TOOLCHAIN="clang"
fi
fi
SUFFIX=""
if [[ "${{ matrix.lto }}" == "ON" ]]; then
SUFFIX="-lto"
fi
if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then
SUFFIX="${SUFFIX}-asan"
fi
ARCHIVE="${ARCH}-${PLATFORM}-${TOOLCHAIN}-${MODE_TAG}${SUFFIX}.tar.xz"
set -eo pipefail
tar -C .llvm -cf - build-install | xz -T0 -9 -c > "${ARCHIVE}"
echo "LLVM_INSTALL_ARCHIVE=${ARCHIVE}" >> "${GITHUB_ENV}"
- name: Upload LLVM install artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.LLVM_INSTALL_ARCHIVE }}
path: ${{ env.LLVM_INSTALL_ARCHIVE }}
if-no-files-found: error
test-cross:
needs: build
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
llvm_mode: RelWithDebInfo
target_triple: x86_64-apple-darwin
- os: ubuntu-24.04-arm
llvm_mode: RelWithDebInfo
target_triple: aarch64-linux-gnu
- os: windows-11-arm
llvm_mode: RelWithDebInfo
target_triple: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: test-run
- name: Download cross-compiled clice
uses: actions/download-artifact@v4
with:
name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }}
path: build/${{ matrix.llvm_mode }}/
- name: Make binaries executable
if: runner.os != 'Windows'
run: chmod +x build/${{ matrix.llvm_mode }}/bin/*
- name: Run tests
run: pixi run -e test-run test ${{ matrix.llvm_mode }}
upload:
needs: build
if: ${{ !cancelled() && inputs.llvm_version && !inputs.skip_upload }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Download all build artifacts
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-llvm.sh "${{ github.run_id }}"
- name: Upload to clice-llvm
env:
GH_TOKEN: ${{ secrets.UPLOAD_LLVM }}
TARGET_REPO: clice-io/clice-llvm
run: python3 scripts/upload-llvm.py "${{ inputs.llvm_version }}" "${TARGET_REPO}" "${{ github.run_id }}"
- name: Save manifest for update-clice job
uses: actions/upload-artifact@v4
with:
name: llvm-manifest-final
path: artifacts/llvm-manifest.json
if-no-files-found: error
compression-level: 0
update-clice:
needs: upload
if: ${{ !inputs.skip_pr }}
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Download manifest
uses: actions/download-artifact@v4
with:
name: llvm-manifest-final
path: .
- name: Update manifest and version
run: |
python3 scripts/update-llvm-version.py \
--version "${{ inputs.llvm_version }}" \
--manifest-src llvm-manifest.json \
--manifest-dest config/llvm-manifest.json \
--package-cmake cmake/package.cmake
- name: Create or update PR
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ inputs.llvm_version }}"
BRANCH="chore/update-llvm-${VERSION}"
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "${BRANCH}"
git add config/llvm-manifest.json cmake/package.cmake
git commit -m "chore: update LLVM to ${VERSION}"
git push --force-with-lease origin "${BRANCH}"
# Check if PR already exists for this branch
EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty')
BODY="$(cat <<EOF
## Summary
- Update LLVM prebuilt binaries to version ${VERSION}
- Updated \`config/llvm-manifest.json\` with new SHA256 hashes
- Updated \`cmake/package.cmake\` version string
**Artifacts:** [clice-llvm release](${RELEASE_URL})
**Build:** [workflow run](${RUN_URL})
> Auto-generated by build-llvm workflow
EOF
)"
if [[ -n "${EXISTING_PR}" ]]; then
echo "Updating existing PR #${EXISTING_PR}"
gh pr edit "${EXISTING_PR}" --body "${BODY}"
else
gh pr create \
--title "chore: update LLVM to ${VERSION}" \
--body "${BODY}" \
--base main
fi

View File

@@ -1,40 +0,0 @@
name: format
on:
workflow_call:
jobs:
check-format:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: format
- name: Validate update-llvm-version.py can still patch package.cmake
run: |
python3 scripts/update-llvm-version.py --check \
--manifest-dest config/llvm-manifest.json \
--package-cmake cmake/package.cmake
- name: Run formatter
run: pixi run format
continue-on-error: true
- name: Auto correct
uses: huacnlee/autocorrect-action@v2
with:
args: --lint ./docs
continue-on-error: true
- name: Check diff
run: |
if ! git diff --quiet; then
echo "::error::Formatting changes detected. Please run 'pixi run format' and commit the result."
git --no-pager diff --stat
git --no-pager diff
exit 1
fi

View File

@@ -1,30 +0,0 @@
name: deploy
on:
workflow_call:
jobs:
deploy-docs:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: node
- name: Build docs
run: pixi run build-docs
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
if: github.ref == 'refs/heads/main'
with:
personal_token: ${{ secrets.PUBLISH_DOCS }}
external_repository: clice-io/docs
publish_dir: ./docs/.vitepress/dist
destination_dir: clice
keep_files: true

View File

@@ -1,137 +0,0 @@
name: main
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changes:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
format: ${{ steps.filter.outputs.format }}
docs: ${{ steps.filter.outputs.docs }}
clice: ${{ steps.filter.outputs.clice }}
vscode: ${{ steps.filter.outputs.vscode }}
cmake: ${{ steps.filter.outputs.cmake }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
format:
- '**/*.{h,c,cpp,hpp,ts,js,lua,md,yml,yaml}'
docs:
- 'docs/**'
- '.github/workflows/deploy-docs.yml'
clice:
- 'src/**'
- 'include/**'
- 'CMakeLists.txt'
- '.github/workflows/publish-clice.yml'
vscode:
- 'editors/vscode/**'
- '.github/workflows/publish-vscode.yml'
cmake:
- 'CMakeLists.txt'
- 'src/**'
- 'include/**'
- 'tests/**'
- 'config/**'
- '.github/workflows/test-cmake.yml'
conventional-commit:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-latest
steps:
- name: Check conventional commit format
env:
IS_PR: ${{ github.event_name == 'pull_request' }}
PR_TITLE: ${{ github.event.pull_request.title }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
pattern='^(feat|fix|refactor|chore|build|ci|docs|test|perf|style|revert)(\(.+\))?: .+'
if [[ "$IS_PR" == "true" ]]; then
subject="$PR_TITLE"
label="PR title"
else
subject=$(echo "$COMMIT_MSG" | head -n1)
label="Commit message"
fi
if [[ ! "$subject" =~ $pattern ]]; then
echo "::error::$label must follow conventional commit format: type(scope)?: description"
echo " Valid types: feat, fix, refactor, chore, build, ci, docs, test, perf, style, revert"
echo " Got: '$subject'"
exit 1
fi
format:
needs: changes
if: ${{ needs.changes.outputs.format == 'true' }}
uses: ./.github/workflows/check-format.yml
deploy:
needs: changes
if: ${{ needs.changes.outputs.docs == 'true' }}
permissions:
contents: write
uses: ./.github/workflows/deploy-docs.yml
secrets: inherit
# clice:
# needs: changes
# if: ${{ needs.changes.outputs.clice == 'true' }}
# uses: ./.github/workflows/publish-clice.yml
vscode:
needs: changes
if: ${{ needs.changes.outputs.vscode == 'true' }}
uses: ./.github/workflows/publish-vscode.yml
cmake:
needs: changes
if: ${{ needs.changes.outputs.cmake == 'true' }}
uses: ./.github/workflows/test-cmake.yml
release-clice:
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/publish-clice.yml
secrets: inherit
release-vscode:
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/publish-vscode.yml
secrets: inherit
checks-passed:
if: ${{ always() && !startsWith(github.ref, 'refs/tags/') }}
needs:
- conventional-commit
- format
- deploy
# - clice
- vscode
- cmake
runs-on: ubuntu-latest
steps:
- name: Check results
uses: re-actors/alls-green@release/v1
with:
allowed-skips: conventional-commit,format,deploy,clice,vscode,cmake
jobs: ${{ toJSON(needs) }}

View File

@@ -1,100 +0,0 @@
name: clice
on:
workflow_call:
jobs:
publish-clice:
strategy:
fail-fast: false
matrix:
include:
# Native builds
- os: windows-2025
artifact_name: clice.zip
asset_name: clice-x64-windows-msvc.zip
symbol_artifact_name: clice-symbol.zip
symbol_asset_name: clice-x64-windows-msvc-symbol.zip
- os: ubuntu-24.04
artifact_name: clice.tar.gz
asset_name: clice-x86_64-linux-gnu.tar.gz
symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-x86_64-linux-gnu-symbol.tar.gz
- os: macos-15
artifact_name: clice.tar.gz
asset_name: clice-arm64-macos-darwin.tar.gz
symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-arm64-macos-darwin-symbol.tar.gz
# Cross-compilation builds
- os: macos-15
target_triple: x86_64-apple-darwin
pixi_env: cross-macos-x64
artifact_name: clice.tar.gz
asset_name: clice-x86_64-macos-darwin.tar.gz
symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-x86_64-macos-darwin-symbol.tar.gz
- os: ubuntu-24.04
target_triple: aarch64-linux-gnu
pixi_env: cross-linux-aarch64
artifact_name: clice.tar.gz
asset_name: clice-aarch64-linux-gnu.tar.gz
symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-aarch64-linux-gnu-symbol.tar.gz
- os: windows-2025
target_triple: aarch64-pc-windows-msvc
pixi_env: cross-windows-arm64
artifact_name: clice.zip
asset_name: clice-aarch64-windows-msvc.zip
symbol_artifact_name: clice-symbol.zip
symbol_asset_name: clice-aarch64-windows-msvc-symbol.zip
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: ${{ matrix.pixi_env || 'package' }}
- name: Package (native)
if: ${{ !matrix.target_triple }}
run: pixi run package
- name: Package (cross-compile)
if: ${{ matrix.target_triple }}
run: |
ENV="${{ matrix.pixi_env }}"
pixi run -e "$ENV" package-config -- \
"-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}"
pixi run -e "$ENV" cmake-build
- name: Upload Main Package to Release
if: startsWith(github.ref, 'refs/tags/v')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/RelWithDebInfo/${{ matrix.artifact_name }}
asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }}
overwrite: true
- name: Upload Symbol Package to Release
if: startsWith(github.ref, 'refs/tags/v')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/RelWithDebInfo/${{ matrix.symbol_artifact_name }}
asset_name: ${{ matrix.symbol_asset_name }}
tag: ${{ github.ref }}
overwrite: true

View File

@@ -1,38 +0,0 @@
name: vscode
on:
workflow_call:
jobs:
publish-vscode:
runs-on: ubuntu-latest
defaults:
run:
working-directory: editors/vscode
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: node
- name: Publish and Package to Marketplace
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
run: |
FLAG="${{ contains(github.ref_name, '-') && '--pre-release' || '' }}"
pixi run build-vscode $FLAG
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
pixi run publish-vscode -p "$VSCE_PAT" $FLAG
fi
- name: Upload .vsix to Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: "editors/vscode/*.vsix"
tag_name: ${{ github.ref }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,170 +0,0 @@
name: cmake
on:
workflow_call:
env:
CCACHE_DIR: ${{ github.workspace }}/.cache/ccache
SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache
CCACHE_BASEDIR: ${{ github.workspace }}
SCCACHE_BASEDIRS: ${{ github.workspace }}
CCACHE_COMPILERCHECK: content
CCACHE_MAXSIZE: 2G
SCCACHE_CACHE_SIZE: 2G
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
# Native builds
- os: windows-2025
build_type: RelWithDebInfo
- os: ubuntu-24.04
build_type: Debug
- os: ubuntu-24.04
build_type: RelWithDebInfo
- os: macos-15
build_type: Debug
- os: macos-15
build_type: RelWithDebInfo
# Cross-compile (build only; tests run on native runners)
- os: macos-15
build_type: RelWithDebInfo
target_triple: x86_64-apple-darwin
build_only: true
- os: ubuntu-24.04
build_type: RelWithDebInfo
target_triple: aarch64-linux-gnu
build_only: true
pixi_env: cross-linux-aarch64
- os: windows-2025
build_type: RelWithDebInfo
target_triple: aarch64-pc-windows-msvc
build_only: true
pixi_env: cross-windows-arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: ${{ matrix.pixi_env || 'default' }}
- name: Restore compiler cache
uses: actions/cache@v4
with:
path: ${{ runner.os == 'Windows' && '.cache/sccache' || '.cache/ccache' }}
key: ${{ runner.os }}-${{ matrix.build_type }}-${{ matrix.target_triple || 'native' }}-ccache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-${{ matrix.build_type }}-${{ matrix.target_triple || 'native' }}-ccache-
- name: Zero cache stats
run: |
ENV="${{ matrix.pixi_env || 'default' }}"
if [ "$RUNNER_OS" = "Windows" ]; then
pixi run -e "$ENV" -- sccache --stop-server || true
pixi run -e "$ENV" -- sccache --zero-stats || true
else
pixi run -e "$ENV" -- ccache --zero-stats || true
fi
shell: bash
- name: Build (native)
if: ${{ !matrix.target_triple }}
run: pixi run build ${{ matrix.build_type }} ON
- name: Build (cross-compile)
if: ${{ matrix.target_triple }}
shell: bash
run: |
ENV="${{ matrix.pixi_env || 'default' }}"
pixi run -e "$ENV" cmake-config ${{ matrix.build_type }} OFF -- \
"-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}"
pixi run -e "$ENV" cmake-build ${{ matrix.build_type }}
- name: Upload cross-compiled binaries
if: ${{ matrix.build_only }}
uses: actions/upload-artifact@v4
with:
name: cross-build-${{ matrix.target_triple }}
path: |
build/${{ matrix.build_type }}/bin/
build/${{ matrix.build_type }}/lib/
if-no-files-found: error
retention-days: 1
- name: Unit tests
if: ${{ !matrix.build_only }}
timeout-minutes: 5
run: pixi run unit-test ${{ matrix.build_type }}
- name: Integration tests
if: ${{ !matrix.build_only }}
timeout-minutes: 20
run: pixi run integration-test ${{ matrix.build_type }}
- name: Smoke tests
if: ${{ !matrix.build_only }}
timeout-minutes: 15
run: pixi run smoke-test ${{ matrix.build_type }}
- name: Print cache stats and stop server
if: always()
run: |
ENV="${{ matrix.pixi_env || 'default' }}"
if [ "$RUNNER_OS" = "Windows" ]; then
pixi run -e "$ENV" -- sccache --show-stats
pixi run -e "$ENV" -- sccache --stop-server || true
else
pixi run -e "$ENV" -- ccache --show-stats
fi
shell: bash
test-cross:
needs: build
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
build_type: RelWithDebInfo
target_triple: x86_64-apple-darwin
- os: ubuntu-24.04-arm
build_type: RelWithDebInfo
target_triple: aarch64-linux-gnu
- os: windows-11-arm
build_type: RelWithDebInfo
target_triple: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: test-run
- name: Download cross-compiled binaries
uses: actions/download-artifact@v4
with:
name: cross-build-${{ matrix.target_triple }}
path: build/${{ matrix.build_type }}/
- name: Make binaries executable
if: runner.os != 'Windows'
run: chmod +x build/${{ matrix.build_type }}/bin/*
- name: Unit tests
timeout-minutes: 5
run: pixi run -e test-run unit-test ${{ matrix.build_type }}
- name: Integration tests
timeout-minutes: 20
run: pixi run -e test-run integration-test ${{ matrix.build_type }}
- name: Smoke tests
timeout-minutes: 10
run: pixi run -e test-run smoke-test ${{ matrix.build_type }}

View File

@@ -1,38 +0,0 @@
name: upload-llvm
permissions:
contents: write
on:
pull_request:
# if you want to run this workflow, change the branch name to main,
# if you want to turn off it, change it to non existent branch.
branches: [main-turn-off]
workflow_dispatch:
inputs:
workflow_id:
description: "Workflow run ID to pull artifacts from"
required: true
type: string
version:
description: "Release version/tag to publish (e.g., v1.2.3)"
required: true
type: string
jobs:
upload:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Download artifacts from workflow
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-llvm.sh "${{ inputs.workflow_id }}"
- name: Recreate release with artifacts
env:
GH_TOKEN: ${{ secrets.UPLOAD_LLVM }}
TARGET_REPO: clice-io/clice-llvm
run: python3 scripts/upload-llvm.py "${{ inputs.version }}" "${TARGET_REPO}" "${{ inputs.workflow_id }}"

2
.gitignore vendored
View File

@@ -68,7 +68,7 @@ tests/unit/Local/
.pixi/*
!.pixi/config.toml
.codex
.codex/
.claude/*
!.claude/CLAUDE.md
!.claude/commands/

View File

@@ -41,7 +41,8 @@ set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
kotatsu
GIT_REPOSITORY https://github.com/clice-io/kotatsu
GIT_TAG 73814044ce8142f4438a3028f44668675fc09fff
GIT_TAG main
GIT_SHALLOW TRUE
)
set(KOTA_ENABLE_ZEST ON)

View File

@@ -153,7 +153,7 @@ String values support `${workspace}` substitution.
## IPC Protocol
The master and workers communicate using custom RPC messages defined in `src/server/protocol/`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type.
The master and workers communicate using custom RPC messages defined in `src/server/protocol.h`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type.
### Stateful Worker Messages

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-22

View File

@@ -0,0 +1,185 @@
## Context
This change extracts decision `2` from `openspec/changes/explore-improve-folding-range-support/design.md` into a standalone proposal. The current folding implementation in `src/feature/folding_ranges.cpp` mixes three responsibilities in one path:
- discovering foldable structure from AST data
- deciding which ranges survive deduplication and validation
- shaping the final LSP response, including output metadata
That coupling makes the code harder to extend safely. Comment folding, directive-based collectors, capability-aware rendering, and range limiting all become riskier when collection and rendering rules share the same code path. The extracted proposal keeps scope narrower: it does not add new fold categories by itself, but it creates the architecture that later changes can build on without destabilizing existing structural folding.
The downloaded clangd reference confirms both the value and the limit of the upstream design. clangd has useful, tested folding behavior for brace bodies, comment blocks, contiguous `//` groups, and `lineFoldingOnly`, but its implementation largely emits protocol-shaped `FoldingRange` objects directly from collection code. In `SemanticSelection.cpp`, both the AST path and the pseudo-parser path build `FoldingRange` results directly, and the pseudo-parser applies rendering details such as delimiter trimming and `lineFoldingOnly` adjustments while collecting ranges. That is a good behavior reference, but it is not the architecture this extracted change should copy.
`clice` already has stronger ingredients for a real pipeline:
- `LocalSourceRange` gives us a main-file, half-open offset representation that is independent of LSP position encoding
- directive metadata already captures information clangd does not expose well, including conditional-branch state, pragma regions, includes, imports, and macro references
- the current tests are boundary-oriented, which makes them a good fit for validating raw spans before protocol rendering
The design therefore separates "what fold exists in the source" from "how that fold should be emitted to this client". clangd's tested boundary rules are still relevant, but they should become renderer policy and normalization rules rather than collector output format.
## Goals / Non-Goals
**Goals:**
- Separate folding processing into collection, normalization, and rendering phases.
- Preserve the existing AST structural folding categories already supported by `clice`.
- Make ordering, deduplication, and boundary validation deterministic and testable.
- Give later changes a stable extension point for comments, directives, and client-driven rendering options.
**Non-Goals:**
- Add comment folding in this change.
- Fix preprocessor branch-closing behavior in this change.
- Add new fold categories such as macro definitions or include/import grouping.
- Depend on initialize-time client capability plumbing being implemented first.
## Decisions
### 1. Use clangd as a behavior reference, not an architecture template
This change should borrow clangd's confirmed folding behavior where it is useful, especially around multiline comments, contiguous `//` comment groups, main-file-only filtering, and `lineFoldingOnly` boundary shaping. It should not copy clangd's habit of emitting protocol-shaped `FoldingRange` objects directly from collection logic.
Why:
- clangd's tests are valuable because they pin down tricky folding behavior around comments, macro boundaries, and line-only rendering
- clangd's data flow is intentionally narrow and mixes collection with response shaping
- `clice` already has richer file-local and directive metadata that supports a cleaner internal representation
Alternative considered:
- Treat clangd's direct `FoldingRange` construction as the architecture to reproduce. Rejected because it would preserve the same coupling this extracted change is meant to remove.
### 2. Introduce a raw internal folding-range model
Collectors should emit an internal `RawFoldingRange`-style structure instead of final LSP protocol objects. The raw model should preserve source locations, an internal category, and optional metadata hints that later phases may use.
The raw model should be shaped around file-local source structure, not LSP transport fields. At minimum it should carry:
- a main-file `LocalSourceRange` span using half-open byte offsets
- an internal fold category such as namespace, record, access section, function body, comment block, comment group, conditional branch, pragma region, include group, or import group
- the collector origin, such as AST, comment scanning, or directive metadata, so normalization has a stable tie-break and debugging surface
- render hints for syntax-specific shaping, such as delimiter trimming, whether line-only folding should hide the final line, and an optional collapsed-text hint
In other words, the raw model should look closer to:
```cpp
struct RawFoldRenderHint {
std::uint8_t trim_start_bytes = 0;
std::uint8_t trim_end_bytes = 0;
bool hide_last_line_when_line_only = false;
std::string collapsed_text_hint;
};
struct RawFoldingRange {
LocalSourceRange span;
RawFoldCategory category;
RawFoldOrigin origin;
RawFoldRenderHint render;
};
```
The important design choice is that `span` represents the foldable source envelope in the main file, while renderer-specific trimming stays in `render` hints. For example:
- brace-based structural folds keep the full braced span and let the renderer trim interior boundaries
- block comments keep the full `/* ... */` span and let the renderer decide whether to hide the closing delimiter or final line
- contiguous `//` groups keep the full grouped span and let the renderer decide how much of the opening sentinel remains visible
Why:
- collectors should describe what was found, not how it will be serialized
- `LocalSourceRange` is already the natural coordinate system for `clice`
- public LSP kinds such as `comment`, `imports`, and `region` are too lossy to use as the internal category model
- future comment and directive collectors can share the same pipeline contract
- tests can validate collection independently from rendering
Alternatives considered:
- Continue emitting LSP ranges directly from collectors. Rejected because it keeps protocol concerns entangled with source discovery.
- Make the raw model store already-trimmed visible interior spans instead of the full source envelope. Rejected because line-only rendering, collapsed text, and comment delimiter rules would still leak back into every collector.
### 3. Normalize ranges before rendering
All collected ranges should pass through a normalization step before any response is emitted. Normalization is responsible for deterministic ordering, duplicate removal, and rejection of degenerate or unmappable ranges.
Normalization should operate on raw spans and internal categories, not on final LSP fields. Its responsibilities include:
- deterministic ordering independent of collector traversal order
- duplicate collapse for collectors that discover the same fold
- invalid-range filtering after raw spans and render hints are reconciled
- stable tie-breaking for overlapping ranges from different origins
Collectors may still reject obviously invalid inputs, such as non-main-file locations that cannot be mapped to `LocalSourceRange`, but normalization remains the phase that decides which collected folds survive to rendering.
Why:
- duplicate or invalid ranges are easier to reason about in one place than across many collectors
- stable ordering reduces regression noise and makes range limiting predictable later
- category-aware normalization preserves internal meaning until the renderer maps it to public kinds
- normalization lets new collectors plug in without each collector re-implementing cleanup logic
Alternative considered:
- Let each collector manage its own sorting and duplicate suppression. Rejected because cross-collector interactions would still remain undefined.
### 4. Keep the current AST visitor as the first collector boundary
The initial extraction should preserve the current AST visitor as one collector feeding the raw model. This reduces refactor risk while still creating the new phase boundaries.
Why:
- the existing structural fold coverage is valuable and should not be rewritten unnecessarily
- an adapter-style refactor is easier to verify against current tests than a full collector redesign
Alternative considered:
- Rewrite collection around a brand-new multi-source manager immediately. Rejected because it adds scope before the phase split is proven.
### 5. Move output shaping into a dedicated renderer
The renderer should translate normalized ranges into LSP folding ranges. Boundary shaping, output kinds, and optional metadata emission should live there, even if some options still use default values until later protocol plumbing exists.
Renderer input should be the normalized raw model plus a separate `FoldingRenderOptions` structure. The renderer then becomes responsible for:
- converting `LocalSourceRange` into protocol positions for the requested encoding
- applying delimiter trimming and line-only adjustments
- mapping internal categories to public LSP kinds
- deciding whether collapsed text is emitted or suppressed
- later applying deterministic `rangeLimit` trimming without changing collectors
This is the key point where `clice` should intentionally diverge from clangd. clangd threads `lineFoldingOnly` into collection and directly produces protocol objects. `clice` should keep those capability and transport decisions isolated in rendering so collectors remain stable as client support evolves.
Why:
- rendering rules are a separate concern from source discovery
- later work on line-only output, metadata gating, or public kind mapping should not force collector rewrites
- clangd-style line-only shaping is still supported, but as renderer policy rather than collector output
- isolating rendering makes behavioral diffs easier to review
Alternative considered:
- Keep final boundary shaping next to the AST collector and only add a small helper for sorting. Rejected because it only moves a symptom, not the architectural problem.
## Risks / Trade-offs
- [Refactoring the current path can accidentally change fold ordering] -> Mitigation: add deterministic-order assertions and compare outputs for existing structural fixtures.
- [The raw model could become too abstract too early] -> Mitigation: keep the initial fields minimal and only include data already needed by current structural folds.
- [Full-envelope raw spans plus render hints may feel less direct than storing already-trimmed ranges] -> Mitigation: use a small, explicit render-hint structure and validate brace/comment shaping with focused renderer tests.
- [A renderer abstraction may appear premature before full capability plumbing exists] -> Mitigation: keep default render options aligned with current behavior and treat future options as extension points, not immediate scope.
## Migration Plan
1. Introduce raw folding-range and render-option types behind the existing entrypoint.
2. Convert the current AST-based collectors to emit raw ranges.
3. Insert normalization between collection and response emission.
4. Move LSP object construction into a dedicated renderer.
5. Verify that existing structural folding fixtures still produce the expected ranges.
Rollback strategy:
- If the refactor destabilizes output, keep the new helper types but temporarily route the old direct-emission path until normalization and rendering regressions are resolved.
## Open Questions
- Whether public kind remapping should land in this extracted change or remain a follow-up proposal once the renderer boundary exists.
- Whether collector origin should remain part of the long-term raw model after normalization policy stabilizes, or only exist temporarily as a debugging and tie-break aid.

View File

@@ -0,0 +1,26 @@
## Why
`explore-improve-folding-range-support` combines several different concerns: upstream comparison work, baseline folding fixes, preprocessor extensions, and an internal refactor. The second design point in that change, splitting the folding-range pipeline into collection, normalization, and rendering, is the architectural slice that other work depends on and should be referenceable as its own proposal.
## What Changes
- Extract the pipeline-splitting work from `explore-improve-folding-range-support` into a standalone change focused on folding-range architecture.
- Introduce an internal raw folding-range model so collectors no longer emit final LSP objects directly.
- Define a normalization phase that performs deterministic sorting, duplicate removal, and boundary validation before response generation.
- Define a rendering phase that owns line/column shaping and optional metadata emission instead of mixing those concerns into collectors.
- Preserve the current AST structural folding coverage while establishing extension points for future comment, directive, and capability-aware rendering work.
## Capabilities
### New Capabilities
- `folding-range-pipeline`: Provide a deterministic folding-range pipeline that separates collection, normalization, and rendering while preserving existing structural folds.
### Modified Capabilities
- None.
## Impact
- `src/feature/folding_ranges.cpp` will be refactored around raw-range collection, normalization, and rendering boundaries.
- Folding-related helper types may be introduced near the folding feature implementation.
- `tests/unit/feature/folding_range_tests.cpp` will need regression coverage for structural folds and deterministic ordering.
- `openspec/changes/explore-improve-folding-range-support/design.md` remains the source change from which this standalone proposal was extracted.

View File

@@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Folding ranges are normalized before response emission
The server SHALL convert collected folding candidates into a deterministic normalized set before emitting the folding range response.
#### Scenario: Duplicate candidates collapse to one emitted fold
- **WHEN** multiple collectors produce the same folding candidate for the same source span and internal category
- **THEN** the server MUST emit at most one folding range for that candidate
#### Scenario: Invalid candidates are dropped during normalization
- **WHEN** a collected folding candidate does not span multiple lines or cannot be mapped back to the main file
- **THEN** the server MUST omit that candidate from the emitted folding ranges
#### Scenario: Output ordering is deterministic
- **WHEN** the same document is analyzed repeatedly without source changes
- **THEN** the server MUST emit folding ranges in a deterministic order that does not depend on collector traversal order
### Requirement: Existing structural folding survives the pipeline split
The server SHALL preserve the currently supported AST structural folding categories after collection, normalization, and rendering are separated.
#### Scenario: Supported structural regions remain foldable
- **WHEN** a document contains a supported multi-line namespace, record, function body, parameter list, lambda body, initializer list, call argument list, or compound statement
- **THEN** the server MUST still return a folding range for that region when its boundaries can be mapped to the main file
#### Scenario: Structural coverage is preserved through normalization
- **WHEN** the document contains only currently supported AST-driven folding categories
- **THEN** normalization and rendering MUST NOT remove a valid structural fold except when it is an exact duplicate or an invalid range
### Requirement: Rendering decisions are applied after normalization
The server SHALL derive final LSP folding-range output from normalized internal ranges instead of requiring collectors to emit protocol-shaped results directly.
#### Scenario: Rendering options do not require collector changes
- **WHEN** rendering rules change how line or metadata output is shaped for a normalized fold
- **THEN** the server MUST apply that change in the rendering phase without requiring collector-specific logic changes
#### Scenario: Metadata hints remain optional until rendering
- **WHEN** a collected or normalized fold carries optional kind or collapsed-text hints
- **THEN** the renderer MUST decide whether to surface, transform, or suppress that metadata in the emitted LSP range

View File

@@ -0,0 +1,16 @@
## 1. Raw Model and Collector Boundary
- [ ] 1.1 Introduce internal raw folding-range and render-option types while keeping the current folding entrypoint stable.
- [ ] 1.2 Convert the existing AST structural folding path in `src/feature/folding_ranges.cpp` to emit raw ranges instead of final LSP ranges.
- [ ] 1.3 Add regression fixtures or assertions that cover the currently supported structural fold categories before further refactoring.
## 2. Normalization and Rendering
- [ ] 2.1 Implement normalization for deterministic sorting, duplicate removal, and invalid-range filtering.
- [ ] 2.2 Introduce a dedicated renderer that converts normalized ranges into final LSP folding-range objects.
- [ ] 2.3 Keep default rendered output compatible with current structural behavior while exposing extension points for future collectors and render rules.
## 3. Verification
- [ ] 3.1 Compare pre-refactor and post-refactor outputs for the existing structural folding test cases.
- [ ] 3.2 Run relevant folding-range unit tests and fix any ordering, deduplication, or boundary regressions introduced by the new pipeline.

View File

@@ -161,7 +161,7 @@ depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }]
[feature.test.tasks.unit-test]
args = [{ arg = "type", default = "RelWithDebInfo" }]
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data" --snapshot-dir="./tests/snapshots" --corpus-dir="./tests/corpus" --verbose'
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
[feature.test.tasks.integration-test]
args = [{ arg = "type", default = "RelWithDebInfo" }]
@@ -257,7 +257,7 @@ format-markdown = "fd -H -e md -x prettier --write"
format-json = "fd -H -e json -E package-lock.json -x prettier --write"
format-toml = "fd -H -e toml -x tombi format"
format-yaml = """
fd -H -e yaml -e yml -E pnpm-lock.yaml -E '*.snap.yml' -x prettier --write && \
fd -H -e yaml -e yml -E pnpm-lock.yaml -x prettier --write && \
fd -H "^\\.clang-(format|tidy)$" -x prettier --write --parser yaml
"""
format = { depends-on = [

View File

@@ -4,33 +4,33 @@
#include <print>
#include <string>
#include "server/service/agentic.h"
#include "server/service/master_server.h"
#include "server/worker/stateful_worker.h"
#include "server/worker/stateless_worker.h"
#include "server/master_server.h"
#include "server/stateful_worker.h"
#include "server/stateless_worker.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/deco/deco.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/recording_transport.h"
#include "kota/ipc/transport.h"
namespace clice {
using kota::deco::decl::KVStyle;
struct Options {
DecoKV(
style = KVStyle::JoinedOrSeparate,
help =
"Running mode: pipe, socket, daemon, relay, agentic, stateless-worker, stateful-worker",
required = false)
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Running mode: pipe, socket, stateless-worker, stateful-worker",
required = false)
<std::string> mode;
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false)
<std::string> host = "127.0.0.1";
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Agentic TCP port (0 = disabled)",
required = false)
<int> port = 0;
DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false)
<int> port = 50051;
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--log-level", "--log-level="},
@@ -43,50 +43,6 @@ struct Options {
required = false)
<std::string> record;
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "File path for agentic queries",
required = false)
<std::string> path;
DecoKV(
style = KVStyle::JoinedOrSeparate,
help =
"Agentic method (compileCommand, symbolSearch, definition, references, "
"documentSymbols, readSymbol, callGraph, typeHierarchy, projectFiles, "
"fileDeps, impactAnalysis, status, shutdown)",
required = false)
<std::string> method;
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Symbol name for agentic queries",
required = false)
<std::string> name;
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Search query for symbolSearch",
required = false)
<std::string> query;
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Line number for position-based lookup",
required = false)
<int> line;
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Direction: callers/callees or supertypes/subtypes",
required = false)
<std::string> direction;
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Unix domain socket path for daemon mode",
required = false)
<std::string> socket;
DecoKV(style = KVStyle::JoinedOrSeparate,
help = "Workspace root directory for daemon mode",
required = false)
<std::string> workspace;
// Internal options (passed from master to worker processes)
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--worker-memory-limit", "--worker-memory-limit="},
@@ -112,6 +68,9 @@ struct Options {
int main(int argc, const char** argv) {
#ifndef _WIN32
// On POSIX systems, ignore SIGPIPE so that writing to a closed pipe
// (e.g. when the LSP client disconnects) returns EPIPE instead of
// killing the process. This is standard practice for pipe-based servers.
signal(SIGPIPE, SIG_IGN);
#endif
@@ -151,6 +110,8 @@ int main(int argc, const char** argv) {
return 1;
}
std::string self_path = argv[0];
auto& mode = *opts.mode;
auto worker_name = opts.worker_name.value_or("");
@@ -170,51 +131,77 @@ int main(int argc, const char** argv) {
log_dir);
}
if(mode == "pipe" || mode == "socket") {
clice::ServerOptions server_opts;
server_opts.mode = mode;
server_opts.host = opts.host.value_or("127.0.0.1");
server_opts.port = opts.port.value_or(0);
server_opts.self_path = argv[0];
server_opts.record = opts.record.value_or("");
return clice::run_server_mode(server_opts);
}
if(mode == "pipe") {
clice::logging::stderr_logger("master", clice::logging::options);
if(mode == "daemon") {
auto workspace = opts.workspace.value_or("");
if(workspace.empty()) {
LOG_ERROR("--workspace is required for daemon mode");
kota::event_loop loop;
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
if(!transport) {
LOG_ERROR("failed to open stdio transport");
return 1;
}
clice::DaemonOptions daemon_opts;
daemon_opts.socket_path = opts.socket.value_or("");
daemon_opts.workspace = std::move(workspace);
daemon_opts.self_path = argv[0];
return clice::run_daemon_mode(daemon_opts);
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
if(opts.record.has_value()) {
final_transport =
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
*opts.record);
}
kota::ipc::JsonPeer peer(loop, std::move(final_transport));
clice::MasterServer server(loop, peer, std::move(self_path));
server.register_handlers();
loop.schedule(peer.run());
loop.run();
return 0;
}
if(mode == "agentic") {
auto port = opts.port.value_or(0);
if(port <= 0) {
LOG_ERROR("--port is required for agentic mode");
if(mode == "socket") {
clice::logging::stderr_logger("master", clice::logging::options);
kota::event_loop loop;
auto host = opts.host.value_or("127.0.0.1");
auto port = opts.port.value_or(50051);
auto acceptor = kota::tcp::listen(host, port, {}, loop);
if(!acceptor) {
LOG_ERROR("failed to listen on {}:{}", host, port);
return 1;
}
clice::AgenticQueryOptions aq;
aq.host = opts.host.value_or("127.0.0.1");
aq.port = port;
aq.method = opts.method.value_or("compileCommand");
aq.path = opts.path.value_or("");
aq.name = opts.name.value_or("");
aq.query = opts.query.value_or("");
aq.line = opts.line.value_or(0);
aq.direction = opts.direction.value_or("");
return clice::run_agentic_mode(aq);
}
if(mode == "relay") {
auto socket = opts.socket.value_or("");
return clice::run_relay_mode(socket);
LOG_INFO("Listening on {}:{} ...", host, port);
auto task = [&]() -> kota::task<> {
auto client = co_await acceptor->accept();
if(!client.has_value()) {
LOG_ERROR("failed to accept connection");
loop.stop();
co_return;
}
LOG_INFO("Client connected");
std::unique_ptr<kota::ipc::Transport> transport =
std::make_unique<kota::ipc::StreamTransport>(std::move(client.value()));
if(opts.record.has_value()) {
transport = std::make_unique<kota::ipc::RecordingTransport>(std::move(transport),
*opts.record);
}
kota::ipc::JsonPeer peer(loop, std::move(transport));
clice::MasterServer server(loop, peer, std::string(self_path));
server.register_handlers();
co_await peer.run();
peer.close();
loop.stop();
};
loop.schedule(task());
loop.run();
return 0;
}
LOG_ERROR("unknown mode '{}'", mode);

View File

@@ -9,7 +9,6 @@
#include "support/logging.h"
#include "kota/meta/enum.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
@@ -18,7 +17,6 @@
#include "llvm/TargetParser/Host.h"
#include "clang/Driver/Compilation.h"
#include "clang/Driver/Driver.h"
#include "clang/Driver/Options.h"
#include "clang/Driver/Tool.h"
#ifndef _WIN32
@@ -472,32 +470,11 @@ std::vector<const char*> query_clang_toolchain(const QueryParams& params) {
continue;
}
// FIXME: the system compiler may be newer than our embedded LLVM,
// producing cc1 flags we don't recognize. Filter them out here.
// Long-term we should unify the command pipeline so the driver
// version always matches the embedded LLVM.
auto& table = clang::driver::getDriverOptTable();
auto cc1_args = llvm::ArrayRef(args).drop_front(2);
unsigned missing_index = 0, missing_count = 0;
auto parsed = table.ParseArgs(cc1_args, missing_index, missing_count);
llvm::DenseSet<unsigned> unknown_indices;
for(auto* a: parsed) {
if(a->getOption().getKind() == llvm::opt::Option::UnknownClass) {
unknown_indices.insert(a->getIndex());
}
}
result.emplace_back(params.callback(args[0]));
result.emplace_back(params.callback(args[1]));
for(unsigned i = 0; i < cc1_args.size(); ++i) {
if(unknown_indices.contains(i)) {
for(auto arg: args) {
if(arg == "-###"sv) {
continue;
}
if(cc1_args[i] == "-###"sv) {
continue;
}
result.emplace_back(params.callback(cc1_args[i]));
result.emplace_back(params.callback(arg));
}
}
}

View File

@@ -92,11 +92,15 @@ tidy::ClangTidyOptions create_options() {
// include-cleaner is directly integrated in IncludeCleaner.cpp
"-misc-include-cleaner",
// ----- False Positives -----
// Check relies on seeing ifndef/define/endif directives,
// clangd doesn't replay those when using a preamble.
"-llvm-header-guard",
"-modernize-macro-to-enum",
// ----- Crashing Checks -----
// Check can choke on invalid (intermediate) c++
// code, which is often the case when clangd
// tries to build an AST.

View File

@@ -93,9 +93,18 @@ auto symbol_detail(clang::ASTContext& context, const clang::NamedDecl& decl) ->
return detail;
}
struct InternalSymbol {
std::string name;
std::string detail;
SymbolKind kind = SymbolKind::Invalid;
LocalSourceRange range;
LocalSourceRange selection_range;
std::vector<InternalSymbol> children;
};
struct SymbolFrame {
std::vector<DocumentSymbol> symbols;
std::vector<DocumentSymbol>* cursor = &symbols;
std::vector<InternalSymbol> symbols;
std::vector<InternalSymbol>* cursor = &symbols;
};
class DocumentSymbolCollector : public FilteredASTVisitor<DocumentSymbolCollector> {
@@ -134,7 +143,7 @@ public:
return ok;
}
auto collect() -> std::vector<DocumentSymbol> {
auto collect() -> std::vector<InternalSymbol> {
TraverseDecl(unit.tu());
return std::move(result.symbols);
}
@@ -165,8 +174,8 @@ private:
SymbolFrame result;
};
void sort_symbols(std::vector<DocumentSymbol>& symbols) {
std::ranges::sort(symbols, [](const DocumentSymbol& lhs, const DocumentSymbol& rhs) {
void sort_symbols(std::vector<InternalSymbol>& symbols) {
std::ranges::sort(symbols, [](const InternalSymbol& lhs, const InternalSymbol& rhs) {
if(lhs.range.begin != rhs.range.begin) {
return lhs.range.begin < rhs.range.begin;
}
@@ -178,7 +187,7 @@ void sort_symbols(std::vector<DocumentSymbol>& symbols) {
}
}
auto to_protocol_symbol(const DocumentSymbol& symbol, const PositionMapper& converter)
auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& converter)
-> protocol::DocumentSymbol {
protocol::DocumentSymbol result{
.name = symbol.name,
@@ -206,15 +215,10 @@ auto to_protocol_symbol(const DocumentSymbol& symbol, const PositionMapper& conv
} // namespace
auto document_symbols(CompilationUnitRef unit) -> std::vector<DocumentSymbol> {
auto result = DocumentSymbolCollector(unit).collect();
sort_symbols(result);
return result;
}
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::DocumentSymbol> {
auto internal = document_symbols(unit);
auto internal = DocumentSymbolCollector(unit).collect();
sort_symbols(internal);
PositionMapper converter(unit.interested_content(), encoding);
std::vector<protocol::DocumentSymbol> symbols;

View File

@@ -7,7 +7,6 @@
#include "compile/compilation.h"
#include "compile/compilation_unit.h"
#include "semantic/symbol_kind.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
@@ -60,66 +59,18 @@ struct InlayHintsOptions {
struct SignatureHelpOptions {};
struct SemanticToken {
LocalSourceRange range;
SymbolKind kind = SymbolKind::Invalid;
std::uint32_t modifiers = 0;
};
struct FoldingRange {
LocalSourceRange range;
std::optional<protocol::FoldingRangeKind> kind;
std::string collapsed_text;
};
struct DocumentSymbol {
std::string name;
std::string detail;
SymbolKind kind = SymbolKind::Invalid;
LocalSourceRange range;
LocalSourceRange selection_range;
std::vector<DocumentSymbol> children;
};
enum class HintCategory : std::uint8_t {
Parameter,
DefaultArgument,
Type,
Designator,
BlockEnd,
};
struct InlayHint {
std::uint32_t offset = 0;
HintCategory kind = HintCategory::Type;
std::string label;
bool padding_left = false;
bool padding_right = false;
};
auto semantic_tokens(CompilationUnitRef unit) -> std::vector<SemanticToken>;
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding)
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> protocol::SemanticTokens;
auto folding_ranges(CompilationUnitRef unit) -> std::vector<FoldingRange>;
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::FoldingRange>;
auto document_symbols(CompilationUnitRef unit) -> std::vector<DocumentSymbol>;
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::DocumentSymbol>;
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options = {}) -> std::vector<InlayHint>;
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options,
PositionEncoding encoding) -> std::vector<protocol::InlayHint>;
auto document_links(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::DocumentLink>;
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::DocumentSymbol>;
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::FoldingRange>;
auto diagnostics(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::Diagnostic>;
@@ -138,6 +89,12 @@ auto hover(CompilationUnitRef unit,
const HoverOptions& options = {},
PositionEncoding encoding = PositionEncoding::UTF16) -> std::optional<protocol::Hover>;
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options = {},
PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::InlayHint>;
auto signature_help(CompilationParams& params, const SignatureHelpOptions& options = {})
-> protocol::SignatureHelp;

View File

@@ -53,6 +53,12 @@ auto to_kind(FoldingKind kind) -> protocol::FoldingRangeKind {
return protocol::FoldingRangeKind(protocol::FoldingRangeKind::region);
}
struct RawFoldingRange {
LocalSourceRange range;
std::optional<protocol::FoldingRangeKind> kind;
std::string collapsed_text;
};
class FoldingRangeCollector : public FilteredASTVisitor<FoldingRangeCollector> {
public:
explicit FoldingRangeCollector(CompilationUnitRef unit) : FilteredASTVisitor(unit, true) {}
@@ -179,7 +185,7 @@ public:
return true;
}
auto collect() -> std::vector<FoldingRange> {
auto collect() -> std::vector<RawFoldingRange> {
TraverseDecl(unit.tu());
auto directives_it = unit.directives().find(unit.interested_file());
@@ -187,7 +193,7 @@ public:
collect_directives(directives_it->second);
}
std::ranges::sort(ranges, [](const FoldingRange& lhs, const FoldingRange& rhs) {
std::ranges::sort(ranges, [](const RawFoldingRange& lhs, const RawFoldingRange& rhs) {
if(lhs.range.begin != rhs.range.begin) {
return lhs.range.begin < rhs.range.begin;
}
@@ -337,18 +343,14 @@ private:
}
private:
std::vector<FoldingRange> ranges;
std::vector<RawFoldingRange> ranges;
};
} // namespace
auto folding_ranges(CompilationUnitRef unit) -> std::vector<FoldingRange> {
return FoldingRangeCollector(unit).collect();
}
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::FoldingRange> {
auto collected = folding_ranges(unit);
auto collected = FoldingRangeCollector(unit).collect();
PositionMapper converter(unit.interested_content(), encoding);
std::vector<protocol::FoldingRange> result;

View File

@@ -49,7 +49,7 @@ auto document_format(llvm::StringRef file,
range ? tooling::Range(range->begin, range->length()) : tooling::Range(0, content.size());
auto replacements = format_content(file, content, selection);
if(!replacements) {
LOG_WARN("Failed to format {}: {}", file, replacements.error());
LOG_INFO("Fail to format for {}\n{}", file, replacements.error());
return edits;
}

View File

@@ -26,6 +26,22 @@ using llvm::dyn_cast_or_null;
// For now, inlay hints are always anchored at the left or right of their range.
enum class HintSide { Left, Right };
enum class HintCategory : std::uint8_t {
Parameter,
DefaultArgument,
Type,
Designator,
BlockEnd,
};
struct RawInlayHint {
std::uint32_t offset = 0;
HintCategory kind = HintCategory::Type;
std::string label;
bool padding_left = false;
bool padding_right = false;
};
bool is_expanded_from_param_pack(const clang::ParmVarDecl* param) {
return ast::underlying_pack_type(param) != nullptr;
}
@@ -107,7 +123,7 @@ struct Callee {
class Builder {
public:
Builder(std::vector<InlayHint>& result,
Builder(std::vector<RawInlayHint>& result,
CompilationUnitRef unit,
LocalSourceRange restrict_range,
const InlayHintsOptions& options) :
@@ -483,7 +499,7 @@ public:
bool pad_left = prefix.consume_front(" ");
bool pad_right = suffix.consume_back(" ");
InlayHint hint{
RawInlayHint hint{
.offset = offset,
.kind = kind,
.label = (prefix + label + suffix).str(),
@@ -538,7 +554,7 @@ public:
}
private:
std::vector<InlayHint>& result;
std::vector<RawInlayHint>& result;
CompilationUnitRef unit;
LocalSourceRange restrict_range;
const InlayHintsOptions& options;
@@ -897,43 +913,36 @@ private:
} // namespace
auto inlay_hints(CompilationUnitRef unit, LocalSourceRange target, const InlayHintsOptions& options)
-> std::vector<InlayHint> {
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options,
PositionEncoding encoding) -> std::vector<protocol::InlayHint> {
if(!options.enabled) {
return {};
}
std::vector<InlayHint> raw_hints;
std::vector<RawInlayHint> raw_hints;
Builder builder(raw_hints, unit, target, options);
Visitor visitor(builder, unit, target, options);
visitor.TraverseDecl(unit.tu());
std::ranges::sort(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) {
std::ranges::sort(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) {
return std::tie(lhs.offset, lhs.label, lhs.kind, lhs.padding_left, lhs.padding_right) <
std::tie(rhs.offset, rhs.label, rhs.kind, rhs.padding_left, rhs.padding_right);
});
auto unique_begin =
std::ranges::unique(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) {
std::ranges::unique(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) {
return lhs.offset == rhs.offset && lhs.kind == rhs.kind && lhs.label == rhs.label &&
lhs.padding_left == rhs.padding_left && lhs.padding_right == rhs.padding_right;
});
raw_hints.erase(unique_begin.begin(), unique_begin.end());
return raw_hints;
}
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options,
PositionEncoding encoding) -> std::vector<protocol::InlayHint> {
auto collected = inlay_hints(unit, target, options);
PositionMapper converter(unit.interested_content(), encoding);
std::vector<protocol::InlayHint> hints;
hints.reserve(collected.size());
hints.reserve(raw_hints.size());
for(const auto& hint: collected) {
for(const auto& hint: raw_hints) {
protocol::InlayHint out{
.position = *converter.to_position(hint.offset),
.label = hint.label,

View File

@@ -18,6 +18,12 @@ namespace clice::feature {
namespace {
struct RawToken {
LocalSourceRange range;
SymbolKind kind = SymbolKind::Invalid;
std::uint32_t modifiers = 0;
};
void add_modifier(std::uint32_t& modifiers, SymbolModifiers::Kind kind) {
modifiers |= SymbolModifiers::to_mask(kind);
}
@@ -34,34 +40,6 @@ bool is_dependent(const clang::Decl* D) {
return isa<clang::UnresolvedUsingValueDecl>(D);
}
/// Whether a declaration name is backed by source text that should be highlighted.
bool can_highlight_name(clang::DeclarationName name) {
switch(name.getNameKind()) {
case clang::DeclarationName::Identifier: {
auto* info = name.getAsIdentifierInfo();
return info && !info->getName().empty();
}
case clang::DeclarationName::CXXConstructorName:
case clang::DeclarationName::CXXDestructorName: {
return true;
}
case clang::DeclarationName::CXXConversionFunctionName:
case clang::DeclarationName::CXXOperatorName:
case clang::DeclarationName::CXXDeductionGuideName:
case clang::DeclarationName::CXXLiteralOperatorName:
case clang::DeclarationName::CXXUsingDirective:
case clang::DeclarationName::ObjCZeroArgSelector:
case clang::DeclarationName::ObjCOneArgSelector:
case clang::DeclarationName::ObjCMultiArgSelector: {
return false;
}
}
std::unreachable();
}
/// Returns true if `decl` is considered to be from a default/system library.
/// This currently checks the systemness of the file by include type, although
/// different heuristics may be used in the future (e.g. sysroot paths).
@@ -188,7 +166,7 @@ class SemanticTokensCollector : public SemanticVisitor<SemanticTokensCollector>
public:
explicit SemanticTokensCollector(CompilationUnitRef unit) : SemanticVisitor(unit, true) {}
auto collect() -> std::vector<SemanticToken> {
auto collect() -> std::vector<RawToken> {
highlight_lexical(unit.interested_file());
run();
highlight_modules();
@@ -199,10 +177,6 @@ public:
void handleDeclOccurrence(const clang::NamedDecl* decl,
RelationKind relation,
clang::SourceLocation location) {
if(relation.isReference() && !can_highlight_name(decl->getDeclName())) {
return;
}
std::uint32_t modifiers = 0;
if(relation.is_one_of(RelationKind::Definition)) {
// todo: clangd add both Declaration and Definition modifiers for definitions.
@@ -424,7 +398,7 @@ private:
}
}
static void resolve_conflict(SemanticToken& last, const SemanticToken& current) {
static void resolve_conflict(RawToken& last, const RawToken& current) {
if(last.kind == SymbolKind::Conflict) {
return;
}
@@ -440,14 +414,14 @@ private:
}
void merge_tokens() {
std::ranges::sort(tokens, [](const SemanticToken& lhs, const SemanticToken& rhs) {
std::ranges::sort(tokens, [](const RawToken& lhs, const RawToken& rhs) {
if(lhs.range.begin != rhs.range.begin) {
return lhs.range.begin < rhs.range.begin;
}
return lhs.range.end < rhs.range.end;
});
std::vector<SemanticToken> merged;
std::vector<RawToken> merged;
merged.reserve(tokens.size());
for(const auto& token: tokens) {
@@ -474,7 +448,7 @@ private:
}
public:
std::vector<SemanticToken> tokens;
std::vector<RawToken> tokens;
};
class SemanticTokenEncoder {
@@ -484,7 +458,7 @@ public:
protocol::SemanticTokens& output) :
content(content), converter(content, encoding), output(output) {}
void append(const SemanticToken& token) {
void append(const RawToken& token) {
if(!token.range.valid() || token.range.end <= token.range.begin ||
token.range.end > content.size()) {
return;
@@ -568,14 +542,10 @@ private:
} // namespace
auto semantic_tokens(CompilationUnitRef unit) -> std::vector<SemanticToken> {
SemanticTokensCollector collector(unit);
return collector.collect();
}
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding)
-> protocol::SemanticTokens {
auto tokens = semantic_tokens(unit);
SemanticTokensCollector collector(unit);
auto tokens = collector.collect();
protocol::SemanticTokens result;
result.data.reserve(tokens.size() * 5);

View File

@@ -1111,6 +1111,8 @@ public:
return Base::TransformDecltypeType(TLB, TL);
}
// --- State ---
private:
clang::Sema& sema;
clang::ASTContext& context;

View File

@@ -1,4 +1,4 @@
#include "server/compiler/compile_graph.h"
#include "server/compile_graph.h"
#include <algorithm>

View File

@@ -1,4 +1,4 @@
#include "server/compiler/compiler.h"
#include "server/compiler.h"
#include <format>
#include <ranges>
@@ -6,7 +6,7 @@
#include "command/search_config.h"
#include "index/tu_index.h"
#include "server/protocol/worker.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "syntax/include_resolver.h"
@@ -28,20 +28,16 @@ using serde_raw = kota::codec::RawValue;
/// Detect whether the cursor is inside a preamble directive (include/import).
Compiler::Compiler(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
Workspace& workspace,
WorkerPool& pool,
llvm::DenseMap<std::uint32_t, Session>& sessions) :
loop(loop), workspace(workspace), pool(pool), sessions(sessions) {}
loop(loop), peer(peer), workspace(workspace), pool(pool), sessions(sessions) {}
Compiler::~Compiler() {
workspace.cancel_all();
}
kota::task<> Compiler::stop() {
compile_tasks.cancel();
co_await compile_tasks.join();
}
void Compiler::init_compile_graph() {
if(workspace.path_to_module.empty()) {
LOG_INFO("No C++20 modules detected, skipping CompileGraph");
@@ -414,8 +410,6 @@ std::string uri_to_path(const std::string& uri) {
void Compiler::publish_diagnostics(const std::string& uri,
int version,
const kota::codec::RawValue& diagnostics_json) {
if(!peer)
return;
std::vector<protocol::Diagnostic> diagnostics;
if(!diagnostics_json.empty()) {
auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics);
@@ -427,16 +421,14 @@ void Compiler::publish_diagnostics(const std::string& uri,
params.uri = uri;
params.version = version;
params.diagnostics = std::move(diagnostics);
peer->send_notification(params);
peer.send_notification(params);
}
void Compiler::clear_diagnostics(const std::string& uri) {
if(!peer)
return;
protocol::PublishDiagnosticsParams params;
params.uri = uri;
params.diagnostics = {};
peer->send_notification(params);
peer.send_notification(params);
}
kota::task<bool> Compiler::ensure_pch(Session& session,
@@ -637,101 +629,6 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef<std::string> deps) {
/// Called lazily by forward_query() / forward_build() before every
/// feature request (hover, semantic tokens, etc.). Guarantees that when it
/// returns true the stateful worker assigned to `path_id` holds an up-to-date
kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptr<Session::PendingCompile> pc) {
auto find_session = [&]() -> Session* {
auto it = sessions.find(pid);
return it != sessions.end() ? &it->second : nullptr;
};
auto* sess = find_session();
if(!sess) {
pc->done.set();
co_return;
}
auto finish_compile = [&]() {
auto* s = find_session();
if(s && s->compiling == pc) {
s->compiling.reset();
}
LOG_INFO("ensure_compiled: finish path_id={}", pid);
pc->done.set();
};
auto gen = sess->generation;
LOG_INFO("ensure_compiled: starting compile path_id={} gen={}", pid, gen);
auto file_path = std::string(workspace.path_pool.resolve(pid));
auto uri = lsp::URI::from_file_path(file_path);
std::string uri_str = uri.has_value() ? uri->str() : file_path;
worker::CompileParams params;
params.path = file_path;
params.version = sess->version;
params.text = sess->text;
if(!fill_compile_args(file_path, params.directory, params.arguments, sess)) {
finish_compile();
co_return;
}
if(!co_await ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
finish_compile();
co_return;
}
sess = find_session();
if(!sess) {
pc->done.set();
co_return;
}
auto result = co_await pool.send_stateful(pid, params);
sess = find_session();
if(!sess) {
pc->done.set();
co_return;
}
if(sess->generation != gen) {
LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}",
sess->generation,
gen,
uri_str);
finish_compile();
co_return;
}
if(!result.has_value()) {
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
clear_diagnostics(uri_str);
finish_compile();
co_return;
}
sess->ast_dirty = false;
pc->succeeded = true;
record_deps(*sess, result.value().deps);
if(!result.value().tu_index_data.empty()) {
auto tu_index = index::TUIndex::from(result.value().tu_index_data.data());
OpenFileIndex ofi;
ofi.file_index = std::move(tu_index.main_file_index);
ofi.symbols = std::move(tu_index.symbols);
ofi.content = sess->text;
ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16);
sess->file_index = std::move(ofi);
}
auto version = sess->version;
finish_compile();
publish_diagnostics(uri_str, version, result.value().diagnostics);
if(on_indexing_needed)
on_indexing_needed();
}
/// AST and diagnostics have been published to the client.
///
/// Lifecycle overview (pull-based model):
@@ -751,9 +648,9 @@ kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptr<Session::P
/// worker); every other file is read from disk by the compiler.
///
/// Concurrency: multiple concurrent feature requests for the same file will
/// each call ensure_compiled(). The first one spawns a compile task into the
/// Compiler's task_group; subsequent ones wait on the shared event.
/// The spawned task is not cancelled by LSP $/cancelRequest, preventing
/// each call ensure_compiled(). The first one launches a detached compile
/// task via loop.schedule(); subsequent ones wait on the shared event.
/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing
/// the race where cancellation wakes all waiters and they all start compiles.
kota::task<bool> Compiler::ensure_compiled(Session& session) {
auto path_id = session.path_id;
@@ -782,12 +679,124 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
co_return true;
}
// No compile in flight and AST is dirty — launch a detached compile task.
// The detached task is scheduled via loop.schedule() so it is NOT subject
// to LSP $/cancelRequest cancellation. This eliminates the race where
// cancellation fires the RAII guard, waking all waiters simultaneously
// and causing them all to start new compiles.
auto pending_compile = std::make_shared<Session::PendingCompile>();
session.compiling = pending_compile;
LOG_INFO("ensure_compiled: launching compile path_id={} gen={}", path_id, session.generation);
LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}",
path_id,
session.generation);
compile_tasks.spawn(run_compile(path_id, pending_compile));
// Capture path_id by value so the detached lambda can re-lookup the session
// from the sessions map after co_await (DenseMap may invalidate pointers).
loop.schedule([](Compiler* self,
std::uint32_t pid,
std::shared_ptr<Session::PendingCompile> pc) -> kota::task<> {
// Re-lookup session from the sessions map (pointer may have been
// invalidated by DenseMap growth during co_await).
auto find_session = [&]() -> Session* {
auto it = self->sessions.find(pid);
return it != self->sessions.end() ? &it->second : nullptr;
};
auto* sess = find_session();
if(!sess) {
pc->done.set();
co_return;
}
auto finish_compile = [&]() {
auto* s = find_session();
if(s && s->compiling == pc) {
s->compiling.reset();
}
LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid);
pc->done.set();
};
auto gen = sess->generation;
LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen);
auto file_path = std::string(self->workspace.path_pool.resolve(pid));
auto uri = lsp::URI::from_file_path(file_path);
std::string uri_str = uri.has_value() ? uri->str() : file_path;
worker::CompileParams params;
params.path = file_path;
params.version = sess->version;
params.text = sess->text;
if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) {
finish_compile();
co_return;
}
if(!co_await self
->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
finish_compile();
co_return;
}
// Re-lookup after co_await (DenseMap may have grown).
sess = find_session();
if(!sess) {
pc->done.set();
co_return;
}
auto result = co_await self->pool.send_stateful(pid, params);
// Re-lookup after co_await.
sess = find_session();
if(!sess) {
pc->done.set();
co_return;
}
if(sess->generation != gen) {
LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}",
sess->generation,
gen,
uri_str);
finish_compile();
co_return;
}
if(!result.has_value()) {
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
self->clear_diagnostics(uri_str);
finish_compile();
co_return;
}
sess->ast_dirty = false;
pc->succeeded = true;
self->record_deps(*sess, result.value().deps);
// Store open file index from the stateful worker's TUIndex.
if(!result.value().tu_index_data.empty()) {
auto tu_index = index::TUIndex::from(result.value().tu_index_data.data());
OpenFileIndex ofi;
ofi.file_index = std::move(tu_index.main_file_index);
ofi.symbols = std::move(tu_index.symbols);
ofi.content = sess->text;
ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16);
sess->file_index = std::move(ofi);
}
auto version = sess->version;
finish_compile();
// Publish diagnostics AFTER marking compile as done, so that concurrent
// forward_query() calls can proceed immediately.
self->publish_diagnostics(uri_str, version, result.value().diagnostics);
if(self->on_indexing_needed)
self->on_indexing_needed();
}(this, path_id, pending_compile));
// Wait for the detached compile to finish. If this wait is cancelled
// by LSP $/cancelRequest, the detached task continues unaffected.
@@ -882,32 +891,6 @@ Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
co_return std::move(result.value().result_json);
}
Compiler::RawResult Compiler::forward_format(Session& session,
std::optional<protocol::Range> range) {
auto path_id = session.path_id;
auto path = std::string(workspace.path_pool.resolve(path_id));
worker::BuildParams wp;
wp.kind = worker::BuildKind::Format;
wp.file = path;
wp.text = session.text;
if(range) {
lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16);
auto begin = mapper.to_offset(range->start);
auto end = mapper.to_offset(range->end);
if(!begin || !end)
co_return serde_raw{"null"};
wp.format_range = {*begin, *end};
}
auto result = co_await pool.send_stateless(wp);
if(!result.has_value()) {
co_return serde_raw{"null"};
}
co_return std::move(result.value().result_json);
}
Compiler::RawResult Compiler::handle_completion(const protocol::Position& position,
Session& session) {
auto path_id = session.path_id;

View File

@@ -8,9 +8,9 @@
#include <vector>
#include "command/command.h"
#include "server/service/session.h"
#include "server/worker/worker_pool.h"
#include "server/workspace/workspace.h"
#include "server/session.h"
#include "server/worker_pool.h"
#include "server/workspace.h"
#include "syntax/completion.h"
#include "kota/async/async.h"
@@ -50,14 +50,10 @@ std::string uri_to_path(const std::string& uri);
class Compiler {
public:
Compiler(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
Workspace& workspace,
WorkerPool& pool,
llvm::DenseMap<std::uint32_t, Session>& sessions);
void set_peer(kota::ipc::JsonPeer* p) {
peer = p;
}
~Compiler();
void init_compile_graph();
@@ -90,9 +86,6 @@ public:
const protocol::Position& position,
Session& session);
/// Forward a formatting request to a stateless worker.
RawResult forward_format(Session& session, std::optional<protocol::Range> range = {});
/// Handle completion requests. Detects preamble context (include/import)
/// and serves those locally; delegates code completion to a stateless worker.
RawResult handle_completion(const protocol::Position& position, Session& session);
@@ -103,12 +96,7 @@ public:
/// Callback invoked when indexing should be scheduled.
std::function<void()> on_indexing_needed;
/// Cancel in-flight compile tasks and wait for them to finish.
kota::task<> stop();
private:
kota::task<> run_compile(std::uint32_t path_id, std::shared_ptr<Session::PendingCompile> pc);
kota::task<bool> ensure_deps(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments,
@@ -137,11 +125,10 @@ private:
private:
kota::event_loop& loop;
kota::ipc::JsonPeer* peer = nullptr;
kota::ipc::JsonPeer& peer;
Workspace& workspace;
WorkerPool& pool;
llvm::DenseMap<std::uint32_t, Session>& sessions;
kota::task_group<> compile_tasks{loop};
};
} // namespace clice

View File

@@ -1,4 +1,4 @@
#include "server/workspace/config.h"
#include "server/config.h"
#include <algorithm>
@@ -156,13 +156,13 @@ std::optional<Config> Config::load(llvm::StringRef path, llvm::StringRef workspa
}
std::optional<Config> Config::load_from_json(llvm::StringRef json, llvm::StringRef workspace_root) {
Config config{};
auto result = kota::codec::json::from_json(json, config);
auto result = kota::codec::json::from_json<Config>(json);
if(!result) {
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message);
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message());
return std::nullopt;
}
auto config = std::move(*result);
config.apply_defaults(workspace_root);
LOG_INFO("Loaded config from initializationOptions");
return config;

View File

@@ -1,4 +1,4 @@
#include "server/compiler/indexer.h"
#include "server/indexer.h"
#include <algorithm>
#include <string>
@@ -6,10 +6,10 @@
#include <vector>
#include "index/tu_index.h"
#include "server/compiler/compiler.h"
#include "server/protocol/worker.h"
#include "server/service/session.h"
#include "server/worker/worker_pool.h"
#include "server/compiler.h"
#include "server/protocol.h"
#include "server/session.h"
#include "server/worker_pool.h"
#include "support/filesystem.h"
#include "support/logging.h"
@@ -447,152 +447,6 @@ std::optional<SymbolInfo> Indexer::resolve_symbol(index::SymbolHash hash) {
return SymbolInfo{hash, std::move(name), kind, def_loc->uri, def_loc->range};
}
static std::string extract_line(llvm::StringRef content, std::uint32_t offset) {
if(content.empty() || offset >= content.size())
return {};
std::size_t line_start = 0;
if(offset > 0) {
auto pos = content.rfind('\n', offset - 1);
if(pos != llvm::StringRef::npos)
line_start = pos + 1;
}
auto line_end = content.find('\n', offset);
if(line_end == llvm::StringRef::npos)
line_end = content.size();
return content.slice(line_start, line_end).str();
}
std::optional<Indexer::DefinitionText> Indexer::get_definition_text(index::SymbolHash hash) {
for(auto& [id, sess]: sessions) {
if(!sess.file_index || !sess.file_index->mapper)
continue;
auto it = sess.file_index->file_index.relations.find(hash);
if(it == sess.file_index->file_index.relations.end())
continue;
for(auto& rel: it->second) {
if(rel.kind.value() != RelationKind::Definition)
continue;
auto def_range = std::bit_cast<LocalSourceRange>(rel.target_symbol);
if(def_range.begin >= def_range.end)
continue;
llvm::StringRef content = sess.file_index->content;
if(def_range.end > content.size())
continue;
auto start = sess.file_index->mapper->to_position(def_range.begin);
auto end = sess.file_index->mapper->to_position(def_range.end);
if(!start || !end)
continue;
return DefinitionText{
.file = std::string(workspace.path_pool.resolve(id)),
.start_line = static_cast<int>(start->line) + 1,
.end_line = static_cast<int>(end->line) + 1,
.text =
std::string(content.substr(def_range.begin, def_range.end - def_range.begin)),
};
}
}
auto sym_it = workspace.project_index.symbols.find(hash);
if(sym_it == workspace.project_index.symbols.end())
return std::nullopt;
for(auto file_id: sym_it->second.reference_files) {
if(is_proj_path_open(file_id))
continue;
auto shard_it = workspace.merged_indices.find(file_id);
if(shard_it == workspace.merged_indices.end())
continue;
auto* m = shard_it->second.mapper();
if(!m)
continue;
auto content = shard_it->second.index.content();
std::optional<DefinitionText> result;
shard_it->second.index.lookup(
hash,
RelationKind::Definition,
[&](const index::Relation& r) {
auto def_range = std::bit_cast<LocalSourceRange>(r.target_symbol);
if(def_range.begin >= def_range.end || def_range.end > content.size())
return true;
auto start = m->to_position(def_range.begin);
auto end = m->to_position(def_range.end);
if(!start || !end)
return true;
result = DefinitionText{
.file = workspace.project_index.path_pool.path(file_id).str(),
.start_line = static_cast<int>(start->line) + 1,
.end_line = static_cast<int>(end->line) + 1,
.text = std::string(
content.substr(def_range.begin, def_range.end - def_range.begin)),
};
return false;
});
if(result)
return result;
}
return std::nullopt;
}
std::vector<Indexer::ReferenceWithContext> Indexer::collect_references(index::SymbolHash hash,
RelationKind kind) {
std::vector<ReferenceWithContext> results;
auto sym_it = workspace.project_index.symbols.find(hash);
if(sym_it != workspace.project_index.symbols.end()) {
for(auto file_id: sym_it->second.reference_files) {
if(is_proj_path_open(file_id))
continue;
auto shard_it = workspace.merged_indices.find(file_id);
if(shard_it == workspace.merged_indices.end())
continue;
auto* m = shard_it->second.mapper();
if(!m)
continue;
auto content = shard_it->second.index.content();
auto file_path = workspace.project_index.path_pool.path(file_id);
shard_it->second.index.lookup(hash, kind, [&](const index::Relation& r) {
auto start = m->to_position(r.range.begin);
if(!start)
return true;
results.push_back(ReferenceWithContext{
.file = file_path.str(),
.line = static_cast<int>(start->line) + 1,
.context = extract_line(content, r.range.begin),
});
return true;
});
}
}
for(auto& [id, sess]: sessions) {
if(!sess.file_index || !sess.file_index->mapper)
continue;
auto it = sess.file_index->file_index.relations.find(hash);
if(it == sess.file_index->file_index.relations.end())
continue;
auto file_path = workspace.path_pool.resolve(id);
llvm::StringRef content = sess.file_index->content;
for(auto& rel: it->second) {
if(rel.kind != kind)
continue;
auto start = sess.file_index->mapper->to_position(rel.range.begin);
if(!start)
continue;
results.push_back(ReferenceWithContext{
.file = file_path.str(),
.line = static_cast<int>(start->line) + 1,
.context = extract_line(content, rel.range.begin),
});
}
}
return results;
}
std::vector<protocol::CallHierarchyIncomingCall>
Indexer::find_incoming_calls(index::SymbolHash hash) {
llvm::DenseMap<index::SymbolHash, std::vector<protocol::Range>> caller_ranges;
@@ -788,11 +642,6 @@ void Indexer::resume_indexing() {
}
}
kota::task<> Indexer::stop() {
bg_tasks.cancel();
co_await bg_tasks.join();
}
void Indexer::schedule() {
if(!*workspace.config.project.enable_indexing || indexing_active || indexing_scheduled)
return;
@@ -802,11 +651,7 @@ void Indexer::schedule() {
index_idle_timer = std::make_shared<kota::timer>(kota::timer::create(loop));
}
index_idle_timer->start(std::chrono::milliseconds(*workspace.config.project.idle_timeout_ms));
if(!bg_tasks.spawn(run_background_indexing())) {
indexing_scheduled = false;
LOG_WARN("Failed to spawn background indexing task (task group stopped)");
}
loop.schedule(run_background_indexing());
}
kota::task<> Indexer::index_one(std::uint32_t server_path_id) {
@@ -849,14 +694,18 @@ kota::task<> Indexer::index_one(std::uint32_t server_path_id) {
}
}
kota::task<> Indexer::monitor_resources() {
while(true) {
co_await kota::sleep(std::chrono::milliseconds(3000));
kota::task<> Indexer::monitor_resources(std::uint32_t generation) {
while(generation == monitor_generation) {
co_await kota::sleep(std::chrono::milliseconds(3000), loop);
if(generation != monitor_generation)
break;
auto mem = kota::sys::memory();
if(mem.total == 0)
continue;
// Respect cgroup/container limits when present.
auto effective_total =
(mem.constrained > 0 && mem.constrained < mem.total) ? mem.constrained : mem.total;
auto ratio = static_cast<double>(mem.available) / static_cast<double>(effective_total);
@@ -887,73 +736,87 @@ kota::task<> Indexer::run_background_indexing() {
}
indexing_active = true;
++monitor_generation;
loop.schedule(monitor_resources(monitor_generation));
kota::cancellation_source monitor_cancel;
bg_tasks.spawn(kota::with_token(monitor_resources(), monitor_cancel.token()));
// Put module interface units first so their PCMs are built before
// non-module files that might import them.
std::stable_partition(
index_queue.begin() + index_queue_pos,
index_queue.end(),
[this](std::uint32_t id) { return workspace.path_to_module.contains(id); });
auto total = index_queue.size() - index_queue_pos;
auto batch = index_queue.size() - index_queue_pos;
std::size_t dispatched = 0;
std::size_t completed = 0;
finished = 0;
// Progress reporting via LSP $/progress.
std::optional<lsp::ProgressReporter<kota::ipc::JsonPeer>> progress;
if(peer) {
progress.emplace(*peer, protocol::ProgressToken(std::string("clice/backgroundIndex")));
auto create_result = co_await progress->create();
if(!create_result.has_error()) {
progress->begin("Indexing", std::format("0/{} files", total), 0);
progress->begin("Indexing", std::format("0/{} files", batch), 0);
} else {
progress.reset();
}
}
kota::task_group<> workers(loop);
std::size_t in_flight = 0;
kota::event slot_available;
while(index_queue_pos < index_queue.size()) {
if(pause_depth > 0)
co_await resume_event.wait();
auto server_path_id = index_queue[index_queue_pos++];
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
if(sessions.contains(server_path_id) || !need_update(file_path)) {
++completed;
continue;
}
while(in_flight >= max_concurrent) {
slot_available.reset();
co_await slot_available.wait();
}
++in_flight;
++dispatched;
workers.spawn([&, server_path_id]() -> kota::task<> {
co_await index_one(server_path_id);
--in_flight;
++completed;
if(progress) {
auto pct = total > 0 ? static_cast<std::uint32_t>(completed * 100 / total) : 100;
progress->report(std::format("{}/{} files", completed, total), pct);
while(index_queue_pos < index_queue.size() || inflight > 0) {
// Dispatch new tasks up to max_concurrent.
while(index_queue_pos < index_queue.size() && inflight < max_concurrent) {
// Wait if paused by a user request.
if(pause_depth > 0) {
co_await resume_event.wait();
}
slot_available.set();
}());
}
co_await workers.join();
auto server_path_id = index_queue[index_queue_pos++];
// Quick pre-filter: skip open files and fresh files without
// consuming a concurrency slot.
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
if(sessions.contains(server_path_id) || !need_update(file_path)) {
++completed;
continue;
}
++inflight;
++dispatched;
// Launch the index task. On completion it decrements
// inflight, bumps finished, and signals the event.
loop.schedule([](Indexer* self, std::uint32_t id, kota::event& done) -> kota::task<> {
co_await self->index_one(id);
--self->inflight;
++self->finished;
done.set();
}(this, server_path_id, completion_event));
}
if(inflight == 0)
break;
// Wait for at least one task to finish.
co_await completion_event.wait();
completion_event.reset();
// Drain all completions that occurred since last wake.
completed += std::exchange(finished, 0);
// Report progress.
if(progress) {
auto pct = batch > 0 ? static_cast<std::uint32_t>(completed * 100 / batch) : 100;
progress->report(std::format("{}/{} files", completed, batch), pct);
}
}
if(progress) {
progress->end(std::format("Indexed {} files", dispatched));
}
monitor_cancel.cancel();
indexing_active = false;
++monitor_generation; // Stop the monitor coroutine.
LOG_INFO("Background indexing complete: {} files dispatched", dispatched);
save(workspace.config.project.index_dir);
}

View File

@@ -9,7 +9,7 @@
#include "semantic/relation_kind.h"
#include "semantic/symbol_kind.h"
#include "server/workspace/workspace.h"
#include "server/workspace.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/json.h"
@@ -61,8 +61,8 @@ public:
WorkerPool& pool,
Compiler& compiler,
std::function<bool(std::uint32_t)> is_file_open = {}) :
loop(loop), bg_tasks(loop), workspace(workspace), sessions(sessions), pool(pool),
compiler(compiler), is_file_open(std::move(is_file_open)) {}
loop(loop), workspace(workspace), sessions(sessions), pool(pool), compiler(compiler),
is_file_open(std::move(is_file_open)) {}
/// Set the LSP peer for progress reporting. Must be called before
/// schedule() if progress notifications are desired.
@@ -167,43 +167,6 @@ public:
std::vector<protocol::SymbolInformation> search_symbols(llvm::StringRef query,
std::size_t max_results = 100);
struct DefinitionText {
std::string file;
int start_line;
int end_line;
std::string text;
};
/// Get full definition text for a symbol, using stored index ranges and content.
std::optional<DefinitionText> get_definition_text(index::SymbolHash hash);
struct ReferenceWithContext {
std::string file;
int line;
std::string context;
};
/// Collect references (or definitions) with context lines from stored content.
std::vector<ReferenceWithContext> collect_references(index::SymbolHash hash, RelationKind kind);
/// Cancel background indexing and wait for all tasks to settle.
kota::task<> stop();
/// Whether background indexing is currently idle (no active or queued work).
bool is_idle() const {
return !indexing_active && index_queue_pos >= index_queue.size();
}
/// Number of files remaining in the indexing queue.
std::size_t pending_files() const {
return index_queue_pos < index_queue.size() ? index_queue.size() - index_queue_pos : 0;
}
/// Total files that were enqueued in the current (or last) indexing round.
std::size_t total_queued() const {
return index_queue.size();
}
/// Convert internal SymbolKind to LSP SymbolKind.
static protocol::SymbolKind to_lsp_symbol_kind(SymbolKind kind);
@@ -245,7 +208,6 @@ private:
private:
kota::event_loop& loop;
kota::task_group<> bg_tasks;
Workspace& workspace;
llvm::DenseMap<std::uint32_t, Session>& sessions;
WorkerPool& pool;
@@ -269,15 +231,27 @@ private:
/// Concurrency control for background indexing.
std::size_t max_concurrent = 2;
std::size_t baseline_concurrent = 2;
std::size_t inflight = 0;
std::size_t finished = 0; ///< Incremented by each completed dispatch task.
/// Pause/resume: when paused, new index tasks wait on this event.
/// Uses a counter so nested pause/resume pairs work correctly.
std::size_t pause_depth = 0;
kota::event resume_event{true};
/// Completion event — signalled by each finished dispatch task so the
/// main loop can wake up. Must be a member (not local to the coroutine)
/// because inflight tasks capture it by reference and may outlive the
/// coroutine frame during server shutdown.
kota::event completion_event;
/// Generation counter — incremented each run so a stale monitor_resources
/// coroutine can detect that its owning run has ended.
std::uint32_t monitor_generation = 0;
kota::task<> run_background_indexing();
kota::task<> index_one(std::uint32_t server_path_id);
kota::task<> monitor_resources();
kota::task<> monitor_resources(std::uint32_t generation);
};
} // namespace clice

View File

@@ -1,4 +1,4 @@
#include "server/service/lsp_client.h"
#include "server/master_server.h"
#include <algorithm>
#include <format>
@@ -7,9 +7,7 @@
#include <variant>
#include "semantic/symbol_kind.h"
#include "server/protocol/extension.h"
#include "server/protocol/worker.h"
#include "server/service/master_server.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "support/logging.h"
@@ -18,6 +16,7 @@
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/uri.h"
#include "kota/meta/enum.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
@@ -30,39 +29,177 @@ using kota::ipc::RequestResult;
using RequestContext = kota::ipc::JsonPeer::RequestContext;
using serde_raw = kota::codec::RawValue;
/// Serialize a value to a JSON RawValue using LSP config.
template <typename T>
static serde_raw to_raw(const T& value) {
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
return serde_raw{json ? std::move(*json) : "null"};
}
LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(server), peer(peer) {
server.compiler.set_peer(&peer);
server.indexer.set_peer(&peer);
MasterServer::MasterServer(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
std::string self_path) :
loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions),
indexer(loop,
workspace,
sessions,
pool,
compiler,
[this](uint32_t proj_path_id) {
// Bridge project-level path_id to server-level path_id.
// The two PathPools may assign different IDs to the same path.
auto path = workspace.project_index.path_pool.path(proj_path_id);
auto server_id = workspace.path_pool.intern(path);
return sessions.contains(server_id);
}),
self_path(std::move(self_path)) {}
MasterServer::~MasterServer() = default;
void MasterServer::load_workspace() {
if(workspace_root.empty())
return;
auto& cfg = workspace.config.project;
if(!cfg.cache_dir.empty()) {
auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
if(ec) {
LOG_WARN("Failed to create cache directory {}: {}",
std::string_view(cfg.cache_dir),
ec.message());
} else {
LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
}
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
auto dir = path::join(cfg.cache_dir, subdir);
if(auto ec2 = llvm::sys::fs::create_directories(dir))
LOG_WARN("Failed to create {}: {}", dir, ec2.message());
}
workspace.cleanup_cache();
workspace.load_cache();
}
// Discover compile_commands.json: configured paths first, then auto-scan.
std::string cdb_path;
for(auto& configured: cfg.compile_commands_paths) {
// Each entry can be a file or a directory containing compile_commands.json.
if(llvm::sys::fs::is_directory(configured)) {
auto candidate = path::join(configured, "compile_commands.json");
if(llvm::sys::fs::exists(candidate)) {
cdb_path = std::move(candidate);
break;
}
} else if(llvm::sys::fs::exists(configured)) {
cdb_path = configured;
break;
} else {
LOG_WARN("Configured compile_commands_path not found: {}", configured);
}
}
// Auto-scan: workspace root + all immediate subdirectories.
if(cdb_path.empty()) {
auto try_candidate = [&](llvm::StringRef dir) -> bool {
auto candidate = path::join(dir, "compile_commands.json");
if(llvm::sys::fs::exists(candidate)) {
cdb_path = std::move(candidate);
return true;
}
return false;
};
if(!try_candidate(workspace_root)) {
std::error_code ec;
for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec;
it.increment(ec)) {
if(it->type() == llvm::sys::fs::file_type::directory_file) {
if(try_candidate(it->path()))
break;
}
}
}
}
if(cdb_path.empty()) {
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
return;
}
auto count = workspace.cdb.load(cdb_path);
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);
auto report = scan_dependency_graph(workspace.cdb,
workspace.path_pool,
workspace.dep_graph,
/*cache=*/nullptr,
[this](llvm::StringRef path,
std::vector<std::string>& append,
std::vector<std::string>& remove) {
workspace.config.match_rules(path, append, remove);
});
workspace.dep_graph.build_reverse_map();
auto unresolved = report.includes_found - report.includes_resolved;
double accuracy =
report.includes_found > 0
? 100.0 * static_cast<double>(report.includes_resolved) / report.includes_found
: 100.0;
LOG_INFO(
"Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves",
report.elapsed_ms,
report.total_files,
report.source_files,
report.header_files,
report.total_edges,
report.includes_resolved,
report.includes_found,
accuracy,
report.waves);
if(unresolved > 0)
LOG_WARN("{} unresolved includes", unresolved);
workspace.build_module_map();
indexer.load(cfg.index_dir);
if(*cfg.enable_indexing) {
for(auto& entry: workspace.cdb.get_entries()) {
auto file = workspace.cdb.resolve_path(entry.file);
auto server_id = workspace.path_pool.intern(file);
indexer.enqueue(server_id);
}
indexer.schedule();
}
compiler.init_compile_graph();
}
void MasterServer::register_handlers() {
using StringVec = std::vector<std::string>;
peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
-> RequestResult<protocol::InitializeParams> {
auto& srv = this->server;
if(srv.lifecycle != ServerLifecycle::Uninitialized) {
if(lifecycle != ServerLifecycle::Uninitialized) {
co_return kota::outcome_error(protocol::Error{"Server already initialized"});
}
auto& init = params.lsp__initialize_params;
if(init.root_uri.has_value()) {
srv.workspace_root = uri_to_path(*init.root_uri);
workspace_root = uri_to_path(*init.root_uri);
}
// Capture initializationOptions as raw JSON for config loading.
if(init.initialization_options.has_value()) {
auto json =
kota::codec::json::to_json<kota::ipc::lsp_config>(*init.initialization_options);
if(json)
srv.init_options_json = std::move(*json);
init_options_json = std::move(*json);
}
srv.lifecycle = ServerLifecycle::Initialized;
LOG_INFO("Initialized with workspace: {}", srv.workspace_root);
lifecycle = ServerLifecycle::Initialized;
LOG_INFO("Initialized with workspace: {}", workspace_root);
protocol::InitializeResult result;
auto& caps = result.capabilities;
@@ -85,6 +222,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
caps.signature_help_provider = protocol::SignatureHelpOptions{
.trigger_characters = StringVec{"(", ")", "{", "}", "<", ">", ","},
};
/// FIXME: In the future, we would support work done progress.
caps.declaration_provider = protocol::DeclarationOptions{
.work_done_progress = false,
};
@@ -108,8 +246,6 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
caps.call_hierarchy_provider = true;
caps.type_hierarchy_provider = true;
caps.workspace_symbol_provider = true;
caps.document_formatting_provider = true;
caps.document_range_formatting_provider = true;
protocol::SemanticTokensOptions sem_opts;
{
@@ -141,32 +277,103 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
co_return result;
});
peer.on_notification([this]([[maybe_unused]] const protocol::InitializedParams& params) {
this->server.initialize();
peer.on_notification([this](const protocol::InitializedParams& params) {
// Config priority: initializationOptions > clice.toml > defaults.
// Load the workspace config (with defaults applied) first, then overlay
// any initializationOptions on top so fields not mentioned in the JSON
// keep the values from clice.toml — kotatsu's deserializer only touches
// fields that are present in the input.
workspace.config = Config::load_from_workspace(workspace_root);
if(!init_options_json.empty()) {
if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
} else {
// Re-run apply_defaults so overridden strings get workspace
// substitution and `compiled_rules` is rebuilt if `rules`
// changed. Defaults are gated on zero/empty sentinels, so
// existing values from the overlay are preserved.
workspace.config.apply_defaults(workspace_root);
LOG_INFO("Applied initializationOptions overlay");
}
init_options_json.clear();
}
auto& cfg = workspace.config.project;
if(!cfg.logging_dir.empty()) {
auto now = std::chrono::system_clock::now();
auto pid = llvm::sys::Process::getProcessId();
auto session_dir =
path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
logging::file_logger("master", session_dir, logging::options);
session_log_dir = session_dir;
}
LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)",
cfg.stateful_worker_count.value,
cfg.stateless_worker_count.value,
*cfg.idle_timeout_ms);
WorkerPoolOptions pool_opts;
pool_opts.self_path = self_path;
pool_opts.stateful_count = cfg.stateful_worker_count;
pool_opts.stateless_count = cfg.stateless_worker_count;
pool_opts.worker_memory_limit = cfg.worker_memory_limit;
pool_opts.log_dir = session_log_dir;
if(!pool.start(pool_opts)) {
LOG_ERROR("Failed to start worker pool");
return;
}
lifecycle = ServerLifecycle::Ready;
compiler.on_indexing_needed = [this]() {
indexer.schedule();
};
indexer.set_peer(&peer);
indexer.set_max_concurrency(cfg.stateless_worker_count.value);
load_workspace();
});
peer.on_request(
[this](RequestContext& ctx,
const protocol::ShutdownParams& params) -> RequestResult<protocol::ShutdownParams> {
this->server.lifecycle = ServerLifecycle::ShuttingDown;
lifecycle = ServerLifecycle::ShuttingDown;
LOG_INFO("Shutdown requested");
co_return nullptr;
});
peer.on_notification([this]([[maybe_unused]] const protocol::ExitParams& params) {
peer.on_notification([this](const protocol::ExitParams& params) {
lifecycle = ServerLifecycle::Exited;
LOG_INFO("Exit notification received");
this->server.schedule_shutdown();
indexer.save(workspace.config.project.index_dir);
workspace.save_cache();
loop.schedule([this]() -> kota::task<> {
co_await pool.stop();
loop.stop();
}());
});
/// Document lifecycle — handled directly by MasterServer.
peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) {
auto& srv = this->server;
if(srv.lifecycle != ServerLifecycle::Ready)
if(lifecycle != ServerLifecycle::Ready)
return;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto path_id = workspace.path_pool.intern(path);
auto& session = srv.open_session(path_id);
auto [it, inserted] = sessions.try_emplace(path_id);
auto& session = it->second;
if(!inserted) {
// DenseMap tombstone may retain stale data — reset to a fresh Session.
session = Session{};
}
session.path_id = path_id;
session.version = params.text_document.version;
session.text = params.text_document.text;
session.generation++;
@@ -175,18 +382,18 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
});
peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) {
auto& srv = this->server;
if(srv.lifecycle != ServerLifecycle::Ready)
if(lifecycle != ServerLifecycle::Ready)
return;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto path_id = workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto it = sessions.find(path_id);
if(it == sessions.end())
return;
session->version = params.text_document.version;
auto& session = it->second;
session.version = params.text_document.version;
for(auto& change: params.content_changes) {
std::visit(
@@ -194,157 +401,186 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
using T = std::remove_cvref_t<decltype(c)>;
if constexpr(std::is_same_v<T,
protocol::TextDocumentContentChangeWholeDocument>) {
session->text = c.text;
session.text = c.text;
} else {
auto& range = c.range;
lsp::PositionMapper mapper(session->text, lsp::PositionEncoding::UTF16);
lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16);
auto start = mapper.to_offset(range.start);
auto end = mapper.to_offset(range.end);
if(start && end && *start <= *end) {
session->text.replace(*start, *end - *start, c.text);
session.text.replace(*start, *end - *start, c.text);
}
}
},
change);
}
session->generation++;
session->ast_dirty = true;
session.generation++;
session.ast_dirty = true;
LOG_DEBUG("didChange: path={} version={} gen={}",
path,
session->version,
session->generation);
session.version,
session.generation);
worker::DocumentUpdateParams update;
update.path = path;
update.version = session->version;
srv.pool.notify_stateful(path_id, update);
update.version = session.version;
pool.notify_stateful(path_id, update);
});
peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) {
auto& srv = this->server;
if(srv.lifecycle != ServerLifecycle::Ready)
return;
auto path_id = srv.workspace.path_pool.intern(uri_to_path(params.text_document.uri));
srv.close_session(path_id, this->peer);
});
peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) {
auto& srv = this->server;
if(srv.lifecycle != ServerLifecycle::Ready)
if(lifecycle != ServerLifecycle::Ready)
return;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
srv.on_file_saved(path_id);
auto path_id = workspace.path_pool.intern(path);
workspace.on_file_closed(path_id);
pool.notify_stateful(path_id, worker::EvictParams{path});
// Clear diagnostics for the closed file.
protocol::PublishDiagnosticsParams diag_params;
diag_params.uri = params.text_document.uri;
peer.send_notification(diag_params);
sessions.erase(path_id);
indexer.enqueue(path_id);
indexer.schedule();
LOG_DEBUG("didClose: {}", path);
});
peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) {
if(lifecycle != ServerLifecycle::Ready)
return;
auto path = uri_to_path(params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto dirtied = workspace.on_file_saved(path_id);
for(auto dirty_id: dirtied) {
if(auto sit = sessions.find(dirty_id); sit != sessions.end()) {
sit->second.ast_dirty = true;
} else {
indexer.enqueue(dirty_id);
}
}
// Invalidate header contexts for sessions whose host is this file.
for(auto& [hdr_id, session]: sessions) {
if(session.header_context && session.header_context->host_path_id == path_id) {
session.header_context.reset();
session.ast_dirty = true;
}
}
indexer.schedule();
LOG_DEBUG("didSave: {}", path);
});
/// Feature requests — stateful forwarding.
peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await srv.compiler.forward_query(
worker::QueryKind::Hover,
*session,
params.text_document_position_params.position);
co_return co_await compiler.forward_query(worker::QueryKind::Hover,
sit->second,
params.text_document_position_params.position);
});
peer.on_request([this](RequestContext& ctx,
const protocol::SemanticTokensParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await srv.compiler.forward_query(worker::QueryKind::SemanticTokens, *session);
co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await srv.compiler.forward_query(worker::QueryKind::InlayHints,
*session,
{},
params.range);
co_return co_await compiler.forward_query(worker::QueryKind::InlayHints,
sit->second,
{},
params.range);
});
peer.on_request([this](RequestContext& ctx,
const protocol::FoldingRangeParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
co_return serde_raw{"null"};
co_return co_await srv.compiler.forward_query(worker::QueryKind::FoldingRange, *session);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult {
auto path = uri_to_path(params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second);
});
peer.on_request([this](RequestContext& ctx,
const protocol::DocumentSymbolParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await srv.compiler.forward_query(worker::QueryKind::DocumentSymbol, *session);
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
});
peer.on_request([this](RequestContext& ctx,
const protocol::DocumentLinkParams& params) -> RawResult {
auto path = uri_to_path(params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
auto& session = sit->second;
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
if(!result.has_value())
co_return serde_raw{"null"};
// Merge document links from PCH if available.
auto& links = result.value();
// Re-lookup session after co_await since iterators may be invalidated.
auto sit2 = sessions.find(path_id);
if(sit2 != sessions.end() && sit2->second.pch_ref) {
auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id);
if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) {
auto& pch_json = pch_it->second.document_links_json;
// Merge two JSON arrays.
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
// "[a,b]" + "[c,d]" -> "[a,b,c,d]"
links.data.pop_back(); // remove trailing ']'
links.data += ',';
links.data.append(pch_json.begin() + 1, pch_json.end()); // skip '['
} else {
links.data = pch_json;
}
}
}
co_return std::move(links);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
co_return serde_raw{"null"};
auto result =
co_await srv.compiler.forward_query(worker::QueryKind::DocumentLink, *session);
if(!result.has_value())
co_return serde_raw{"null"};
auto& links = result.value();
auto* session2 = srv.find_session(path_id);
if(session2 && session2->pch_ref) {
auto& pch_cache = srv.workspace.pch_cache;
auto pch_it = pch_cache.find(session2->pch_ref->path_id);
if(pch_it != pch_cache.end() && !pch_it->second.document_links_json.empty()) {
auto& pch_json = pch_it->second.document_links_json;
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
links.data.pop_back();
links.data += ',';
links.data.append(pch_json.begin() + 1, pch_json.end());
} else {
links.data = pch_json;
}
}
}
co_return std::move(links);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await srv.compiler.forward_query(worker::QueryKind::CodeAction, *session);
co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second);
});
/// Helper: resolve URI to path, path_id, and Session pointer.
auto resolve_uri = [this](const std::string& uri) {
struct Result {
std::string path;
@@ -352,21 +588,22 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
Session* session;
};
auto path = uri_to_path(uri);
auto path_id = this->server.workspace.path_pool.intern(path);
auto* session = this->server.find_session(path_id);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
Session* session = (sit != sessions.end()) ? &sit->second : nullptr;
return Result{std::move(path), path_id, session};
};
auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) {
auto [path, path_id, session] = resolve_uri(uri);
return this->server.indexer.lookup_symbol(uri, path, pos, session);
return indexer.lookup_symbol(uri, path, pos, session);
};
auto query_at = [this, resolve_uri](const std::string& uri,
const protocol::Position& pos,
RelationKind kind) -> std::vector<protocol::Location> {
auto [path, path_id, session] = resolve_uri(uri);
return this->server.indexer.query_relations(path, pos, kind, session);
return indexer.query_relations(path, pos, kind, session);
};
auto resolve_item =
@@ -375,9 +612,11 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
const protocol::Range& range,
const std::optional<protocol::LSPAny>& data) -> std::optional<SymbolInfo> {
auto [path, path_id, session] = resolve_uri(uri);
return this->server.indexer.resolve_hierarchy_item(uri, path, range, data, session);
return indexer.resolve_hierarchy_item(uri, path, range, data, session);
};
/// Feature requests — index-based with AST fallback.
peer.on_request([this, query_at](RequestContext& ctx,
const protocol::DefinitionParams& params) -> RawResult {
auto& uri = params.text_document_position_params.text_document.uri;
@@ -388,15 +627,14 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
co_return to_raw(result);
}
auto& srv = this->server;
auto path = uri_to_path(uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await srv.compiler.forward_query(worker::QueryKind::GoToDefinition,
*session,
pos);
co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition,
sit->second,
pos);
});
peer.on_request([this, query_at](RequestContext& ctx,
@@ -433,61 +671,38 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
co_return serde_raw{"null"};
});
peer.on_request([this](RequestContext& ctx,
const protocol::CompletionParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
co_return serde_raw{"null"};
auto pause = srv.indexer.scoped_pause();
auto result =
co_await srv.compiler.handle_completion(params.text_document_position_params.position,
*session);
co_return std::move(result);
});
/// Feature requests — stateless forwarding.
peer.on_request(
[this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult {
auto& srv = this->server;
[this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult {
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
auto pause = srv.indexer.scoped_pause();
auto pause = indexer.scoped_pause();
auto result =
co_await srv.compiler.forward_build(worker::BuildKind::SignatureHelp,
params.text_document_position_params.position,
*session);
co_await compiler.handle_completion(params.text_document_position_params.position,
sit->second);
co_return std::move(result);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::DocumentFormattingParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
co_return serde_raw{"null"};
auto pause = srv.indexer.scoped_pause();
co_return co_await srv.compiler.forward_format(*session);
});
peer.on_request([this](RequestContext& ctx,
const protocol::DocumentRangeFormattingParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.text_document.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto* session = srv.find_session(path_id);
if(!session)
const protocol::SignatureHelpParams& params) -> RawResult {
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
auto pause = srv.indexer.scoped_pause();
co_return co_await srv.compiler.forward_format(*session, params.range);
auto pause = indexer.scoped_pause();
auto result = co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
params.text_document_position_params.position,
sit->second);
co_return std::move(result);
});
/// Hierarchy queries — index-based.
peer.on_request(
[this, lookup_at](RequestContext& ctx,
const protocol::CallHierarchyPrepareParams& params) -> RawResult {
@@ -511,7 +726,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_return serde_raw{"null"};
auto results = this->server.indexer.find_incoming_calls(info->hash);
auto results = indexer.find_incoming_calls(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
@@ -523,7 +738,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_return serde_raw{"null"};
auto results = this->server.indexer.find_outgoing_calls(info->hash);
auto results = indexer.find_outgoing_calls(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
@@ -553,7 +768,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_return serde_raw{"null"};
auto results = this->server.indexer.find_supertypes(info->hash);
auto results = indexer.find_supertypes(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
@@ -565,7 +780,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_return serde_raw{"null"};
auto results = this->server.indexer.find_subtypes(info->hash);
auto results = indexer.find_subtypes(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
@@ -573,29 +788,29 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
peer.on_request(
[this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult {
auto results = this->server.indexer.search_symbols(params.query);
auto results = indexer.search_symbols(params.query);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
});
/// clice/ extension commands.
peer.on_request(
"clice/queryContext",
[this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto path_id = workspace.path_pool.intern(path);
int offset_val = std::max(0, params.offset.value_or(0));
constexpr int page_size = 10;
ext::QueryContextResult result;
std::vector<ext::ContextItem> all_items;
auto& ws = srv.workspace;
auto hosts = ws.dep_graph.find_host_sources(path_id);
auto hosts = workspace.dep_graph.find_host_sources(path_id);
for(auto host_id: hosts) {
auto host_path = ws.path_pool.resolve(host_id);
auto host_cdb = ws.cdb.lookup(host_path, {.suppress_logging = true});
auto host_path = workspace.path_pool.resolve(host_id);
auto host_cdb = workspace.cdb.lookup(host_path, {.suppress_logging = true});
if(host_cdb.empty())
continue;
auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path));
@@ -609,7 +824,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
}
if(hosts.empty()) {
auto entries = ws.cdb.lookup(path, {.suppress_logging = true});
auto entries = workspace.cdb.lookup(path, {.suppress_logging = true});
for(std::size_t i = 0; i < entries.size(); ++i) {
auto& cmd = entries[i];
auto argv = cmd.to_argv();
@@ -651,14 +866,13 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
peer.on_request(
"clice/currentContext",
[this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto path_id = workspace.path_pool.intern(path);
ext::CurrentContextResult result;
auto* session = srv.find_session(path_id);
if(session && session->active_context) {
auto ctx_path = srv.workspace.path_pool.resolve(*session->active_context);
auto sit = sessions.find(path_id);
if(sit != sessions.end() && sit->second.active_context) {
auto ctx_path = workspace.path_pool.resolve(*sit->second.active_context);
auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path));
if(ctx_uri_opt) {
ext::ContextItem item;
@@ -674,41 +888,34 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
peer.on_request(
"clice/switchContext",
[this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult {
auto& srv = this->server;
auto path = uri_to_path(params.uri);
auto path_id = srv.workspace.path_pool.intern(path);
auto path_id = workspace.path_pool.intern(path);
auto context_path = uri_to_path(params.context_uri);
auto context_path_id = srv.workspace.path_pool.intern(context_path);
auto context_path_id = workspace.path_pool.intern(context_path);
ext::SwitchContextResult result;
auto& ws = srv.workspace;
auto context_cdb = ws.cdb.lookup(context_path, {.suppress_logging = true});
auto context_cdb = workspace.cdb.lookup(context_path, {.suppress_logging = true});
if(context_cdb.empty()) {
result.success = false;
co_return to_raw(result);
}
auto* session = srv.find_session(path_id);
if(!session) {
auto sit = sessions.find(path_id);
if(sit == sessions.end()) {
result.success = false;
co_return to_raw(result);
}
session->active_context = context_path_id;
session->header_context.reset();
session->pch_ref.reset();
session->ast_deps.reset();
session->ast_dirty = true;
sit->second.active_context = context_path_id;
sit->second.header_context.reset();
sit->second.pch_ref.reset();
sit->second.ast_deps.reset();
sit->second.ast_dirty = true;
result.success = true;
co_return to_raw(result);
});
}
LSPClient::~LSPClient() {
server.compiler.set_peer(nullptr);
server.indexer.set_peer(nullptr);
}
} // namespace clice

View File

@@ -0,0 +1,81 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "server/compiler.h"
#include "server/indexer.h"
#include "server/session.h"
#include "server/worker_pool.h"
#include "server/workspace.h"
#include "kota/async/async.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/peer.h"
#include "llvm/ADT/DenseMap.h"
namespace clice {
enum class ServerLifecycle : std::uint8_t {
Uninitialized,
Initialized,
Ready,
ShuttingDown,
Exited,
};
/// Top-level LSP server — the single orchestration point for the language
/// server process.
///
/// Responsibilities:
/// - Owns the two-layer state model: Workspace (disk truth) and Sessions
/// (per-open-file volatile state).
/// - Manages Session lifecycle directly: didOpen creates, didChange mutates,
/// didSave syncs to Workspace, didClose destroys.
/// - Dispatches compilation and feature queries to Compiler.
/// - Dispatches index lookups and background indexing to Indexer.
///
/// Design principle:
/// Open files are never depended upon by other files. Dependencies always
/// point to disk files. The only path from Session to Workspace is didSave.
class MasterServer {
public:
MasterServer(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path);
~MasterServer();
void register_handlers();
private:
kota::event_loop& loop;
kota::ipc::JsonPeer& peer;
/// Persistent project-wide state (config, CDB, path pool, dependency
/// graphs, compilation caches, symbol index).
Workspace workspace;
/// Per-file editing sessions, keyed by server-level path_id.
llvm::DenseMap<std::uint32_t, Session> sessions;
/// Worker process pool for offloading compilation and queries.
WorkerPool pool;
/// Compilation lifecycle manager (reads/writes workspace and sessions).
Compiler compiler;
/// Index query and background scheduling (reads from workspace and sessions).
Indexer indexer;
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
std::string self_path;
std::string workspace_root;
std::string session_log_dir;
std::string init_options_json; ///< Raw JSON from initializationOptions, consumed once.
void load_workspace();
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
};
} // namespace clice

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
@@ -9,6 +10,7 @@
#include "syntax/token.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/protocol.h"
namespace clice::worker {
@@ -64,7 +66,6 @@ enum class BuildKind : uint8_t {
Index,
Completion,
SignatureHelp,
Format,
};
/// Unified parameters for all stateless build/compilation tasks.
@@ -75,7 +76,6 @@ enum class BuildKind : uint8_t {
/// - Index: + pcms
/// - Completion: + text, version, offset, pch, pcms
/// - SignatureHelp: + text, version, offset, pch, pcms
/// - Format: + text, format_range (optional)
struct BuildParams {
BuildKind kind;
std::string file;
@@ -92,7 +92,6 @@ struct BuildParams {
std::string output_path; ///< BuildPCH, BuildPCM
std::string module_name; ///< BuildPCM
uint32_t preamble_bound = UINT32_MAX; ///< BuildPCH
LocalSourceRange format_range; ///< Format (default = full document)
};
/// Unified result for stateless build tasks.
@@ -123,6 +122,43 @@ struct EvictedParams {
} // namespace clice::worker
namespace clice::ext {
struct ContextItem {
std::string label;
std::string description;
std::string uri;
};
struct QueryContextParams {
std::string uri;
std::optional<int> offset;
};
struct QueryContextResult {
std::vector<ContextItem> contexts;
int total;
};
struct CurrentContextParams {
std::string uri;
};
struct CurrentContextResult {
std::optional<ContextItem> context;
};
struct SwitchContextParams {
std::string uri;
std::string context_uri;
};
struct SwitchContextResult {
bool success;
};
} // namespace clice::ext
namespace kota::ipc::protocol {
template <>

View File

@@ -1,297 +0,0 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "kota/ipc/protocol.h"
namespace clice::agentic {
struct CompileCommandParams {
std::string path;
};
struct CompileCommandResult {
std::string file;
std::string directory;
std::vector<std::string> arguments;
};
struct FileInfo {
std::string path;
std::string kind;
std::optional<std::string> module_name;
};
struct ProjectFilesParams {
std::optional<std::string> filter;
};
struct ProjectFilesResult {
std::vector<FileInfo> files;
int total = 0;
};
struct DepEntry {
std::string path;
int depth = 0;
};
struct FileDepsParams {
std::string path;
std::optional<std::string> direction;
std::optional<int> depth;
};
struct FileDepsResult {
std::string file;
std::vector<DepEntry> includes;
std::vector<DepEntry> includers;
};
struct ImpactAnalysisParams {
std::string path;
};
struct ImpactAnalysisResult {
std::vector<std::string> direct_dependents;
std::vector<std::string> transitive_dependents;
std::vector<std::string> affected_modules;
};
struct SymbolEntry {
std::string name;
std::string kind;
std::string file;
int line = 0;
std::optional<std::string> container;
std::uint64_t symbol_id = 0;
};
struct SymbolSearchParams {
std::string query;
std::optional<std::vector<std::string>> kind_filter;
std::optional<int> max_results;
};
struct SymbolSearchResult {
std::vector<SymbolEntry> symbols;
};
struct ReadSymbolParams {
std::optional<std::string> name;
std::optional<std::string> path;
std::optional<int> line;
std::optional<std::uint64_t> symbol_id;
};
struct ReadSymbolResult {
std::string name;
std::string kind;
std::string file;
int start_line = 0;
int end_line = 0;
std::string text;
std::optional<std::string> signature;
std::uint64_t symbol_id = 0;
};
struct DocumentSymbolEntry {
std::string name;
std::string kind;
int start_line = 0;
int end_line = 0;
std::uint64_t symbol_id = 0;
};
struct DocumentSymbolsParams {
std::string path;
};
struct DocumentSymbolsResult {
std::vector<DocumentSymbolEntry> symbols;
};
struct DefinitionParams {
std::optional<std::string> name;
std::optional<std::string> path;
std::optional<int> line;
std::optional<std::uint64_t> symbol_id;
};
struct LocationEntry {
std::string file;
int start_line = 0;
int end_line = 0;
std::string text;
};
struct DefinitionResult {
std::string name;
std::string kind;
std::uint64_t symbol_id = 0;
std::optional<LocationEntry> definition;
};
struct ReferenceEntry {
std::string file;
int line = 0;
std::string context;
};
struct ReferencesParams {
std::optional<std::string> name;
std::optional<std::string> path;
std::optional<int> line;
std::optional<std::uint64_t> symbol_id;
std::optional<bool> include_declaration;
};
struct ReferencesResult {
std::string name;
std::string kind;
std::uint64_t symbol_id = 0;
std::vector<ReferenceEntry> references;
int total = 0;
};
struct CallGraphEntry {
std::string name;
std::string kind;
std::string file;
int line = 0;
std::uint64_t symbol_id = 0;
};
struct CallGraphParams {
std::optional<std::string> name;
std::optional<std::string> path;
std::optional<int> line;
std::optional<std::uint64_t> symbol_id;
std::optional<std::string> direction;
std::optional<int> depth;
};
struct CallGraphResult {
CallGraphEntry root;
std::vector<CallGraphEntry> callers;
std::vector<CallGraphEntry> callees;
};
struct TypeHierarchyEntry {
std::string name;
std::string kind;
std::string file;
int line = 0;
std::uint64_t symbol_id = 0;
};
struct TypeHierarchyParams {
std::optional<std::string> name;
std::optional<std::string> path;
std::optional<int> line;
std::optional<std::uint64_t> symbol_id;
std::optional<std::string> direction;
};
struct TypeHierarchyResult {
TypeHierarchyEntry root;
std::vector<TypeHierarchyEntry> supertypes;
std::vector<TypeHierarchyEntry> subtypes;
};
struct StatusParams {};
struct StatusResult {
bool idle = true;
int pending = 0;
int total = 0;
int indexed = 0;
};
struct ShutdownParams {};
} // namespace clice::agentic
namespace kota::ipc::protocol {
template <>
struct RequestTraits<clice::agentic::CompileCommandParams> {
using Result = clice::agentic::CompileCommandResult;
constexpr inline static std::string_view method = "agentic/compileCommand";
};
template <>
struct RequestTraits<clice::agentic::ProjectFilesParams> {
using Result = clice::agentic::ProjectFilesResult;
constexpr inline static std::string_view method = "agentic/projectFiles";
};
template <>
struct RequestTraits<clice::agentic::FileDepsParams> {
using Result = clice::agentic::FileDepsResult;
constexpr inline static std::string_view method = "agentic/fileDeps";
};
template <>
struct RequestTraits<clice::agentic::ImpactAnalysisParams> {
using Result = clice::agentic::ImpactAnalysisResult;
constexpr inline static std::string_view method = "agentic/impactAnalysis";
};
template <>
struct RequestTraits<clice::agentic::SymbolSearchParams> {
using Result = clice::agentic::SymbolSearchResult;
constexpr inline static std::string_view method = "agentic/symbolSearch";
};
template <>
struct RequestTraits<clice::agentic::ReadSymbolParams> {
using Result = clice::agentic::ReadSymbolResult;
constexpr inline static std::string_view method = "agentic/readSymbol";
};
template <>
struct RequestTraits<clice::agentic::DocumentSymbolsParams> {
using Result = clice::agentic::DocumentSymbolsResult;
constexpr inline static std::string_view method = "agentic/documentSymbols";
};
template <>
struct RequestTraits<clice::agentic::DefinitionParams> {
using Result = clice::agentic::DefinitionResult;
constexpr inline static std::string_view method = "agentic/definition";
};
template <>
struct RequestTraits<clice::agentic::ReferencesParams> {
using Result = clice::agentic::ReferencesResult;
constexpr inline static std::string_view method = "agentic/references";
};
template <>
struct RequestTraits<clice::agentic::CallGraphParams> {
using Result = clice::agentic::CallGraphResult;
constexpr inline static std::string_view method = "agentic/callGraph";
};
template <>
struct RequestTraits<clice::agentic::TypeHierarchyParams> {
using Result = clice::agentic::TypeHierarchyResult;
constexpr inline static std::string_view method = "agentic/typeHierarchy";
};
template <>
struct RequestTraits<clice::agentic::StatusParams> {
using Result = clice::agentic::StatusResult;
constexpr inline static std::string_view method = "agentic/status";
};
template <>
struct NotificationTraits<clice::agentic::ShutdownParams> {
constexpr inline static std::string_view method = "agentic/shutdown";
};
} // namespace kota::ipc::protocol

View File

@@ -1,42 +0,0 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
namespace clice::ext {
struct ContextItem {
std::string label;
std::string description;
std::string uri;
};
struct QueryContextParams {
std::string uri;
std::optional<int> offset;
};
struct QueryContextResult {
std::vector<ContextItem> contexts;
int total = 0;
};
struct CurrentContextParams {
std::string uri;
};
struct CurrentContextResult {
std::optional<ContextItem> context;
};
struct SwitchContextParams {
std::string uri;
std::string context_uri;
};
struct SwitchContextResult {
bool success = false;
};
} // namespace clice::ext

View File

@@ -1,787 +0,0 @@
#include "server/service/agent_client.h"
#include <algorithm>
#include <format>
#include <ranges>
#include <string>
#include <vector>
#include "server/protocol/agentic.h"
#include "server/service/master_server.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/ipc/lsp/uri.h"
#include "kota/meta/enum.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/SmallVector.h"
namespace clice {
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::JsonPeer::RequestContext;
namespace lsp = kota::ipc::lsp;
namespace protocol = kota::ipc::protocol;
static std::string_view symbol_kind_name(SymbolKind kind) {
constexpr auto names = kota::meta::reflection<SymbolKind::Kind>::member_names;
auto idx = static_cast<std::size_t>(kind.value());
if(idx < names.size())
return names[idx];
return "Unknown";
}
struct ResolvedSymbol {
index::SymbolHash hash = 0;
std::string name;
SymbolKind kind;
std::string file;
int line = 0;
};
static std::vector<ResolvedSymbol> resolve_locator(const agentic::ReadSymbolParams& loc,
Workspace& workspace,
llvm::DenseMap<std::uint32_t, Session>& sessions,
Indexer& indexer) {
if(loc.symbol_id.has_value() && *loc.symbol_id != 0) {
auto hash = static_cast<index::SymbolHash>(*loc.symbol_id);
std::string name;
SymbolKind kind;
if(!indexer.find_symbol_info(hash, name, kind))
return {};
auto def_loc = indexer.find_definition_location(hash);
if(!def_loc)
return {};
auto file = uri_to_path(def_loc->uri);
int line_num = static_cast<int>(def_loc->range.start.line) + 1;
return {
{hash, std::move(name), kind, std::move(file), line_num}
};
}
if(loc.name.has_value() && !loc.name->empty()) {
std::string query_lower = llvm::StringRef(*loc.name).lower();
std::vector<ResolvedSymbol> candidates;
std::vector<ResolvedSymbol> exact_matches;
llvm::DenseSet<index::SymbolHash> seen;
auto try_symbol = [&](index::SymbolHash hash, const index::Symbol& symbol) {
if(symbol.name.empty())
return;
if(llvm::StringRef(symbol.name).lower().find(query_lower) == std::string::npos)
return;
auto def_loc = indexer.find_definition_location(hash);
if(!def_loc)
return;
if(!seen.insert(hash).second)
return;
auto file = uri_to_path(def_loc->uri);
int line_num = static_cast<int>(def_loc->range.start.line) + 1;
if(loc.path.has_value() && !loc.path->empty()) {
llvm::StringRef wanted(*loc.path);
bool basename_only = wanted.find_last_of("/\\") == llvm::StringRef::npos;
if(basename_only) {
if(llvm::sys::path::filename(file) != wanted)
return;
} else if(!llvm::StringRef(file).ends_with(wanted)) {
return;
}
}
bool is_exact = llvm::StringRef(symbol.name).lower() == query_lower ||
llvm::StringRef(symbol.name).ends_with("::" + *loc.name);
ResolvedSymbol rs{hash, symbol.name, symbol.kind, std::move(file), line_num};
if(is_exact)
exact_matches.push_back(std::move(rs));
else
candidates.push_back(std::move(rs));
};
for(auto& [hash, symbol]: workspace.project_index.symbols)
try_symbol(hash, symbol);
for(auto& [_, sess]: sessions) {
if(!sess.file_index)
continue;
for(auto& [hash, symbol]: sess.file_index->symbols)
try_symbol(hash, symbol);
}
if(!exact_matches.empty())
return exact_matches;
return candidates;
}
if(loc.path.has_value() && loc.line.has_value()) {
auto path_str = *loc.path;
auto target_line = static_cast<protocol::uinteger>(*loc.line - 1);
auto pool_it = workspace.path_pool.cache.find(path_str);
auto server_id = pool_it != workspace.path_pool.cache.end() ? pool_it->second : ~0u;
auto* sess =
server_id != ~0u && sessions.contains(server_id) ? &sessions[server_id] : nullptr;
if(sess && sess->file_index) {
auto& fi = *sess->file_index;
if(fi.mapper) {
for(auto& [hash, rels]: fi.file_index.relations) {
for(auto& rel: rels) {
if(rel.kind.value() != RelationKind::Definition)
continue;
auto start = fi.mapper->to_position(rel.range.begin);
if(start && start->line == target_line) {
std::string name;
SymbolKind kind;
if(indexer.find_symbol_info(hash, name, kind))
return {
{hash, std::move(name), kind, path_str, *loc.line}
};
}
}
}
}
}
auto it = workspace.project_index.path_pool.find(path_str);
if(it == workspace.project_index.path_pool.cache.end())
return {};
auto proj_id = it->second;
auto shard_it = workspace.merged_indices.find(proj_id);
if(shard_it == workspace.merged_indices.end())
return {};
for(auto& [hash, symbol]: workspace.project_index.symbols) {
if(!symbol.reference_files.contains(proj_id))
continue;
bool found = false;
shard_it->second.find_relations(hash,
RelationKind::Definition,
[&](const index::Relation&, protocol::Range range) {
if(range.start.line == target_line) {
found = true;
return false;
}
return true;
});
if(found)
return {
{hash, symbol.name, symbol.kind, path_str, *loc.line}
};
}
return {};
}
return {};
}
static std::uint64_t extract_symbol_id(const std::optional<protocol::LSPAny>& data) {
if(!data.has_value())
return 0;
if(auto* val = std::get_if<std::int64_t>(&static_cast<const protocol::LSPVariant&>(*data)))
return static_cast<std::uint64_t>(*val);
LOG_WARN("extract_symbol_id: unexpected LSPAny variant type");
return 0;
}
AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) :
server(server), peer(peer) {
using namespace agentic;
auto& srv = this->server;
peer.on_request(
[&srv](RequestContext&,
const CompileCommandParams& params) -> RequestResult<CompileCommandParams> {
std::string directory;
std::vector<std::string> arguments;
if(!srv.compiler.fill_compile_args(params.path, directory, arguments)) {
co_return kota::outcome_error(
kota::ipc::Error{std::format("no compile command found for {}", params.path)});
}
co_return CompileCommandResult{
.file = params.path,
.directory = std::move(directory),
.arguments = std::move(arguments),
};
});
peer.on_request([&srv](RequestContext&,
const ProjectFilesParams& params) -> RequestResult<ProjectFilesParams> {
auto& ws = srv.workspace;
auto filter = params.filter.value_or("all");
ProjectFilesResult result;
llvm::DenseSet<std::uint32_t> seen;
for(auto& entry: ws.cdb.get_entries()) {
auto file_path = ws.cdb.resolve_path(entry.file);
if(file_path.empty())
continue;
auto proj_it = ws.project_index.path_pool.find(file_path);
if(proj_it != ws.project_index.path_pool.cache.end()) {
if(!seen.insert(proj_it->second).second)
continue;
}
std::string kind_str;
auto mod_it = ws.path_to_module.find(ws.path_pool.intern(file_path));
if(mod_it != ws.path_to_module.end()) {
kind_str = "module";
} else {
auto ext = llvm::sys::path::extension(file_path);
if(ext == ".h" || ext == ".hpp" || ext == ".hxx" || ext == ".hh")
kind_str = "header";
else
kind_str = "source";
}
if(filter != "all" && filter != kind_str)
continue;
FileInfo fi;
fi.path = file_path.str();
fi.kind = std::move(kind_str);
if(mod_it != ws.path_to_module.end())
fi.module_name = mod_it->second;
result.files.push_back(std::move(fi));
}
if(filter == "all" || filter == "header") {
for(auto& [path_id, shard]: ws.merged_indices) {
if(seen.contains(path_id))
continue;
auto path_str = ws.project_index.path_pool.path(path_id);
auto ext = llvm::sys::path::extension(path_str);
if(ext == ".h" || ext == ".hpp" || ext == ".hxx" || ext == ".hh") {
seen.insert(path_id);
result.files.push_back(FileInfo{
.path = path_str.str(),
.kind = "header",
});
}
}
}
result.total = static_cast<int>(result.files.size());
co_return result;
});
peer.on_request(
[&srv](RequestContext&, const FileDepsParams& params) -> RequestResult<FileDepsParams> {
auto& ws = srv.workspace;
auto pool_it = ws.path_pool.cache.find(params.path);
if(pool_it == ws.path_pool.cache.end())
co_return FileDepsResult{.file = params.path};
auto path_id = pool_it->second;
auto direction = params.direction.value_or("both");
auto max_depth = params.depth.value_or(1);
FileDepsResult result;
result.file = params.path;
if(direction == "includes" || direction == "both") {
auto includes = ws.dep_graph.get_all_includes(path_id);
for(auto inc_id: includes) {
auto real_id = inc_id & DependencyGraph::PATH_ID_MASK;
auto inc_path = ws.path_pool.resolve(real_id);
result.includes.push_back(DepEntry{.path = inc_path.str(), .depth = 1});
}
if(max_depth == 0 || max_depth > 1) {
llvm::DenseSet<std::uint32_t> visited;
visited.insert(path_id);
for(auto& dep: result.includes)
visited.insert(ws.path_pool.intern(dep.path));
for(std::size_t i = 0; i < result.includes.size(); ++i) {
if(max_depth > 0 && result.includes[i].depth >= max_depth)
continue;
auto dep_id = ws.path_pool.intern(result.includes[i].path);
auto sub = ws.dep_graph.get_all_includes(dep_id);
for(auto sub_id: sub) {
auto real_id = sub_id & DependencyGraph::PATH_ID_MASK;
if(!visited.insert(real_id).second)
continue;
auto sub_path = ws.path_pool.resolve(real_id);
result.includes.push_back(DepEntry{
.path = sub_path.str(),
.depth = result.includes[i].depth + 1,
});
}
}
}
}
if(direction == "includers" || direction == "both") {
auto includers = ws.dep_graph.get_includers(path_id);
for(auto inc_id: includers) {
auto inc_path = ws.path_pool.resolve(inc_id);
result.includers.push_back(DepEntry{.path = inc_path.str(), .depth = 1});
}
if(max_depth == 0 || max_depth > 1) {
llvm::DenseSet<std::uint32_t> visited;
visited.insert(path_id);
for(auto& dep: result.includers) {
auto it = ws.path_pool.cache.find(dep.path);
if(it != ws.path_pool.cache.end())
visited.insert(it->second);
}
for(std::size_t i = 0; i < result.includers.size(); ++i) {
if(max_depth > 0 && result.includers[i].depth >= max_depth)
continue;
auto dep_it = ws.path_pool.cache.find(result.includers[i].path);
if(dep_it == ws.path_pool.cache.end())
continue;
auto sub = ws.dep_graph.get_includers(dep_it->second);
for(auto sub_id: sub) {
if(!visited.insert(sub_id).second)
continue;
auto sub_path = ws.path_pool.resolve(sub_id);
result.includers.push_back(DepEntry{
.path = sub_path.str(),
.depth = result.includers[i].depth + 1,
});
}
}
}
}
co_return result;
});
peer.on_request(
[&srv](RequestContext&,
const ImpactAnalysisParams& params) -> RequestResult<ImpactAnalysisParams> {
auto& ws = srv.workspace;
auto pool_it = ws.path_pool.cache.find(params.path);
if(pool_it == ws.path_pool.cache.end())
co_return ImpactAnalysisResult{};
auto path_id = pool_it->second;
ImpactAnalysisResult result;
auto direct_includers = ws.dep_graph.get_includers(path_id);
for(auto inc_id: direct_includers) {
result.direct_dependents.push_back(ws.path_pool.resolve(inc_id).str());
}
auto hosts = ws.dep_graph.find_host_sources(path_id);
llvm::DenseSet<std::uint32_t> seen;
seen.insert(path_id);
for(auto inc_id: direct_includers)
seen.insert(inc_id);
for(auto host_id: hosts) {
if(seen.insert(host_id).second)
result.transitive_dependents.push_back(ws.path_pool.resolve(host_id).str());
}
for(auto host_id: hosts) {
auto it = ws.path_to_module.find(host_id);
if(it != ws.path_to_module.end())
result.affected_modules.push_back(it->second);
}
auto mod_it = ws.path_to_module.find(path_id);
if(mod_it != ws.path_to_module.end())
result.affected_modules.push_back(mod_it->second);
co_return result;
});
peer.on_request([&srv](RequestContext&,
const SymbolSearchParams& params) -> RequestResult<SymbolSearchParams> {
auto max = params.max_results.value_or(100);
std::string query_lower = llvm::StringRef(params.query).lower();
SymbolSearchResult result;
llvm::DenseSet<index::SymbolHash> seen;
auto try_symbol = [&](index::SymbolHash hash, const index::Symbol& symbol) {
if(static_cast<int>(result.symbols.size()) >= max)
return;
if(symbol.name.empty())
return;
if(!query_lower.empty() &&
llvm::StringRef(symbol.name).lower().find(query_lower) == std::string::npos)
return;
if(params.kind_filter.has_value()) {
auto kind_name = std::string(symbol_kind_name(symbol.kind));
auto& filter = *params.kind_filter;
if(std::ranges::find(filter, kind_name) == filter.end())
return;
}
auto def_loc = srv.indexer.find_definition_location(hash);
if(!def_loc)
return;
if(!seen.insert(hash).second)
return;
auto file = uri_to_path(def_loc->uri);
result.symbols.push_back(SymbolEntry{
.name = symbol.name,
.kind = std::string(symbol_kind_name(symbol.kind)),
.file = std::move(file),
.line = static_cast<int>(def_loc->range.start.line) + 1,
.symbol_id = hash,
});
};
for(auto& [hash, symbol]: srv.workspace.project_index.symbols)
try_symbol(hash, symbol);
for(auto& [_, sess]: srv.sessions) {
if(!sess.file_index)
continue;
for(auto& [hash, symbol]: sess.file_index->symbols)
try_symbol(hash, symbol);
}
co_return result;
});
peer.on_request(
[&srv](RequestContext&, const ReadSymbolParams& params) -> RequestResult<ReadSymbolParams> {
auto candidates = resolve_locator(params, srv.workspace, srv.sessions, srv.indexer);
if(candidates.empty())
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
if(candidates.size() > 1) {
co_return kota::outcome_error(kota::ipc::Error{
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
candidates.size())});
}
auto& rs = candidates[0];
auto def_text = srv.indexer.get_definition_text(rs.hash);
if(!def_text)
co_return kota::outcome_error(kota::ipc::Error{"definition not found"});
co_return ReadSymbolResult{
.name = rs.name,
.kind = std::string(symbol_kind_name(rs.kind)),
.file = std::move(def_text->file),
.start_line = def_text->start_line,
.end_line = def_text->end_line,
.text = std::move(def_text->text),
.symbol_id = rs.hash,
};
});
peer.on_request(
[&srv](RequestContext&,
const DocumentSymbolsParams& params) -> RequestResult<DocumentSymbolsParams> {
auto is_document_level = [](SymbolKind kind) {
return kind == SymbolKind::Namespace || kind == SymbolKind::Class ||
kind == SymbolKind::Struct || kind == SymbolKind::Union ||
kind == SymbolKind::Enum || kind == SymbolKind::Type ||
kind == SymbolKind::Field || kind == SymbolKind::EnumMember ||
kind == SymbolKind::Function || kind == SymbolKind::Method ||
kind == SymbolKind::Variable || kind == SymbolKind::Macro ||
kind == SymbolKind::Concept || kind == SymbolKind::Module ||
kind == SymbolKind::Operator || kind == SymbolKind::Attribute;
};
DocumentSymbolsResult result;
auto pool_it = srv.workspace.path_pool.cache.find(params.path);
if(pool_it == srv.workspace.path_pool.cache.end())
co_return result;
auto server_id = pool_it->second;
auto sess_it = srv.sessions.find(server_id);
if(sess_it != srv.sessions.end() && sess_it->second.file_index) {
auto& fi = *sess_it->second.file_index;
for(auto& [hash, rels]: fi.file_index.relations) {
for(auto& rel: rels) {
if(rel.kind.value() != RelationKind::Definition)
continue;
std::string name;
SymbolKind kind;
if(!srv.indexer.find_symbol_info(hash, name, kind))
continue;
if(!is_document_level(kind))
continue;
if(fi.mapper) {
auto start = fi.mapper->to_position(rel.range.begin);
auto end = fi.mapper->to_position(rel.range.end);
if(start && end) {
result.symbols.push_back(DocumentSymbolEntry{
.name = std::move(name),
.kind = std::string(symbol_kind_name(kind)),
.start_line = static_cast<int>(start->line) + 1,
.end_line = static_cast<int>(end->line) + 1,
.symbol_id = hash,
});
break;
}
}
}
}
co_return result;
}
auto it = srv.workspace.project_index.path_pool.find(params.path);
if(it == srv.workspace.project_index.path_pool.cache.end())
co_return result;
auto proj_id = it->second;
auto shard_it = srv.workspace.merged_indices.find(proj_id);
if(shard_it == srv.workspace.merged_indices.end())
co_return result;
for(auto& [hash, symbol]: srv.workspace.project_index.symbols) {
if(symbol.name.empty())
continue;
if(!is_document_level(symbol.kind))
continue;
if(!symbol.reference_files.contains(proj_id))
continue;
shard_it->second.find_relations(
hash,
RelationKind::Definition,
[&](const index::Relation&, protocol::Range range) {
result.symbols.push_back(DocumentSymbolEntry{
.name = symbol.name,
.kind = std::string(symbol_kind_name(symbol.kind)),
.start_line = static_cast<int>(range.start.line) + 1,
.end_line = static_cast<int>(range.end.line) + 1,
.symbol_id = hash,
});
return true;
});
}
co_return result;
});
peer.on_request(
[&srv](RequestContext&, const DefinitionParams& params) -> RequestResult<DefinitionParams> {
auto candidates = resolve_locator(
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
srv.workspace,
srv.sessions,
srv.indexer);
if(candidates.empty())
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
if(candidates.size() > 1) {
co_return kota::outcome_error(kota::ipc::Error{
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
candidates.size())});
}
auto& rs = candidates[0];
DefinitionResult result;
result.name = rs.name;
result.kind = std::string(symbol_kind_name(rs.kind));
result.symbol_id = rs.hash;
if(auto def_text = srv.indexer.get_definition_text(rs.hash)) {
result.definition = LocationEntry{
.file = std::move(def_text->file),
.start_line = def_text->start_line,
.end_line = def_text->end_line,
.text = std::move(def_text->text),
};
}
co_return result;
});
peer.on_request(
[&srv](RequestContext&, const ReferencesParams& params) -> RequestResult<ReferencesParams> {
auto candidates = resolve_locator(
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
srv.workspace,
srv.sessions,
srv.indexer);
if(candidates.empty())
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
if(candidates.size() > 1) {
co_return kota::outcome_error(kota::ipc::Error{
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
candidates.size())});
}
auto& rs = candidates[0];
ReferencesResult result;
result.name = rs.name;
result.kind = std::string(symbol_kind_name(rs.kind));
result.symbol_id = rs.hash;
for(auto& ref: srv.indexer.collect_references(rs.hash, RelationKind::Reference)) {
result.references.push_back(ReferenceEntry{
.file = std::move(ref.file),
.line = ref.line,
.context = std::move(ref.context),
});
}
if(params.include_declaration.value_or(false)) {
for(auto& ref: srv.indexer.collect_references(rs.hash, RelationKind::Definition)) {
result.references.push_back(ReferenceEntry{
.file = std::move(ref.file),
.line = ref.line,
.context = std::move(ref.context),
});
}
}
result.total = static_cast<int>(result.references.size());
co_return result;
});
peer.on_request(
[&srv](RequestContext&, const CallGraphParams& params) -> RequestResult<CallGraphParams> {
auto candidates = resolve_locator(
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
srv.workspace,
srv.sessions,
srv.indexer);
if(candidates.empty())
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
if(candidates.size() > 1) {
co_return kota::outcome_error(kota::ipc::Error{
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
candidates.size())});
}
auto& rs = candidates[0];
auto direction = params.direction.value_or("both");
CallGraphResult result;
result.root = CallGraphEntry{
.name = rs.name,
.kind = std::string(symbol_kind_name(rs.kind)),
.file = rs.file,
.line = rs.line,
.symbol_id = rs.hash,
};
auto resolve_kind = [&](std::uint64_t sym_id) -> std::string {
if(sym_id == 0)
return "Function";
std::string name;
SymbolKind kind;
if(srv.indexer.find_symbol_info(sym_id, name, kind))
return std::string(symbol_kind_name(kind));
return "Function";
};
if(direction == "callers" || direction == "both") {
auto incoming = srv.indexer.find_incoming_calls(rs.hash);
for(auto& call: incoming) {
auto sid = extract_symbol_id(call.from.data);
result.callers.push_back(CallGraphEntry{
.name = call.from.name,
.kind = resolve_kind(sid),
.file = uri_to_path(call.from.uri),
.line = static_cast<int>(call.from.range.start.line) + 1,
.symbol_id = sid,
});
}
}
if(direction == "callees" || direction == "both") {
auto outgoing = srv.indexer.find_outgoing_calls(rs.hash);
for(auto& call: outgoing) {
auto sid = extract_symbol_id(call.to.data);
result.callees.push_back(CallGraphEntry{
.name = call.to.name,
.kind = resolve_kind(sid),
.file = uri_to_path(call.to.uri),
.line = static_cast<int>(call.to.range.start.line) + 1,
.symbol_id = sid,
});
}
}
co_return result;
});
peer.on_request(
[&srv](RequestContext&,
const TypeHierarchyParams& params) -> RequestResult<TypeHierarchyParams> {
auto candidates = resolve_locator(
ReadSymbolParams{params.name, params.path, params.line, params.symbol_id},
srv.workspace,
srv.sessions,
srv.indexer);
if(candidates.empty())
co_return kota::outcome_error(kota::ipc::Error{"symbol not found"});
if(candidates.size() > 1) {
co_return kota::outcome_error(kota::ipc::Error{
std::format("ambiguous: {} candidates, use symbolId to disambiguate",
candidates.size())});
}
auto& rs = candidates[0];
auto direction = params.direction.value_or("both");
TypeHierarchyResult result;
result.root = TypeHierarchyEntry{
.name = rs.name,
.kind = std::string(symbol_kind_name(rs.kind)),
.file = rs.file,
.line = rs.line,
.symbol_id = rs.hash,
};
auto resolve_kind = [&](std::uint64_t sym_id) -> std::string {
if(sym_id == 0)
return "Class";
std::string name;
SymbolKind kind;
if(srv.indexer.find_symbol_info(sym_id, name, kind))
return std::string(symbol_kind_name(kind));
return "Class";
};
if(direction == "supertypes" || direction == "both") {
for(auto& item: srv.indexer.find_supertypes(rs.hash)) {
auto sid = extract_symbol_id(item.data);
result.supertypes.push_back(TypeHierarchyEntry{
.name = item.name,
.kind = resolve_kind(sid),
.file = uri_to_path(item.uri),
.line = static_cast<int>(item.range.start.line) + 1,
.symbol_id = sid,
});
}
}
if(direction == "subtypes" || direction == "both") {
for(auto& item: srv.indexer.find_subtypes(rs.hash)) {
auto sid = extract_symbol_id(item.data);
result.subtypes.push_back(TypeHierarchyEntry{
.name = item.name,
.kind = resolve_kind(sid),
.file = uri_to_path(item.uri),
.line = static_cast<int>(item.range.start.line) + 1,
.symbol_id = sid,
});
}
}
co_return result;
});
peer.on_request([&srv](RequestContext&, const StatusParams&) -> RequestResult<StatusParams> {
StatusResult result;
result.idle = srv.indexer.is_idle();
result.pending = static_cast<int>(srv.indexer.pending_files());
result.total = static_cast<int>(srv.indexer.total_queued());
result.indexed = std::max(0, result.total - result.pending);
co_return result;
});
peer.on_notification([&srv](const ShutdownParams&) {
LOG_INFO("agentic/shutdown received, shutting down");
srv.schedule_shutdown();
});
}
} // namespace clice

View File

@@ -1,18 +0,0 @@
#pragma once
#include "kota/ipc/codec/json.h"
namespace clice {
class MasterServer;
class AgentClient {
public:
AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer);
private:
MasterServer& server;
kota::ipc::JsonPeer& peer;
};
} // namespace clice

View File

@@ -1,177 +0,0 @@
#include "server/service/agentic.h"
#include <memory>
#include <print>
#include <string>
#include "server/protocol/agentic.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/transport.h"
namespace clice {
template <typename Params>
static kota::task<bool> send_and_print(kota::ipc::JsonPeer& peer, Params params) {
auto result = co_await peer.send_request(std::move(params));
if(!result) {
LOG_ERROR("request failed: {}", result.error().message);
co_return false;
}
auto json = kota::codec::json::to_string<kota::ipc::lsp_config>(*result);
std::println("{}", json ? *json : "null");
co_return true;
}
static kota::task<> agentic_request(kota::ipc::JsonPeer& peer,
int& exit_code,
const AgenticQueryOptions& opts) {
bool ok = false;
if(opts.method == "compileCommand") {
ok = co_await send_and_print(peer, agentic::CompileCommandParams{.path = opts.path});
} else if(opts.method == "projectFiles") {
auto filter = opts.query.empty() ? std::nullopt : std::optional(opts.query);
ok = co_await send_and_print(peer, agentic::ProjectFilesParams{.filter = filter});
} else if(opts.method == "symbolSearch") {
ok = co_await send_and_print(peer, agentic::SymbolSearchParams{.query = opts.query});
} else if(opts.method == "definition") {
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
ok = co_await send_and_print(
peer,
agentic::DefinitionParams{.name = name, .path = path, .line = line});
} else if(opts.method == "references") {
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
ok = co_await send_and_print(
peer,
agentic::ReferencesParams{.name = name, .path = path, .line = line});
} else if(opts.method == "readSymbol") {
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
ok = co_await send_and_print(
peer,
agentic::ReadSymbolParams{.name = name, .path = path, .line = line});
} else if(opts.method == "documentSymbols") {
ok = co_await send_and_print(peer, agentic::DocumentSymbolsParams{.path = opts.path});
} else if(opts.method == "callGraph") {
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
auto dir = opts.direction.empty() ? std::nullopt : std::optional(opts.direction);
ok = co_await send_and_print(peer,
agentic::CallGraphParams{
.name = name,
.path = path,
.line = line,
.direction = dir,
});
} else if(opts.method == "typeHierarchy") {
auto name = opts.name.empty() ? std::nullopt : std::optional(opts.name);
auto path = opts.path.empty() ? std::nullopt : std::optional(opts.path);
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
auto dir = opts.direction.empty() ? std::nullopt : std::optional(opts.direction);
ok = co_await send_and_print(peer,
agentic::TypeHierarchyParams{
.name = name,
.path = path,
.line = line,
.direction = dir,
});
} else if(opts.method == "fileDeps") {
auto dir = opts.direction.empty() ? std::nullopt : std::optional(opts.direction);
ok = co_await send_and_print(peer,
agentic::FileDepsParams{.path = opts.path, .direction = dir});
} else if(opts.method == "impactAnalysis") {
ok = co_await send_and_print(peer, agentic::ImpactAnalysisParams{.path = opts.path});
} else if(opts.method == "status") {
ok = co_await send_and_print(peer, agentic::StatusParams{});
} else if(opts.method == "shutdown") {
peer.send_notification(agentic::ShutdownParams{});
ok = true;
} else {
LOG_ERROR("unknown agentic method '{}'", opts.method);
}
if(ok)
exit_code = 0;
peer.close();
}
static kota::task<> agentic_client(int& exit_code,
std::unique_ptr<kota::ipc::JsonPeer>& peer_out,
const AgenticQueryOptions& opts) {
auto& loop = kota::event_loop::current();
auto transport = co_await kota::ipc::StreamTransport::connect_tcp(opts.host, opts.port, loop);
if(!transport) {
LOG_ERROR("failed to connect to {}:{}", opts.host, opts.port);
co_return;
}
peer_out = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(*transport));
co_await kota::when_all(peer_out->run(), agentic_request(*peer_out, exit_code, opts));
}
int run_agentic_mode(const AgenticQueryOptions& opts) {
logging::stderr_logger("agentic", logging::options);
kota::event_loop loop;
int exit_code = 1;
std::unique_ptr<kota::ipc::JsonPeer> peer;
loop.schedule(agentic_client(exit_code, peer, opts));
loop.run();
return exit_code;
}
static kota::task<> relay_forward(kota::ipc::Transport& from, kota::ipc::Transport& to) {
while(true) {
auto msg = co_await from.read_message();
if(!msg)
break;
co_await to.write_message(*msg);
}
to.close();
}
static kota::task<> relay_main(kota::event_loop& loop, int& exit_code, std::string socket_path) {
auto stdio = kota::ipc::StreamTransport::open_stdio(loop);
if(!stdio) {
LOG_ERROR("failed to open stdio transport");
loop.stop();
co_return;
}
auto conn = co_await kota::pipe::connect(socket_path, {}, loop);
if(!conn) {
LOG_ERROR("failed to connect to {}", socket_path);
loop.stop();
co_return;
}
auto socket = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
co_await kota::when_all(relay_forward(**stdio, *socket), relay_forward(*socket, **stdio));
exit_code = 0;
loop.stop();
}
int run_relay_mode(llvm::StringRef socket_path) {
logging::stderr_logger("relay", logging::options);
auto path = socket_path.empty() ? path::default_socket_path() : socket_path.str();
kota::event_loop loop;
int exit_code = 1;
loop.schedule(relay_main(loop, exit_code, std::move(path)));
loop.run();
return exit_code;
}
} // namespace clice

View File

@@ -1,24 +0,0 @@
#pragma once
#include <string>
#include "llvm/ADT/StringRef.h"
namespace clice {
struct AgenticQueryOptions {
std::string host;
int port = 0;
std::string method;
std::string path;
std::string name;
std::string query;
int line = 0;
std::string direction;
};
int run_agentic_mode(const AgenticQueryOptions& opts);
int run_relay_mode(llvm::StringRef socket_path);
} // namespace clice

View File

@@ -1,23 +0,0 @@
#pragma once
#include "kota/async/async.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/codec/json.h"
namespace clice {
class MasterServer;
class LSPClient {
public:
LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer);
~LSPClient();
private:
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
MasterServer& server;
kota::ipc::JsonPeer& peer;
};
} // namespace clice

View File

@@ -1,551 +0,0 @@
#include "server/service/master_server.h"
#include <cerrno>
#include <cstring>
#include <list>
#include <memory>
#include <string>
#include <vector>
#ifndef _WIN32
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif
#include "server/protocol/worker.h"
#include "server/service/agent_client.h"
#include "server/service/lsp_client.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/async/io/fs_event.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/uri.h"
#include "kota/ipc/recording_transport.h"
#include "kota/ipc/transport.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
namespace clice {
namespace lsp = kota::ipc::lsp;
namespace protocol = kota::ipc::protocol;
MasterServer::MasterServer(kota::event_loop& loop, std::string self_path) :
loop(loop), pool(loop), compiler(loop, workspace, pool, sessions),
indexer(loop,
workspace,
sessions,
pool,
compiler,
[this](uint32_t proj_path_id) {
auto path = workspace.project_index.path_pool.path(proj_path_id);
auto server_id = workspace.path_pool.intern(path);
return sessions.contains(server_id);
}),
self_path(std::move(self_path)) {}
MasterServer::~MasterServer() = default;
void MasterServer::initialize() {
workspace.config = Config::load_from_workspace(workspace_root);
if(!init_options_json.empty()) {
if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
} else {
workspace.config.apply_defaults(workspace_root);
LOG_INFO("Applied initializationOptions overlay");
}
init_options_json.clear();
}
auto& cfg = workspace.config.project;
if(!cfg.logging_dir.empty()) {
auto now = std::chrono::system_clock::now();
auto pid = llvm::sys::Process::getProcessId();
session_log_dir =
path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
logging::file_logger("master", session_log_dir, logging::options);
}
LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)",
cfg.stateful_worker_count.value,
cfg.stateless_worker_count.value,
*cfg.idle_timeout_ms);
WorkerPoolOptions pool_opts;
pool_opts.self_path = self_path;
pool_opts.stateful_count = cfg.stateful_worker_count;
pool_opts.stateless_count = cfg.stateless_worker_count;
pool_opts.worker_memory_limit = cfg.worker_memory_limit;
pool_opts.log_dir = session_log_dir;
if(!pool.start(pool_opts)) {
LOG_ERROR("Failed to start worker pool");
return;
}
lifecycle = ServerLifecycle::Ready;
compiler.on_indexing_needed = [this]() {
indexer.schedule();
};
indexer.set_max_concurrency(cfg.stateless_worker_count.value);
load_workspace();
}
void MasterServer::initialize(llvm::StringRef root) {
workspace_root = root.str();
initialize();
}
void MasterServer::start_file_watcher() {
if(workspace_root.empty())
return;
loop.schedule([this]() -> kota::task<> {
auto watcher = kota::fs_event::create(workspace_root, {}, loop);
if(!watcher) {
LOG_WARN("Failed to start file watcher for {}", workspace_root);
co_return;
}
LOG_INFO("File watcher started for {}", workspace_root);
while(true) {
auto changes = co_await watcher->next();
if(!changes)
break;
for(auto& change: *changes) {
if(change.type != kota::fs_event::effect::modify &&
change.type != kota::fs_event::effect::create)
continue;
llvm::StringRef file(change.path);
if(file.ends_with("compile_commands.json")) {
LOG_INFO("CDB changed, reloading workspace");
load_workspace();
continue;
}
if(file.ends_with(".cpp") || file.ends_with(".cc") || file.ends_with(".cxx") ||
file.ends_with(".c") || file.ends_with(".h") || file.ends_with(".hpp") ||
file.ends_with(".hxx") || file.ends_with(".cppm") || file.ends_with(".ixx")) {
auto path_id = workspace.path_pool.intern(file);
on_file_saved(path_id);
}
}
}
}());
}
Session* MasterServer::find_session(std::uint32_t path_id) {
auto it = sessions.find(path_id);
return it != sessions.end() ? &it->second : nullptr;
}
Session& MasterServer::open_session(std::uint32_t path_id) {
auto [it, inserted] = sessions.try_emplace(path_id);
auto& session = it->second;
if(!inserted)
session = Session{};
session.path_id = path_id;
return session;
}
void MasterServer::close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer) {
namespace protocol = kota::ipc::protocol;
auto path = workspace.path_pool.resolve(path_id);
workspace.on_file_closed(path_id);
pool.notify_stateful(path_id, worker::EvictParams{std::string(path)});
protocol::PublishDiagnosticsParams diag_params;
auto uri = lsp::URI::from_file_path(std::string(path));
if(uri)
diag_params.uri = uri->str();
diag_params.diagnostics = {};
peer.send_notification(diag_params);
sessions.erase(path_id);
indexer.enqueue(path_id);
indexer.schedule();
LOG_DEBUG("didClose: {}", path);
}
void MasterServer::on_file_saved(std::uint32_t path_id) {
auto dirtied = workspace.on_file_saved(path_id);
for(auto dirty_id: dirtied) {
if(auto* session = find_session(dirty_id)) {
session->ast_dirty = true;
} else {
indexer.enqueue(dirty_id);
}
}
for(auto& [hdr_id, session]: sessions) {
if(session.header_context && session.header_context->host_path_id == path_id) {
session.header_context.reset();
session.ast_dirty = true;
}
}
indexer.schedule();
}
void MasterServer::schedule_shutdown() {
if(lifecycle == ServerLifecycle::Exited)
return;
lifecycle = ServerLifecycle::Exited;
indexer.save(workspace.config.project.index_dir);
workspace.save_cache();
shutdown_event.set();
loop.schedule([this]() -> kota::task<> {
co_await kota::when_all(indexer.stop(), compiler.stop(), pool.stop());
loop.stop();
}());
}
void MasterServer::load_workspace() {
if(workspace_root.empty())
return;
auto& cfg = workspace.config.project;
if(!cfg.cache_dir.empty()) {
auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
if(ec) {
LOG_WARN("Failed to create cache directory {}: {}",
std::string_view(cfg.cache_dir),
ec.message());
} else {
LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
}
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
auto dir = path::join(cfg.cache_dir, subdir);
if(auto ec2 = llvm::sys::fs::create_directories(dir))
LOG_WARN("Failed to create {}: {}", dir, ec2.message());
}
workspace.cleanup_cache();
workspace.load_cache();
}
std::string cdb_path;
for(auto& configured: cfg.compile_commands_paths) {
if(llvm::sys::fs::is_directory(configured)) {
auto candidate = path::join(configured, "compile_commands.json");
if(llvm::sys::fs::exists(candidate)) {
cdb_path = std::move(candidate);
break;
}
} else if(llvm::sys::fs::exists(configured)) {
cdb_path = configured;
break;
} else {
LOG_WARN("Configured compile_commands_path not found: {}", configured);
}
}
if(cdb_path.empty()) {
auto try_candidate = [&](llvm::StringRef dir) -> bool {
auto candidate = path::join(dir, "compile_commands.json");
if(llvm::sys::fs::exists(candidate)) {
cdb_path = std::move(candidate);
return true;
}
return false;
};
if(!try_candidate(workspace_root)) {
std::error_code ec;
for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec;
it.increment(ec)) {
if(it->type() == llvm::sys::fs::file_type::directory_file) {
if(try_candidate(it->path()))
break;
}
}
}
}
if(cdb_path.empty()) {
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
return;
}
auto count = workspace.cdb.load(cdb_path);
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);
auto report = scan_dependency_graph(workspace.cdb,
workspace.path_pool,
workspace.dep_graph,
/*cache=*/nullptr,
[this](llvm::StringRef path,
std::vector<std::string>& append,
std::vector<std::string>& remove) {
workspace.config.match_rules(path, append, remove);
});
workspace.dep_graph.build_reverse_map();
auto unresolved = report.includes_found - report.includes_resolved;
double accuracy =
report.includes_found > 0
? 100.0 * static_cast<double>(report.includes_resolved) / report.includes_found
: 100.0;
LOG_INFO(
"Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves",
report.elapsed_ms,
report.total_files,
report.source_files,
report.header_files,
report.total_edges,
report.includes_resolved,
report.includes_found,
accuracy,
report.waves);
if(unresolved > 0)
LOG_WARN("{} unresolved includes", unresolved);
workspace.build_module_map();
indexer.load(cfg.index_dir);
if(*cfg.enable_indexing) {
for(auto& entry: workspace.cdb.get_entries()) {
auto file = workspace.cdb.resolve_path(entry.file);
auto server_id = workspace.path_pool.intern(file);
indexer.enqueue(server_id);
}
indexer.schedule();
}
compiler.init_compile_graph();
}
struct Connection {
std::unique_ptr<kota::ipc::JsonPeer> peer;
std::unique_ptr<LSPClient> lsp_client;
std::unique_ptr<AgentClient> agent_client;
};
static kota::task<> run_connection(kota::ipc::JsonPeer* peer,
std::list<Connection>& connections,
std::list<Connection>::iterator pos) {
co_await peer->run();
LOG_INFO("Client disconnected");
connections.erase(pos);
}
static kota::task<> accept_connections(MasterServer& server,
kota::tcp::acceptor acceptor,
bool register_lsp,
std::list<Connection>& connections) {
auto& loop = kota::event_loop::current();
kota::task_group<> connection_group(loop);
bool lsp_registered = false;
while(true) {
auto conn = co_await acceptor.accept();
if(!conn.has_value())
break;
LOG_INFO("Client connected");
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
auto peer = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(transport));
std::unique_ptr<LSPClient> lsp;
if(register_lsp && !lsp_registered) {
lsp = std::make_unique<LSPClient>(server, *peer);
lsp_registered = true;
}
auto agent = std::make_unique<AgentClient>(server, *peer);
auto* peer_ptr = peer.get();
auto it = connections.emplace(connections.end(),
Connection{
.peer = std::move(peer),
.lsp_client = std::move(lsp),
.agent_client = std::move(agent),
});
connection_group.spawn(run_connection(peer_ptr, connections, it));
}
co_await connection_group.join();
}
int run_server_mode(const ServerOptions& opts) {
logging::stderr_logger("master", logging::options);
kota::event_loop loop;
MasterServer server(loop, opts.self_path);
std::list<Connection> connections;
if(opts.mode == "pipe") {
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
if(!transport) {
LOG_ERROR("failed to open stdio transport");
return 1;
}
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
if(!opts.record.empty()) {
final_transport =
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
opts.record);
}
kota::ipc::JsonPeer lsp_peer(loop, std::move(final_transport));
LSPClient lsp_client(server, lsp_peer);
if(opts.port > 0) {
auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop);
if(acceptor) {
LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port);
loop.schedule(accept_connections(server, std::move(*acceptor), false, connections));
} else {
LOG_WARN("Failed to start agentic listener on {}:{}", opts.host, opts.port);
}
}
loop.schedule(lsp_peer.run());
loop.run();
return 0;
}
if(opts.mode == "socket") {
auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop);
if(!acceptor) {
LOG_ERROR("failed to listen on {}:{}", opts.host, opts.port);
return 1;
}
LOG_INFO("Listening on {}:{} ...", opts.host, opts.port);
loop.schedule(accept_connections(server, std::move(*acceptor), true, connections));
loop.run();
return 0;
}
LOG_ERROR("unknown server mode '{}'", opts.mode);
return 1;
}
struct DaemonConnection {
std::unique_ptr<kota::ipc::JsonPeer> peer;
std::unique_ptr<AgentClient> agent_client;
};
static kota::task<> run_daemon_connection(kota::ipc::JsonPeer* peer,
std::list<DaemonConnection>& connections,
std::list<DaemonConnection>::iterator pos) {
co_await peer->run();
LOG_INFO("Daemon client disconnected");
connections.erase(pos);
}
static kota::task<> daemon_main(MasterServer& server, kota::pipe::acceptor acceptor) {
auto& loop = kota::event_loop::current();
std::list<DaemonConnection> connections;
kota::task_group<> connection_group(loop);
co_await kota::when_all(
[&]() -> kota::task<> {
while(true) {
auto conn = co_await acceptor.accept();
if(!conn.has_value())
break;
LOG_INFO("Daemon client connected");
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
auto peer = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(transport));
auto agent = std::make_unique<AgentClient>(server, *peer);
auto* peer_ptr = peer.get();
auto it = connections.emplace(connections.end(),
DaemonConnection{
.peer = std::move(peer),
.agent_client = std::move(agent),
});
connection_group.spawn(run_daemon_connection(peer_ptr, connections, it));
}
}(),
[&]() -> kota::task<> {
co_await server.get_shutdown_event().wait();
acceptor.stop();
for(auto& conn: connections) {
conn.peer->close();
}
}());
co_await connection_group.join();
}
int run_daemon_mode(const DaemonOptions& opts) {
logging::stderr_logger("daemon", logging::options);
auto socket_path = opts.socket_path.empty() ? path::default_socket_path() : opts.socket_path;
auto socket_dir = llvm::sys::path::parent_path(socket_path);
if(auto ec = llvm::sys::fs::create_directories(socket_dir)) {
LOG_ERROR("Failed to create socket directory {}: {}", socket_dir, ec.message());
return 1;
}
if(llvm::sys::fs::exists(socket_path)) {
#ifndef _WIN32
int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if(fd >= 0) {
struct sockaddr_un addr{};
addr.sun_family = AF_UNIX;
auto len = std::min(socket_path.size(), sizeof(addr.sun_path) - 1);
std::memcpy(addr.sun_path, socket_path.data(), len);
bool live = ::connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) == 0;
::close(fd);
if(live) {
LOG_ERROR("Another daemon is already running on {}", socket_path);
return 1;
}
}
#endif
llvm::sys::fs::remove(socket_path);
}
kota::event_loop loop;
MasterServer server(loop, opts.self_path);
if(!opts.workspace.empty()) {
server.initialize(opts.workspace);
server.start_file_watcher();
}
auto acceptor = kota::pipe::listen(socket_path, {}, loop);
if(!acceptor) {
LOG_ERROR("Failed to listen on {}", socket_path);
return 1;
}
LOG_INFO("Daemon listening on {}", socket_path);
loop.schedule(daemon_main(server, std::move(*acceptor)));
loop.run();
llvm::sys::fs::remove(socket_path);
return 0;
}
} // namespace clice

View File

@@ -1,93 +0,0 @@
#pragma once
#include <cstdint>
#include <string>
#include "server/compiler/compiler.h"
#include "server/compiler/indexer.h"
#include "server/service/session.h"
#include "server/worker/worker_pool.h"
#include "server/workspace/workspace.h"
#include "kota/async/async.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
enum class ServerLifecycle : std::uint8_t {
Uninitialized,
Initialized,
Ready,
ShuttingDown,
Exited,
};
/// Core server state — owns the two-layer state model (Workspace + Sessions),
/// the worker pool, compilation engine, and indexer.
///
/// Does NOT own any transport or peer. Protocol-specific handler registration
/// is done by LSPClient and AgentClient, which access private members directly.
class MasterServer {
friend class LSPClient;
friend class AgentClient;
public:
MasterServer(kota::event_loop& loop, std::string self_path);
~MasterServer();
void initialize();
void initialize(llvm::StringRef root);
void start_file_watcher();
Session* find_session(std::uint32_t path_id);
Session& open_session(std::uint32_t path_id);
void close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer);
void on_file_saved(std::uint32_t path_id);
void schedule_shutdown();
kota::event& get_shutdown_event() {
return shutdown_event;
}
private:
kota::event shutdown_event;
void load_workspace();
kota::event_loop& loop;
Workspace workspace;
llvm::DenseMap<std::uint32_t, Session> sessions;
WorkerPool pool;
Compiler compiler;
Indexer indexer;
ServerLifecycle lifecycle = ServerLifecycle::Uninitialized;
std::string self_path;
std::string workspace_root;
std::string session_log_dir;
std::string init_options_json;
};
struct ServerOptions {
std::string mode;
std::string host = "127.0.0.1";
int port = 0;
std::string self_path;
std::string record;
};
int run_server_mode(const ServerOptions& opts);
struct DaemonOptions {
std::string socket_path;
std::string workspace;
std::string self_path;
};
int run_daemon_mode(const DaemonOptions& opts);
} // namespace clice

View File

@@ -5,7 +5,7 @@
#include <optional>
#include <string>
#include "server/workspace/workspace.h"
#include "server/workspace.h"
#include "kota/async/async.h"
#include "llvm/ADT/SmallVector.h"

View File

@@ -1,4 +1,4 @@
#include "server/worker/stateful_worker.h"
#include "server/stateful_worker.h"
#include <atomic>
#include <cstdint>
@@ -10,8 +10,8 @@
#include "compile/compilation.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol/worker.h"
#include "server/worker/worker_common.h"
#include "server/protocol.h"
#include "server/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
@@ -245,33 +245,26 @@ void StatefulWorker::register_handlers() {
co_return kota::codec::RawValue{"[]"};
case K::SemanticTokens:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(
feature::semantic_tokens(doc.unit, feature::PositionEncoding::UTF16));
return to_raw(feature::semantic_tokens(doc.unit));
});
case K::InlayHints:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
auto range = params.range;
if(range.begin == static_cast<uint32_t>(-1))
range = LocalSourceRange{0, static_cast<uint32_t>(doc.text.size())};
return to_raw(feature::inlay_hints(doc.unit,
range,
{},
feature::PositionEncoding::UTF16));
return to_raw(feature::inlay_hints(doc.unit, range));
});
case K::FoldingRange:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(
feature::folding_ranges(doc.unit, feature::PositionEncoding::UTF16));
return to_raw(feature::folding_ranges(doc.unit));
});
case K::DocumentSymbol:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(
feature::document_symbols(doc.unit, feature::PositionEncoding::UTF16));
return to_raw(feature::document_symbols(doc.unit));
});
case K::DocumentLink:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(
feature::document_links(doc.unit, feature::PositionEncoding::UTF16));
return to_raw(feature::document_links(doc.unit));
});
case K::CodeAction:
// TODO: Implement code actions

View File

@@ -1,10 +1,10 @@
#include "server/worker/stateless_worker.h"
#include "server/stateless_worker.h"
#include "compile/compilation.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol/worker.h"
#include "server/worker/worker_common.h"
#include "server/protocol.h"
#include "server/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
@@ -274,22 +274,6 @@ static worker::BuildResult handle_signature_help(const worker::BuildParams& para
return result;
}
static worker::BuildResult handle_format(const worker::BuildParams& params) {
ScopedTimer timer;
std::optional<LocalSourceRange> range;
if(params.format_range.valid()) {
range = params.format_range;
}
auto edits = feature::document_format(params.file, params.text, range);
LOG_DEBUG("Format done: {} edits, {}ms", edits.size(), timer.ms());
worker::BuildResult result;
result.result_json = to_raw(edits);
return result;
}
int run_stateless_worker_mode(const std::string& worker_name, const std::string& log_dir) {
logging::stderr_logger(worker_name, logging::options);
if(!log_dir.empty()) {
@@ -321,7 +305,6 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
}
case K::Completion: return handle_completion(params);
case K::SignatureHelp: return handle_signature_help(params);
case K::Format: return handle_format(params);
}
return {false, "Unknown build kind"};
});

View File

@@ -1,4 +1,4 @@
#include "server/worker/worker_pool.h"
#include "server/worker_pool.h"
#include <csignal>
#include <string>
@@ -96,8 +96,9 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
std::move(spawn.stdin_pipe));
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
// Schedule stderr log collection
std::string prefix = "[" + worker_name + "]";
io_group.spawn(drain_stderr(std::move(spawn.stderr_pipe), prefix));
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
workers.push_back(WorkerProcess{
.proc = std::move(spawn.proc),
@@ -107,7 +108,8 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
auto& w = workers.back();
w.alive = true;
io_group.spawn(w.peer->run());
++alive_count_;
loop.schedule(w.peer->run());
return true;
}
@@ -120,14 +122,14 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
if(!spawn_worker(options.self_path, false, 0)) {
return false;
}
monitor_group.spawn(monitor_worker(stateless_workers.size() - 1, false));
loop.schedule(monitor_worker(stateless_workers.size() - 1, false));
}
for(std::uint32_t i = 0; i < options.stateful_count; ++i) {
if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) {
return false;
}
monitor_group.spawn(monitor_worker(stateful_workers.size() - 1, true));
loop.schedule(monitor_worker(stateful_workers.size() - 1, true));
}
// Register evicted notification handler for each stateful worker
@@ -149,17 +151,23 @@ kota::task<> WorkerPool::stop() {
LOG_INFO("WorkerPool stopping...");
shutting_down_ = true;
// Close output pipes to signal workers to exit gracefully.
for(auto& w: stateless_workers)
w.peer->close_output();
for(auto& w: stateful_workers)
w.peer->close_output();
// Send SIGTERM. monitor_worker coroutines handle the wait.
for(auto& w: stateless_workers)
w.proc.kill(SIGTERM);
for(auto& w: stateful_workers)
w.proc.kill(SIGTERM);
co_await kota::when_all(monitor_group.join(), io_group.join());
// Wait until all monitor_worker coroutines have finished.
if(alive_count_ > 0) {
all_exited_.reset();
co_await all_exited_.wait();
}
LOG_INFO("WorkerPool stopped");
}
@@ -234,9 +242,13 @@ kota::task<> WorkerPool::monitor_worker(std::size_t index, bool stateful) {
auto result = co_await w.proc.wait();
w.alive = false;
--alive_count_;
if(shutting_down_)
if(shutting_down_) {
if(alive_count_ == 0)
all_exited_.set();
co_return;
}
if(result.has_value()) {
auto& exit = result.value();
@@ -319,7 +331,7 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
std::string prefix = "[" + worker_name + "]";
io_group.spawn(drain_stderr(std::move(spawn.stderr_pipe), prefix));
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
workers[index] = WorkerProcess{
.proc = std::move(spawn.proc),
@@ -330,7 +342,8 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
};
auto& w = workers[index];
io_group.spawn(w.peer->run());
++alive_count_;
loop.schedule(w.peer->run());
if(stateful) {
w.peer->on_notification([this](const worker::EvictedParams& params) {
@@ -339,7 +352,7 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
});
}
monitor_group.spawn(monitor_worker(index, stateful));
loop.schedule(monitor_worker(index, stateful));
LOG_INFO("Worker {} restarted (attempt {})", worker_name, old_restart_count);
return true;

View File

@@ -6,7 +6,7 @@
#include <list>
#include <memory>
#include "server/protocol/worker.h"
#include "server/protocol.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
@@ -83,8 +83,8 @@ private:
std::size_t pick_least_loaded();
bool shutting_down_ = false;
kota::task_group<> monitor_group{loop};
kota::task_group<> io_group{loop};
std::size_t alive_count_ = 0;
kota::event all_exited_{true}; // Signalled when alive_count_ reaches 0.
WorkerPoolOptions options_;
std::string log_dir_;

View File

@@ -1,4 +1,4 @@
#include "server/workspace/workspace.h"
#include "server/workspace.h"
#include <algorithm>
#include <chrono>

View File

@@ -11,8 +11,8 @@
#include "index/merged_index.h"
#include "index/project_index.h"
#include "semantic/relation_kind.h"
#include "server/compiler/compile_graph.h"
#include "server/workspace/config.h"
#include "server/compile_graph.h"
#include "server/config.h"
#include "support/path_pool.h"
#include "syntax/dependency_graph.h"

View File

@@ -37,14 +37,6 @@ inline std::string real_path(llvm::StringRef file) {
return path.str().str();
}
inline std::string default_socket_path() {
llvm::SmallString<128> home;
if(!llvm::sys::path::home_directory(home))
return "/tmp/clice.sock";
llvm::sys::path::append(home, ".clice", "clice.sock");
return home.str().str();
}
} // namespace path
namespace fs {

View File

@@ -1,7 +1,6 @@
import asyncio
import json
import shutil
import socket
import subprocess
import sys
from pathlib import Path
@@ -94,16 +93,17 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non
@pytest.fixture
async def client(
request: pytest.FixtureRequest,
executable: Path,
workspace: Path | None,
request: pytest.FixtureRequest, executable: Path, workspace: Path | None
):
"""Spawn clice server, auto-initialize if @pytest.mark.workspace is present."""
config = request.config
mode = config.getoption("--mode")
host = config.getoption("--host")
cmd = [str(executable), "--mode", mode, "--host", host]
cmd = [str(executable), "--mode", mode]
if mode == "socket":
host = config.getoption("--host")
port = config.getoption("--port")
cmd += ["--host", host, "--port", str(port)]
c = CliceClient()
await c.start_io(*cmd)
@@ -122,39 +122,6 @@ async def client(
await _shutdown_client(c)
def _find_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
@pytest.fixture
async def agentic(
request: pytest.FixtureRequest,
executable: Path,
workspace: Path | None,
):
"""Start a server with agentic TCP port, yield (executable, host, port)."""
host = "127.0.0.1"
port = _find_free_port()
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
c = CliceClient()
await c.start_io(*cmd)
if workspace is not None:
init_options_marker = request.node.get_closest_marker("init_options")
init_options = dict(init_options_marker.args[0]) if init_options_marker else {}
project = dict(init_options.get("project", {}))
project.setdefault("cache_dir", str(workspace / ".clice"))
init_options["project"] = project
await c.initialize(workspace, initialization_options=init_options)
yield executable, host, port
await _shutdown_client(c)
def generate_cdb(workspace: Path) -> None:
"""Generate compile_commands.json using CMake with Ninja backend."""
cmake = shutil.which("cmake")
@@ -292,12 +259,6 @@ def _generate_test_data_cdbs(data_dir: Path) -> None:
if cr_main.exists():
_write(cr_dir, [_entry(cr_dir, cr_main)])
# formatting
fmt_dir = data_dir / "formatting"
fmt_main = fmt_dir / "main.cpp"
if fmt_main.exists():
_write(fmt_dir, [_entry(fmt_dir, fmt_main)])
# pch_test
pt_dir = data_dir / "pch_test"
if pt_dir.exists():

View File

@@ -1,36 +0,0 @@
// basic if and if-else
namespace basic_if {
int abs_val(int x) {
if(x < 0)
return -x;
return x;
}
const char* sign(int x) {
if(x > 0) {
return "positive";
} else if(x < 0) {
return "negative";
} else {
return "zero";
}
}
// dangling else: else binds to nearest if
int nested_if(int a, int b) {
if(a > 0)
if(b > 0)
return 1;
else
return 2;
return 0;
}
void test() {
[[maybe_unused]] int r1 = abs_val(-3);
[[maybe_unused]] auto r2 = sign(5);
[[maybe_unused]] int r3 = nested_if(1, -1);
}
} // namespace basic_if

View File

@@ -1,3 +0,0 @@
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 80

View File

@@ -1 +0,0 @@
int add(int a, int b) { return a + b; }

View File

@@ -1,592 +0,0 @@
"""Tests for the agentic protocol handlers."""
import asyncio
import json
import socket
import subprocess
from concurrent.futures import ThreadPoolExecutor
import pytest
from tests.integration.utils.wait import wait_for_index
class AgenticRpcClient:
"""Minimal JSON-RPC client that speaks Content-Length framing over TCP."""
def __init__(self, host: str, port: int):
self.sock = socket.create_connection((host, port), timeout=10)
self.request_id = 0
self.buffer = b""
def request(self, method: str, params: dict):
self.request_id += 1
body = json.dumps(
{
"jsonrpc": "2.0",
"id": self.request_id,
"method": method,
"params": params,
}
)
payload = f"Content-Length: {len(body)}\r\n\r\n{body}".encode("utf-8")
self.sock.sendall(payload)
return self._read_response()
def _read_response(self):
while b"\r\n\r\n" not in self.buffer:
data = self.sock.recv(4096)
if not data:
raise ConnectionError("connection closed")
self.buffer += data
header_end = self.buffer.index(b"\r\n\r\n")
headers = self.buffer[:header_end].decode("utf-8")
self.buffer = self.buffer[header_end + 4 :]
content_length = 0
for line in headers.split("\r\n"):
if line.lower().startswith("content-length:"):
content_length = int(line.split(":")[1].strip())
while len(self.buffer) < content_length:
data = self.sock.recv(4096)
if not data:
raise ConnectionError("connection closed")
self.buffer += data
body = self.buffer[:content_length].decode("utf-8")
self.buffer = self.buffer[content_length:]
return json.loads(body)
def close(self):
self.sock.close()
def run_agentic(executable, host, port, path, timeout=10):
result = subprocess.run(
[
str(executable),
"--mode",
"agentic",
"--host",
host,
"--port",
str(port),
"--path",
path,
],
capture_output=True,
text=True,
timeout=timeout,
)
return result
@pytest.mark.workspace("hello_world")
async def test_compile_command(agentic, workspace):
executable, host, port = agentic
main_cpp = (workspace / "main.cpp").as_posix()
result = run_agentic(executable, host, port, main_cpp)
assert result.returncode == 0, f"stderr: {result.stderr}"
data = json.loads(result.stdout)
assert data["file"] == main_cpp
assert data["directory"] == workspace.as_posix()
assert len(data["arguments"]) > 0
@pytest.mark.workspace("hello_world")
async def test_compile_command_fallback(agentic, workspace):
executable, host, port = agentic
result = run_agentic(executable, host, port, "/nonexistent/file.cpp")
assert result.returncode == 0, f"stderr: {result.stderr}"
data = json.loads(result.stdout)
assert data["file"] == "/nonexistent/file.cpp"
@pytest.mark.workspace("hello_world")
async def test_multiple_requests(agentic, workspace):
executable, host, port = agentic
main_cpp = (workspace / "main.cpp").as_posix()
for _ in range(3):
result = run_agentic(executable, host, port, main_cpp)
assert result.returncode == 0, f"stderr: {result.stderr}"
data = json.loads(result.stdout)
assert data["file"] == main_cpp
async def test_connection_refused(executable):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
free_port = s.getsockname()[1]
result = run_agentic(executable, "127.0.0.1", free_port, "/some/file.cpp")
assert result.returncode != 0
@pytest.mark.workspace("hello_world")
async def test_concurrent_connections(agentic, workspace):
executable, host, port = agentic
main_cpp = (workspace / "main.cpp").as_posix()
def do_request(_):
return run_agentic(executable, host, port, main_cpp)
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(do_request, range(4)))
for r in results:
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["file"] == main_cpp
@pytest.fixture
async def indexed_agentic(request, executable, workspace):
"""Start server with LSP+agentic, compile a file, wait for indexing."""
from tests.integration.utils.client import CliceClient
from tests.conftest import _shutdown_client, _find_free_port
host = "127.0.0.1"
port = _find_free_port()
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
c = CliceClient()
await c.start_io(*cmd)
init_options = {"project": {"cache_dir": str(workspace / ".clice")}}
await c.initialize(workspace, initialization_options=init_options)
uri, _ = await c.open_and_wait(workspace / "main.cpp")
assert await wait_for_index(c, uri, "add"), "Index not ready"
rpc = AgenticRpcClient(host, port)
for _ in range(30):
resp = rpc.request("agentic/symbolSearch", {"query": "add"})
if "result" in resp and resp["result"]["symbols"]:
break
await asyncio.sleep(1)
else:
pytest.fail("agentic/symbolSearch never returned indexed symbols")
yield rpc, workspace
rpc.close()
c.close(uri)
await _shutdown_client(c)
@pytest.mark.workspace("index_features")
async def test_rpc_compile_command(indexed_agentic, workspace):
rpc, _ = indexed_agentic
path = (workspace / "main.cpp").as_posix()
resp = rpc.request("agentic/compileCommand", {"path": path})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["file"] == path
assert len(result["arguments"]) > 0
@pytest.mark.workspace("index_features")
async def test_rpc_project_files(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/projectFiles", {})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["total"] > 0
paths = [f["path"] for f in result["files"]]
assert any("main.cpp" in p for p in paths)
@pytest.mark.workspace("index_features")
async def test_rpc_project_files_filter(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/projectFiles", {"filter": "source"})
assert "result" in resp
for f in resp["result"]["files"]:
assert f["kind"] == "source"
@pytest.mark.workspace("index_features")
async def test_rpc_symbol_search(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/symbolSearch", {"query": "add"})
assert "result" in resp, f"unexpected response: {resp}"
symbols = resp["result"]["symbols"]
add_sym = next((s for s in symbols if s["name"] == "add"), None)
assert add_sym is not None, f"'add' not found in {[s['name'] for s in symbols]}"
assert add_sym["kind"] == "Function"
assert add_sym["line"] == 19
assert add_sym["symbolId"] != 0
assert "main.cpp" in add_sym["file"]
@pytest.mark.workspace("index_features")
async def test_rpc_symbol_search_kind(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request(
"agentic/symbolSearch", {"query": "Animal", "kindFilter": ["Struct"]}
)
assert "result" in resp
for s in resp["result"]["symbols"]:
assert s["kind"] == "Struct"
@pytest.mark.workspace("index_features")
async def test_rpc_symbol_search_max(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/symbolSearch", {"query": "", "maxResults": 3})
assert "result" in resp
assert len(resp["result"]["symbols"]) <= 3
@pytest.mark.workspace("index_features")
async def test_rpc_read_symbol(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/readSymbol", {"name": "add"})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["name"] == "add"
assert result["symbolId"] != 0
assert result["startLine"] == 19
assert result["endLine"] == 21
assert "int add(int a, int b)" in result["text"]
assert "return a + b;" in result["text"]
@pytest.mark.workspace("index_features")
async def test_rpc_read_symbol_by_id(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp1 = rpc.request("agentic/readSymbol", {"name": "add"})
assert "result" in resp1
sid = resp1["result"]["symbolId"]
resp2 = rpc.request("agentic/readSymbol", {"symbolId": sid})
assert "result" in resp2
assert resp2["result"]["name"] == "add"
assert resp2["result"]["symbolId"] == sid
@pytest.mark.workspace("index_features")
async def test_rpc_document_symbols(indexed_agentic, workspace):
rpc, _ = indexed_agentic
path = (workspace / "main.cpp").as_posix()
resp = rpc.request("agentic/documentSymbols", {"path": path})
assert "result" in resp, f"unexpected response: {resp}"
symbols = resp["result"]["symbols"]
names = [s["name"] for s in symbols]
kinds = [s["kind"] for s in symbols]
assert "add" in names, f"expected 'add' in {names}"
assert "main" in names, f"expected 'main' in {names}"
assert "global_var" in names, f"expected 'global_var' in {names}"
assert "Parameter" not in kinds, (
f"Parameters should be filtered: {list(zip(names, kinds))}"
)
@pytest.mark.workspace("index_features")
async def test_rpc_definition(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/definition", {"name": "add"})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["name"] == "add"
assert result["definition"] is not None
defn = result["definition"]
assert "main.cpp" in defn["file"]
assert defn["startLine"] == 19
assert defn["endLine"] == 21
assert "int add(int a, int b)" in defn["text"]
assert "return a + b;" in defn["text"]
@pytest.mark.workspace("index_features")
async def test_rpc_definition_by_position(indexed_agentic, workspace):
rpc, _ = indexed_agentic
path = (workspace / "main.cpp").as_posix()
resp = rpc.request("agentic/definition", {"path": path, "line": 19})
assert "result" in resp, f"unexpected response: {resp}"
assert resp["result"]["name"] == "add"
@pytest.mark.workspace("index_features")
async def test_rpc_references(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/references", {"name": "global_var"})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["name"] == "global_var"
assert result["total"] == 2
lines = sorted(r["line"] for r in result["references"])
assert lines == [34, 38]
contexts = [r["context"] for r in result["references"]]
assert any("global_var + 1" in c for c in contexts)
assert any("global_var * 2" in c for c in contexts)
@pytest.mark.workspace("index_features")
async def test_rpc_references_include_decl(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request(
"agentic/references", {"name": "global_var", "includeDeclaration": True}
)
assert "result" in resp
result = resp["result"]
assert result["total"] == 3
lines = sorted(r["line"] for r in result["references"])
assert 31 in lines, f"expected declaration line 31 in {lines}"
@pytest.mark.workspace("index_features")
async def test_rpc_call_graph_incoming(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/callGraph", {"name": "add", "direction": "callers"})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["root"]["name"] == "add"
assert result["root"]["line"] == 19
assert result["root"]["symbolId"] != 0
callers = result["callers"]
caller_names = [c["name"] for c in callers]
assert "compute" in caller_names, f"expected 'compute' in {caller_names}"
compute = next(c for c in callers if c["name"] == "compute")
assert compute["line"] == 24
assert compute["symbolId"] != 0
assert result["callees"] == []
@pytest.mark.workspace("index_features")
async def test_rpc_call_graph_outgoing(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/callGraph", {"name": "compute", "direction": "callees"})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["root"]["name"] == "compute"
callees = result["callees"]
callee_names = [c["name"] for c in callees]
assert "add" in callee_names, f"expected 'add' in {callee_names}"
add_entry = next(c for c in callees if c["name"] == "add")
assert add_entry["line"] == 19
assert result["callers"] == []
@pytest.mark.workspace("index_features")
async def test_rpc_type_hierarchy_supertypes(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request(
"agentic/typeHierarchy", {"name": "Dog", "direction": "supertypes"}
)
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["root"]["name"] == "Dog"
assert result["root"]["line"] == 9
supertypes = result["supertypes"]
supertype_names = [t["name"] for t in supertypes]
assert "Animal" in supertype_names, f"expected 'Animal' in {supertype_names}"
animal = next(t for t in supertypes if t["name"] == "Animal")
assert animal["line"] == 2
assert animal["symbolId"] != 0
@pytest.mark.workspace("index_features")
async def test_rpc_type_hierarchy_subtypes(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request(
"agentic/typeHierarchy", {"name": "Animal", "direction": "subtypes"}
)
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["root"]["name"] == "Animal"
assert result["root"]["line"] == 2
subtypes = result["subtypes"]
subtype_names = [t["name"] for t in subtypes]
assert "Dog" in subtype_names, f"expected 'Dog' in {subtype_names}"
assert "Cat" in subtype_names, f"expected 'Cat' in {subtype_names}"
dog = next(t for t in subtypes if t["name"] == "Dog")
assert dog["line"] == 9
cat = next(t for t in subtypes if t["name"] == "Cat")
assert cat["line"] == 14
@pytest.mark.workspace("index_features")
async def test_rpc_status(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/status", {})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert isinstance(result["idle"], bool)
assert result["total"] > 0
assert isinstance(result["pending"], int)
assert isinstance(result["indexed"], int)
@pytest.mark.workspace("hello_world")
async def test_rpc_shutdown(executable, workspace):
"""Shutdown notification should cause the server to exit."""
from tests.integration.utils.client import CliceClient
from tests.conftest import _shutdown_client, _find_free_port
host = "127.0.0.1"
port = _find_free_port()
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
c = CliceClient()
await c.start_io(*cmd)
init_options = {"project": {"cache_dir": str(workspace / ".clice")}}
await c.initialize(workspace, initialization_options=init_options)
rpc = AgenticRpcClient(host, port)
body = json.dumps({"jsonrpc": "2.0", "method": "agentic/shutdown", "params": {}})
rpc.sock.sendall(f"Content-Length: {len(body)}\r\n\r\n{body}".encode())
rpc.sock.settimeout(5)
try:
rpc.sock.recv(4096)
except (socket.timeout, OSError):
pass
rpc.sock.close()
import asyncio
for _ in range(20):
if c._server.returncode is not None:
break
await asyncio.sleep(0.5)
assert c._server.returncode is not None, "Server did not exit after shutdown"
@pytest.mark.workspace("index_features")
async def test_rpc_symbol_not_found(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/definition", {"name": "nonexistent_symbol_xyz"})
assert "error" in resp
@pytest.mark.workspace("index_features")
async def test_rpc_symbol_id_roundtrip(indexed_agentic, workspace):
"""Search -> get symbolId -> definition -> verify consistency."""
rpc, _ = indexed_agentic
search = rpc.request("agentic/symbolSearch", {"query": "compute"})
assert "result" in search
symbols = search["result"]["symbols"]
compute = next((s for s in symbols if s["name"] == "compute"), None)
assert compute is not None, f"'compute' not found in {[s['name'] for s in symbols]}"
defn = rpc.request("agentic/definition", {"symbolId": compute["symbolId"]})
assert "result" in defn
assert defn["result"]["name"] == "compute"
assert defn["result"]["symbolId"] == compute["symbolId"]
@pytest.mark.workspace("index_features")
async def test_rpc_file_deps(indexed_agentic, workspace):
rpc, _ = indexed_agentic
path = (workspace / "main.cpp").as_posix()
resp = rpc.request("agentic/fileDeps", {"path": path})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert result["file"] == path
assert isinstance(result["includes"], list)
assert isinstance(result["includers"], list)
@pytest.mark.workspace("index_features")
async def test_rpc_file_deps_direction(indexed_agentic, workspace):
rpc, _ = indexed_agentic
path = (workspace / "main.cpp").as_posix()
resp = rpc.request("agentic/fileDeps", {"path": path, "direction": "includes"})
assert "result" in resp
assert resp["result"]["includers"] == []
@pytest.mark.workspace("index_features")
async def test_rpc_file_deps_unknown(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/fileDeps", {"path": "/nonexistent/file.cpp"})
assert "result" in resp
assert resp["result"]["includes"] == []
assert resp["result"]["includers"] == []
@pytest.mark.workspace("index_features")
async def test_rpc_impact_analysis(indexed_agentic, workspace):
rpc, _ = indexed_agentic
path = (workspace / "main.cpp").as_posix()
resp = rpc.request("agentic/impactAnalysis", {"path": path})
assert "result" in resp, f"unexpected response: {resp}"
result = resp["result"]
assert isinstance(result["directDependents"], list)
assert isinstance(result["transitiveDependents"], list)
assert isinstance(result["affectedModules"], list)
@pytest.mark.workspace("index_features")
async def test_rpc_impact_analysis_unknown(indexed_agentic, workspace):
rpc, _ = indexed_agentic
resp = rpc.request("agentic/impactAnalysis", {"path": "/nonexistent/file.cpp"})
assert "result" in resp
assert resp["result"]["directDependents"] == []
async def test_shutdown_during_indexing(executable, tmp_path):
"""Shutdown during active background indexing must exit cleanly."""
from tests.integration.utils.client import CliceClient
from tests.conftest import _find_free_port
workspace = tmp_path / "ws"
workspace.mkdir()
entries = []
for i in range(20):
src = workspace / f"file_{i}.cpp"
src.write_text(
f"struct Type_{i} {{ int v = {i}; void m() {{}} }};\n"
f"int func_{i}(int x) {{ return x + {i}; }}\n"
f"int caller_{i}() {{ return func_{i}({i}); }}\n"
)
entries.append(
{
"directory": workspace.as_posix(),
"file": src.as_posix(),
"arguments": ["clang++", "-std=c++17", "-fsyntax-only", src.as_posix()],
}
)
(workspace / "compile_commands.json").write_text(json.dumps(entries))
host = "127.0.0.1"
port = _find_free_port()
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
c = CliceClient()
await c.start_io(*cmd)
init_options = {
"project": {
"cache_dir": str(workspace / ".clice"),
"idle_timeout_ms": 0,
}
}
await c.initialize(workspace, initialization_options=init_options)
# Give indexing a moment to start, then send shutdown
await asyncio.sleep(0.5)
rpc = AgenticRpcClient(host, port)
body = json.dumps({"jsonrpc": "2.0", "method": "agentic/shutdown", "params": {}})
rpc.sock.sendall(f"Content-Length: {len(body)}\r\n\r\n{body}".encode())
rpc.sock.settimeout(5)
try:
rpc.sock.recv(4096)
except (socket.timeout, OSError):
pass
rpc.sock.close()
for _ in range(30):
if c._server.returncode is not None:
break
await asyncio.sleep(0.5)
assert c._server.returncode is not None, "Server did not exit after shutdown"
assert c._server.returncode >= 0, (
f"Server crashed with signal {-c._server.returncode}"
)

View File

@@ -1,189 +0,0 @@
"""CLI-based tests for agentic mode — run clice --mode agentic as a subprocess."""
import json
import subprocess
import pytest
from tests.integration.utils.wait import wait_for_index
def run_cli(executable, host, port, method, **kwargs):
cmd = [
str(executable),
"--mode",
"agentic",
"--host",
host,
"--port",
str(port),
"--method",
method,
]
for k, v in kwargs.items():
cmd.extend([f"--{k}", str(v)])
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return result
@pytest.fixture
async def indexed_server(request, executable, workspace):
"""Start server with LSP+agentic, compile a file, wait for indexing."""
import asyncio
from tests.integration.utils.client import CliceClient
from tests.conftest import _shutdown_client, _find_free_port
host = "127.0.0.1"
port = _find_free_port()
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
c = CliceClient()
await c.start_io(*cmd)
init_options = {"project": {"cache_dir": str(workspace / ".clice")}}
await c.initialize(workspace, initialization_options=init_options)
uri, _ = await c.open_and_wait(workspace / "main.cpp")
assert await wait_for_index(c, uri, "add"), "Index not ready"
from tests.integration.agentic.test_agentic import AgenticRpcClient
rpc = AgenticRpcClient(host, port)
for _ in range(30):
resp = rpc.request("agentic/symbolSearch", {"query": "add"})
if "result" in resp and resp["result"]["symbols"]:
break
await asyncio.sleep(1)
rpc.close()
yield executable, host, port, workspace
c.close(uri)
await _shutdown_client(c)
@pytest.mark.workspace("index_features")
async def test_cli_compile_command(indexed_server, workspace):
exe, host, port, _ = indexed_server
path = (workspace / "main.cpp").as_posix()
r = run_cli(exe, host, port, "compileCommand", path=path)
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["file"] == path
assert len(data["arguments"]) > 0
@pytest.mark.workspace("index_features")
async def test_cli_symbol_search(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "symbolSearch", query="add")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
names = [s["name"] for s in data["symbols"]]
assert "add" in names
add_sym = next(s for s in data["symbols"] if s["name"] == "add")
assert add_sym["kind"] == "Function"
assert add_sym["line"] == 19
@pytest.mark.workspace("index_features")
async def test_cli_definition(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "definition", name="add")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["name"] == "add"
defn = data["definition"]
assert defn["startLine"] == 19
assert defn["endLine"] == 21
assert "return a + b;" in defn["text"]
@pytest.mark.workspace("index_features")
async def test_cli_definition_by_position(indexed_server, workspace):
exe, host, port, _ = indexed_server
path = (workspace / "main.cpp").as_posix()
r = run_cli(exe, host, port, "definition", path=path, line=19)
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["name"] == "add"
@pytest.mark.workspace("index_features")
async def test_cli_references(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "references", name="global_var")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["name"] == "global_var"
assert data["total"] == 2
lines = sorted(ref["line"] for ref in data["references"])
assert lines == [34, 38]
@pytest.mark.workspace("index_features")
async def test_cli_read_symbol(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "readSymbol", name="compute")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["name"] == "compute"
assert "add(1, 2)" in data["text"]
@pytest.mark.workspace("index_features")
async def test_cli_document_symbols(indexed_server, workspace):
exe, host, port, _ = indexed_server
path = (workspace / "main.cpp").as_posix()
r = run_cli(exe, host, port, "documentSymbols", path=path)
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
names = [s["name"] for s in data["symbols"]]
assert "add" in names
assert "main" in names
assert "global_var" in names
@pytest.mark.workspace("index_features")
async def test_cli_call_graph(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "callGraph", name="add", direction="callers")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["root"]["name"] == "add"
caller_names = [c["name"] for c in data["callers"]]
assert "compute" in caller_names
@pytest.mark.workspace("index_features")
async def test_cli_type_hierarchy(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "typeHierarchy", name="Dog", direction="supertypes")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["root"]["name"] == "Dog"
supertype_names = [t["name"] for t in data["supertypes"]]
assert "Animal" in supertype_names
@pytest.mark.workspace("index_features")
async def test_cli_project_files(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "projectFiles")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert data["total"] > 0
paths = [f["path"] for f in data["files"]]
assert any("main.cpp" in p for p in paths)
@pytest.mark.workspace("index_features")
async def test_cli_status(indexed_server, workspace):
exe, host, port, _ = indexed_server
r = run_cli(exe, host, port, "status")
assert r.returncode == 0, f"stderr: {r.stderr}"
data = json.loads(r.stdout)
assert isinstance(data["idle"], bool)
assert data["total"] > 0
assert isinstance(data["pending"], int)
assert isinstance(data["indexed"], int)

View File

@@ -1,74 +0,0 @@
import pytest
from lsprotocol.types import Position, Range
from tests.integration.utils.workspace import did_change
UNFORMATTED = "int add( int a , int b ) {\nreturn a+b ;\n}\n"
FORMATTED = "int add(int a, int b) { return a + b; }\n"
def apply_edits(text, edits):
"""Apply LSP TextEdits to a string, processing from end to start."""
lines = text.split("\n")
for edit in sorted(
edits, key=lambda e: (e.range.start.line, e.range.start.character), reverse=True
):
start = edit.range.start
end = edit.range.end
before = (
"\n".join(lines[: start.line])
+ ("\n" if start.line > 0 else "")
+ lines[start.line][: start.character]
)
after = (
lines[end.line][end.character :]
+ ("\n" if end.line < len(lines) - 1 else "")
+ "\n".join(lines[end.line + 1 :])
)
text = before + edit.new_text + after
lines = text.split("\n")
return text
@pytest.mark.workspace("formatting")
async def test_format_document(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
did_change(client, uri, 1, UNFORMATTED)
edits = await client.format_document(uri)
assert edits is not None
assert len(edits) > 0
result = apply_edits(UNFORMATTED, edits)
assert result == FORMATTED
client.close(uri)
@pytest.mark.workspace("formatting")
async def test_format_range(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
did_change(client, uri, 1, UNFORMATTED)
edits = await client.format_range(
uri,
Range(start=Position(line=1, character=0), end=Position(line=2, character=0)),
)
assert edits is not None
assert len(edits) > 0
client.close(uri)
@pytest.mark.workspace("formatting")
async def test_format_already_formatted(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
did_change(client, uri, 1, FORMATTED)
edits = await client.format_document(uri)
assert edits is not None
assert len(edits) == 0
client.close(uri)

View File

@@ -34,8 +34,6 @@ async def test_capabilities(client, workspace):
assert capability_enabled(caps.folding_range_provider)
assert capability_enabled(caps.inlay_hint_provider)
assert capability_enabled(caps.code_action_provider)
assert caps.document_formatting_provider is True
assert caps.document_range_formatting_provider is True
assert caps.semantic_tokens_provider is not None

View File

@@ -16,12 +16,9 @@ from lsprotocol.types import (
Diagnostic,
DidCloseTextDocumentParams,
DidOpenTextDocumentParams,
DocumentFormattingParams,
DocumentLinkParams,
DocumentRangeFormattingParams,
DocumentSymbolParams,
FoldingRangeParams,
FormattingOptions,
HoverParams,
InlayHintParams,
InitializeParams,
@@ -95,18 +92,13 @@ class CliceClient(BaseLanguageClient):
*,
initialization_options: dict | None = None,
) -> InitializeResult:
if initialization_options is None:
initialization_options = {}
project = dict(initialization_options.get("project", {}))
project.setdefault("cache_dir", str(workspace / ".clice"))
initialization_options["project"] = project
params = InitializeParams(
capabilities=ClientCapabilities(),
root_uri=workspace.as_uri(),
workspace_folders=[WorkspaceFolder(uri=workspace.as_uri(), name="test")],
)
params.initialization_options = initialization_options
if initialization_options is not None:
params.initialization_options = initialization_options
result = await self.initialize_async(params)
self.initialized(InitializedParams())
self.init_result = result
@@ -315,29 +307,6 @@ class CliceClient(BaseLanguageClient):
timeout=timeout,
)
async def format_document(self, uri: str, *, timeout: float = 30.0):
return await asyncio.wait_for(
self.text_document_formatting_async(
DocumentFormattingParams(
text_document=TextDocumentIdentifier(uri=uri),
options=FormattingOptions(tab_size=4, insert_spaces=True),
)
),
timeout=timeout,
)
async def format_range(self, uri: str, range_: Range, *, timeout: float = 30.0):
return await asyncio.wait_for(
self.text_document_range_formatting_async(
DocumentRangeFormattingParams(
text_document=TextDocumentIdentifier(uri=uri),
range=range_,
options=FormattingOptions(tab_size=4, insert_spaces=True),
)
),
timeout=timeout,
)
# ── Extension protocol ───────────────────────────────────────────
async def query_context(self, uri: str, *, timeout: float = 30.0):

View File

@@ -1,14 +0,0 @@
---
source: document_symbol_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { name: "basic_if", kind: Namespace, range: "1:0-35:1", selection_range: "1:10-1:18" }
- { name: "abs_val", kind: Function, range: "3:0-7:1", selection_range: "3:4-3:11", detail: "int (int)" }
- { name: "sign", kind: Function, range: "9:0-17:1", selection_range: "9:12-9:16", detail: "const char *(int)" }
- { name: "nested_if", kind: Function, range: "20:0-27:1", selection_range: "20:4-20:13", detail: "int (int, int)" }
- { name: "test", kind: Function, range: "29:0-33:1", selection_range: "29:5-29:9", detail: "void ()" }
- { name: "r1", kind: Variable, range: "30:21-30:41", selection_range: "30:25-30:27", detail: "int" }
- { name: "r2", kind: Variable, range: "31:21-31:38", selection_range: "31:26-31:28", detail: "const char *" }
- { name: "r3", kind: Variable, range: "32:21-32:46", selection_range: "32:25-32:27", detail: "int" }

View File

@@ -1,11 +0,0 @@
---
source: folding_range_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { range: "1:19-35:1", kind: namespace, collapsed_text: "{...}" }
- { range: "3:19-7:1", kind: functionBody, collapsed_text: "{...}" }
- { range: "9:24-17:1", kind: functionBody, collapsed_text: "{...}" }
- { range: "20:28-27:1", kind: functionBody, collapsed_text: "{...}" }
- { range: "29:12-33:1", kind: functionBody, collapsed_text: "{...}" }

View File

@@ -1,11 +0,0 @@
---
source: inlay_hint_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { pos: "30:38", kind: Parameter, label: "x:", padding_right: true }
- { pos: "31:28", kind: Type, label: ": const char *" }
- { pos: "31:36", kind: Parameter, label: "x:", padding_right: true }
- { pos: "32:40", kind: Parameter, label: "a:", padding_right: true }
- { pos: "32:43", kind: Parameter, label: "b:", padding_right: true }

View File

@@ -1,75 +0,0 @@
---
source: semantic_tokens_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { loc: "0:0", text: "// basic if and if-else", kind: Comment }
- { loc: "1:0", text: "namespace", kind: Keyword }
- { loc: "1:10", text: "basic_if", kind: Namespace, modifiers: [Definition] }
- { loc: "3:0", text: "int", kind: Keyword }
- { loc: "3:4", text: "abs_val", kind: Function, modifiers: [Definition] }
- { loc: "3:12", text: "int", kind: Keyword }
- { loc: "3:16", text: "x", kind: Parameter, modifiers: [Definition] }
- { loc: "4:4", text: "if", kind: Keyword }
- { loc: "4:7", text: "x", kind: Parameter }
- { loc: "4:11", text: "0", kind: Number }
- { loc: "5:8", text: "return", kind: Keyword }
- { loc: "5:16", text: "x", kind: Parameter }
- { loc: "6:4", text: "return", kind: Keyword }
- { loc: "6:11", text: "x", kind: Parameter }
- { loc: "9:0", text: "const", kind: Keyword }
- { loc: "9:6", text: "char", kind: Keyword }
- { loc: "9:12", text: "sign", kind: Function, modifiers: [Definition, Readonly] }
- { loc: "9:17", text: "int", kind: Keyword }
- { loc: "9:21", text: "x", kind: Parameter, modifiers: [Definition] }
- { loc: "10:4", text: "if", kind: Keyword }
- { loc: "10:7", text: "x", kind: Parameter }
- { loc: "10:11", text: "0", kind: Number }
- { loc: "11:8", text: "return", kind: Keyword }
- { loc: "11:15", text: "\"positive\"", kind: String }
- { loc: "12:6", text: "else", kind: Keyword }
- { loc: "12:11", text: "if", kind: Keyword }
- { loc: "12:14", text: "x", kind: Parameter }
- { loc: "12:18", text: "0", kind: Number }
- { loc: "13:8", text: "return", kind: Keyword }
- { loc: "13:15", text: "\"negative\"", kind: String }
- { loc: "14:6", text: "else", kind: Keyword }
- { loc: "15:8", text: "return", kind: Keyword }
- { loc: "15:15", text: "\"zero\"", kind: String }
- { loc: "19:0", text: "// dangling else: else binds to nearest if", kind: Comment }
- { loc: "20:0", text: "int", kind: Keyword }
- { loc: "20:4", text: "nested_if", kind: Function, modifiers: [Definition] }
- { loc: "20:14", text: "int", kind: Keyword }
- { loc: "20:18", text: "a", kind: Parameter, modifiers: [Definition] }
- { loc: "20:21", text: "int", kind: Keyword }
- { loc: "20:25", text: "b", kind: Parameter, modifiers: [Definition] }
- { loc: "21:4", text: "if", kind: Keyword }
- { loc: "21:7", text: "a", kind: Parameter }
- { loc: "21:11", text: "0", kind: Number }
- { loc: "22:8", text: "if", kind: Keyword }
- { loc: "22:11", text: "b", kind: Parameter }
- { loc: "22:15", text: "0", kind: Number }
- { loc: "23:12", text: "return", kind: Keyword }
- { loc: "23:19", text: "1", kind: Number }
- { loc: "24:8", text: "else", kind: Keyword }
- { loc: "25:12", text: "return", kind: Keyword }
- { loc: "25:19", text: "2", kind: Number }
- { loc: "26:4", text: "return", kind: Keyword }
- { loc: "26:11", text: "0", kind: Number }
- { loc: "29:0", text: "void", kind: Keyword }
- { loc: "29:5", text: "test", kind: Function, modifiers: [Definition] }
- { loc: "30:21", text: "int", kind: Keyword }
- { loc: "30:25", text: "r1", kind: Variable, modifiers: [Definition] }
- { loc: "30:30", text: "abs_val", kind: Function }
- { loc: "30:39", text: "3", kind: Number }
- { loc: "31:21", text: "auto", kind: Keyword }
- { loc: "31:26", text: "r2", kind: Variable, modifiers: [Definition, Readonly] }
- { loc: "31:31", text: "sign", kind: Function, modifiers: [Readonly] }
- { loc: "31:36", text: "5", kind: Number }
- { loc: "32:21", text: "int", kind: Keyword }
- { loc: "32:25", text: "r3", kind: Variable, modifiers: [Definition] }
- { loc: "32:30", text: "nested_if", kind: Function }
- { loc: "32:40", text: "1", kind: Number }
- { loc: "32:44", text: "1", kind: Number }
- { loc: "35:3", text: "// namespace basic_if", kind: Comment }

View File

@@ -1,28 +0,0 @@
---
source: tu_index_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { loc: "1:10", kind: Namespace, text: "basic_if", relations: [Definition] }
- { loc: "3:4", kind: Function, text: "abs_val", relations: [Definition] }
- { loc: "3:16", kind: Parameter, text: "x", relations: [Definition] }
- { loc: "4:7", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "5:16", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "6:11", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "9:12", kind: Function, text: "sign", relations: [Definition] }
- { loc: "9:21", kind: Parameter, text: "x", relations: [Definition] }
- { loc: "10:7", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "12:14", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "20:4", kind: Function, text: "nested_if", relations: [Definition] }
- { loc: "20:18", kind: Parameter, text: "a", relations: [Definition] }
- { loc: "20:25", kind: Parameter, text: "b", relations: [Definition] }
- { loc: "21:7", kind: Parameter, text: "a", relations: [Reference] }
- { loc: "22:11", kind: Parameter, text: "b", relations: [Reference] }
- { loc: "29:5", kind: Function, text: "test", relations: [Definition] }
- { loc: "30:25", kind: Variable, text: "r1", relations: [Definition] }
- { loc: "30:30", kind: Function, text: "abs_val", relations: [Reference] }
- { loc: "31:26", kind: Variable, text: "r2", relations: [Definition] }
- { loc: "31:31", kind: Function, text: "sign", relations: [Reference] }
- { loc: "32:25", kind: Variable, text: "r3", relations: [Definition] }
- { loc: "32:30", kind: Function, text: "nested_if", relations: [Reference] }

View File

@@ -11,7 +11,7 @@ namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(document_link, Tester) {
TEST_SUITE(DocumentLink, Tester) {
std::vector<protocol::DocumentLink> links;
@@ -136,7 +136,7 @@ ABCDE
EXPECT_LINK(0, "0", TestVFS::path("data.bin"));
}
}; // TEST_SUITE(document_link)
}; // TEST_SUITE(DocumentLink)
} // namespace

View File

@@ -1,5 +1,4 @@
#include <cstddef>
#include <format>
#include <functional>
#include <memory>
#include <vector>
@@ -8,15 +7,13 @@
#include "test/tester.h"
#include "feature/feature.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(document_symbol, Tester) {
TEST_SUITE(DocumentSymbol, Tester) {
std::vector<protocol::DocumentSymbol> symbols;
@@ -183,57 +180,7 @@ VAR(test)
ASSERT_EQ(total_size(symbols), 3U);
}
void format_document_symbols(std::string& out,
const feature::PositionMapper& mapper,
llvm::ArrayRef<feature::DocumentSymbol> nodes,
int depth) {
auto pad = std::string(depth * 2, ' ');
for(auto& node: nodes) {
auto kind = kota::meta::enum_name(static_cast<SymbolKind::Kind>(node.kind), "Unknown");
auto start = mapper.to_position(node.range.begin);
auto end = mapper.to_position(node.range.end);
if(!start || !end)
continue;
auto sel_start = mapper.to_position(node.selection_range.begin);
auto sel_end = mapper.to_position(node.selection_range.end);
out += std::format("- {}{{ name: {}, kind: {}, range: \"{}:{}-{}:{}\"",
pad,
yaml_str(node.name),
kind,
start->line,
start->character,
end->line,
end->character);
if(sel_start && sel_end) {
out += std::format(", selection_range: \"{}:{}-{}:{}\"",
sel_start->line,
sel_start->character,
sel_end->line,
sel_end->character);
}
if(!node.detail.empty()) {
out += std::format(", detail: {}", yaml_str(node.detail));
}
out += " }\n";
if(!node.children.empty()) {
format_document_symbols(out, mapper, node.children, depth + 1);
}
}
}
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto content = unit->interested_content();
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
format_document_symbols(result, mapper, feature::document_symbols(*unit), 0);
return result;
});
}
}; // TEST_SUITE(document_symbol)
}; // TEST_SUITE(DocumentSymbol)
} // namespace

View File

@@ -11,7 +11,7 @@ namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(folding_range, Tester) {
TEST_SUITE(FoldingRange, Tester) {
std::vector<protocol::FoldingRange> ranges;
@@ -429,36 +429,7 @@ $(1)#pragma region level1
)cpp");
}
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto ranges = feature::folding_ranges(*unit);
feature::PositionMapper mapper(unit->interested_content(), feature::PositionEncoding::UTF8);
std::string result;
for(auto& r: ranges) {
auto start = mapper.to_position(r.range.begin);
auto end = mapper.to_position(r.range.end);
if(!start || !end)
continue;
result += std::format("- {{ range: \"{}:{}-{}:{}\"",
start->line,
start->character,
end->line,
end->character);
if(r.kind.has_value()) {
result += std::format(", kind: {}", static_cast<const std::string&>(*r.kind));
}
if(!r.collapsed_text.empty()) {
result += std::format(", collapsed_text: {}", yaml_str(r.collapsed_text));
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(folding_range)
}; // TEST_SUITE(FoldingRange)
} // namespace

View File

@@ -12,29 +12,6 @@ TEST_CASE(Simple) {
ASSERT_NE(edits.size(), 0U);
}
TEST_CASE(RangeFormat) {
llvm::StringRef code = "int x=1;\nint y = 2 ;\nint z=3;\n";
LocalSourceRange range;
range.begin = static_cast<std::uint32_t>(code.find("int y"));
range.end = static_cast<std::uint32_t>(code.find("\nint z") + 1);
auto range_edits = feature::document_format("main.cpp", code, range);
auto full_edits = feature::document_format("main.cpp", code, std::nullopt);
ASSERT_NE(range_edits.size(), 0U);
EXPECT_LE(range_edits.size(), full_edits.size());
}
TEST_CASE(Idempotent) {
llvm::StringRef code = "int main() {\n return 0;\n}\n";
auto edits = feature::document_format("main.cpp", code, std::nullopt);
EXPECT_EQ(edits.size(), 0U);
}
TEST_CASE(IncludeSort) {
llvm::StringRef code = "#include <vector>\n#include <algorithm>\n\nint main() {}\n";
auto edits = feature::document_format("main.cpp", code, std::nullopt);
ASSERT_NE(edits.size(), 0U);
}
}; // TEST_SUITE(Formatting)
} // namespace

View File

@@ -1,19 +1,16 @@
#include <format>
#include <string>
#include "test/test.h"
#include "test/tester.h"
#include "feature/feature.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(inlay_hint, Tester) {
TEST_SUITE(InlayHint, Tester) {
std::vector<protocol::InlayHint> hints;
llvm::DenseMap<std::uint32_t, protocol::InlayHint> hints_map;
@@ -1532,38 +1529,6 @@ TEST_CASE(Dependent, skip = true) {
EXPECT_HINT("2", "par3:");
}
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto content = unit->interested_content();
LocalSourceRange range(0, content.size());
auto hints = feature::inlay_hints(*unit, range);
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
for(auto& hint: hints) {
auto pos = mapper.to_position(hint.offset);
if(!pos)
continue;
auto kind = kota::meta::enum_name(hint.kind, "Unknown");
result += std::format("- {{ pos: \"{}:{}\", kind: {}, label: {}",
pos->line,
pos->character,
kind,
yaml_str(hint.label));
if(hint.padding_left) {
result += ", padding_left: true";
}
if(hint.padding_right) {
result += ", padding_right: true";
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(inlay_hint)
}; // TEST_SUITE(InlayHint)
} // namespace
} // namespace clice::testing

View File

@@ -9,8 +9,6 @@
#include "feature/feature.h"
#include "semantic/symbol_kind.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
@@ -101,7 +99,7 @@ auto decode_relative_tokens(const protocol::SemanticTokens& tokens) -> std::vect
return result;
}
TEST_SUITE(semantic_tokens, Tester) {
TEST_SUITE(SemanticTokens, Tester) {
protocol::SemanticTokens tokens;
std::vector<DecodedToken> decoded;
@@ -140,10 +138,6 @@ void EXPECT_TOKEN(llvm::StringRef name,
ASSERT_EQ(token->modifiers, expected_modifiers);
}
void EXPECT_NO_TOKEN(llvm::StringRef name) {
ASSERT_TRUE(find_by_range(name) == nullptr);
}
TEST_CASE(BasicLexicalKinds) {
run_utf8(R"cpp(
@d1[#define] @m0[FOO]
@@ -270,44 +264,6 @@ int main() {
EXPECT_TOKEN("x3", SymbolKind::Variable, 0);
}
TEST_CASE(IneligibleOperatorReferenceIsSuppressed) {
run_utf8(R"cpp(
struct S {};
S operator+(S lhs, S rhs);
void use(S lhs, S rhs) {
(void)(lhs @plus[+] rhs);
}
)cpp");
EXPECT_NO_TOKEN("plus");
}
TEST_CASE(ConstructorAndDestructorNamesRemainHighlighted) {
run_utf8(R"cpp(
struct S {
@ctor_decl[S]();
@dtor_decl[~]S();
};
S::@ctor_def[S]() {}
void use(S* value) {
value->@dtor_ref[~]S();
}
)cpp");
auto declaration = modifier_mask({SymbolModifiers::Declaration});
auto definition = modifier_mask({SymbolModifiers::Definition});
auto special_member = modifier_mask({SymbolModifiers::ConstructorOrDestructor});
EXPECT_TOKEN("ctor_decl", SymbolKind::Method, declaration | special_member);
EXPECT_TOKEN("dtor_decl", SymbolKind::Method, declaration | special_member);
EXPECT_TOKEN("ctor_def", SymbolKind::Method, definition | special_member);
EXPECT_TOKEN("dtor_ref", SymbolKind::Method, special_member);
}
TEST_CASE(LegacyVarDeclTemplates) {
run_utf8(R"cpp(
extern int @x1[x];
@@ -583,53 +539,7 @@ void f() {
EXPECT_TOKEN("v2", SymbolKind::Variable, definition);
}
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto content = unit->interested_content();
auto tokens = feature::semantic_tokens(*unit);
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
for(auto& token: tokens) {
if(!token.range.valid() || token.range.end <= token.range.begin ||
token.range.end > content.size())
continue;
auto pos = mapper.to_position(token.range.begin);
if(!pos)
continue;
auto text = content.substr(token.range.begin, token.range.length());
auto kind = kota::meta::enum_name(static_cast<SymbolKind::Kind>(token.kind), "Unknown");
result += std::format("- {{ loc: \"{}:{}\", text: {}, kind: {}",
pos->line,
pos->character,
yaml_str(text),
kind);
std::string mods;
for(std::uint32_t i = 0; i < 32; ++i) {
if(token.modifiers & (1u << i)) {
auto name = kota::meta::enum_name(static_cast<SymbolModifiers::Kind>(i));
if(!name.empty()) {
if(!mods.empty())
mods += ", ";
mods += name;
}
}
}
if(!mods.empty()) {
result += std::format(", modifiers: [{}]", mods);
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(semantic_tokens)
}; // TEST_SUITE(SemanticTokens)
} // namespace

View File

@@ -1,18 +1,13 @@
#include <algorithm>
#include <format>
#include <set>
#include "test/test.h"
#include "test/tester.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
TEST_SUITE(tu_index, Tester) {
TEST_SUITE(TUIndex, Tester) {
index::TUIndex tu_index;
@@ -505,64 +500,6 @@ TEST_CASE(SymbolKinds) {
check_kind("ns", SymbolKind::Namespace);
}
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto idx = index::TUIndex::build(*unit);
auto content = unit->interested_content();
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
auto sorted = idx.main_file_index.occurrences;
std::ranges::sort(sorted, [](auto& lhs, auto& rhs) {
return std::tuple(lhs.range.begin, lhs.range.end, lhs.target) <
std::tuple(rhs.range.begin, rhs.range.end, rhs.target);
});
for(auto& occ: sorted) {
auto text = content.substr(occ.range.begin, occ.range.end - occ.range.begin);
auto pos = mapper.to_position(occ.range.begin);
if(!pos)
continue;
auto sym_it = idx.symbols.find(occ.target);
std::string_view kind_name = "?";
if(sym_it != idx.symbols.end()) {
kind_name =
kota::meta::enum_name(static_cast<SymbolKind::Kind>(sym_it->second.kind),
"Unknown");
}
result += std::format("- {{ loc: \"{}:{}\", kind: {}, text: {}",
pos->line,
pos->character,
kind_name,
yaml_str(text));
auto rel_it = idx.main_file_index.relations.find(occ.target);
if(rel_it != idx.main_file_index.relations.end()) {
std::string rels;
for(auto& rel: rel_it->second) {
if(rel.range != occ.range)
continue;
if(!rels.empty())
rels += ", ";
rels += kota::meta::enum_name(static_cast<RelationKind::Kind>(rel.kind), "?");
}
if(!rels.empty()) {
result += std::format(", relations: [{}]", rels);
}
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(tu_index)
}; // TEST_SUITE(TUIndex)
} // namespace
} // namespace clice::testing

View File

@@ -456,6 +456,8 @@ TEST_CASE(BasePackExpansion) {
)code");
}
// --- Robustness tests for edge cases found during stress testing ---
TEST_CASE(RecursiveBaseClass) {
// Regression test: callback_traits<F> inherits callback_traits<decltype(&F::operator())>,
// creating infinite recursion through lookupInBases. CTD cycle detection must bail out.

View File

@@ -3,7 +3,7 @@
#include "test/test.h"
#include "command/command.h"
#include "compile/compilation.h"
#include "server/compiler/compile_graph.h"
#include "server/compile_graph.h"
#include "support/path_pool.h"
#include "syntax/dependency_graph.h"
#include "syntax/scan.h"

View File

@@ -1,7 +1,7 @@
#include <optional>
#include "test/test.h"
#include "server/compiler/compile_graph.h"
#include "server/compile_graph.h"
namespace clice::testing {
namespace {

View File

@@ -2,7 +2,7 @@
#include "test/temp_dir.h"
#include "test/test.h"
#include "server/workspace/config.h"
#include "server/config.h"
#include "support/filesystem.h"
#include "kota/codec/json/json.h"

View File

@@ -2,7 +2,7 @@
#include <vector>
#include "test/test.h"
#include "server/protocol/worker.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
namespace clice::testing {
@@ -29,6 +29,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
tmp.touch("consumer.cpp", "import Hello;\n" "int main() { return hello()[0]; }\n");
auto consumer = tmp.path("consumer.cpp");
// --- Phase 1: Build PCM via stateless worker ---
WorkerHandle sl;
ASSERT_TRUE(sl.spawn("stateless-worker"));
@@ -62,6 +63,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
ASSERT_TRUE(phase1_done);
ASSERT_FALSE(pcm_path.empty());
// --- Phase 2: Compile consumer with the PCM via stateful worker ---
WorkerHandle sf;
ASSERT_TRUE(sf.spawn("stateful-worker"));

View File

@@ -2,7 +2,7 @@
#include <vector>
#include "test/test.h"
#include "server/protocol/worker.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
#include "syntax/scan.h"
@@ -30,6 +30,7 @@ TEST_CASE(BuildPCHThenCompile) {
auto dir = std::string(tmp.root);
// --- Phase 1: Build PCH via stateless worker ---
WorkerHandle sl;
ASSERT_TRUE(sl.spawn("stateless-worker"));
@@ -68,6 +69,7 @@ TEST_CASE(BuildPCHThenCompile) {
// Verify the PCH file exists on disk.
ASSERT_TRUE(llvm::sys::fs::exists(pch_path));
// --- Phase 2: Compile with PCH via stateful worker ---
WorkerHandle sf;
ASSERT_TRUE(sf.spawn("stateful-worker"));

View File

@@ -2,7 +2,7 @@
#include <vector>
#include "test/test.h"
#include "server/protocol/worker.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
#include "kota/codec/json/json.h"

View File

@@ -2,7 +2,7 @@
#include <vector>
#include "test/test.h"
#include "server/protocol/worker.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
#include "kota/codec/bincode/bincode.h"

View File

@@ -11,7 +11,7 @@
#include "test/temp_dir.h"
#include "command/argument_parser.h"
#include "command/command.h"
#include "server/protocol/worker.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "kota/async/async.h"

View File

@@ -291,6 +291,8 @@ int x;
TEST_SUITE(PreambleComplete) {
// --- #include completeness ---
TEST_CASE(CompleteQuotedInclude) {
llvm::StringRef content = "#include \"foo.h\"\nint x;";
auto bound = compute_preamble_bound(content);
@@ -339,7 +341,8 @@ TEST_CASE(MultipleIncludesLastIncomplete) {
EXPECT_FALSE(is_preamble_complete(content, bound));
}
// compute_preamble_bound does not include import/export lines in its
// --- C++20 module statements ---
// Note: compute_preamble_bound does not include import/export lines in its
// bound, so we pass manual bounds covering the relevant lines.
TEST_CASE(CompleteImport) {
@@ -378,6 +381,8 @@ TEST_CASE(CompleteExportImport) {
EXPECT_TRUE(is_preamble_complete(content, 19));
}
// --- Edge cases ---
TEST_CASE(EmptyPreamble) {
llvm::StringRef content = "int x;";
EXPECT_TRUE(is_preamble_complete(content, 0));

View File

@@ -1,5 +1,3 @@
#include <string>
#include "llvm/ADT/SmallString.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
@@ -7,12 +5,6 @@
namespace clice::testing {
/// Set by --test-dir from the command line; empty if not specified.
inline std::string test_dir;
/// Set by --corpus-dir from the command line; empty if not specified.
inline std::string corpus_dir;
#ifdef _WIN32
constexpr inline bool Windows = true;
#else

View File

@@ -230,17 +230,6 @@ bool Tester::compile_with_modules(llvm::StringRef standard) {
return try_compile();
}
bool Tester::compile_file(llvm::StringRef path, llvm::StringRef standard) {
auto buffer = llvm::MemoryBuffer::getFile(path);
if(!buffer) {
LOG_ERROR("Failed to read file: {}", path);
return false;
}
auto filename = llvm::sys::path::filename(path);
add_main(filename, (*buffer)->getBuffer());
return compile(standard);
}
std::uint32_t Tester::point(llvm::StringRef name, llvm::StringRef file) {
if(file.empty()) {
file = src_path;

View File

@@ -1,6 +1,5 @@
#pragma once
#include <format>
#include <optional>
#include <string>
#include <vector>
@@ -64,9 +63,6 @@ struct Tester {
bool compile_with_modules(llvm::StringRef standard = "-std=c++20");
/// Read a file from disk and compile it directly (no VFS content needed).
bool compile_file(llvm::StringRef path, llvm::StringRef standard = "-std=c++20");
/// Driver path: uses CompilationDatabase + toolchain cache, has system headers.
void prepare_driver(llvm::StringRef standard = "-std=c++20");
@@ -89,28 +85,4 @@ struct Tester {
void clear();
};
inline std::string yaml_str(llvm::StringRef s) {
std::string result;
result.reserve(s.size() + 2);
result += '"';
for(char c: s) {
switch(c) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default:
if(static_cast<unsigned char>(c) < 0x20) {
result += std::format("\\x{:02x}", static_cast<unsigned char>(c));
} else {
result += c;
}
break;
}
}
result += '"';
return result;
}
} // namespace clice::testing

View File

@@ -1,7 +1,6 @@
#include <string>
#include <string_view>
#include "test/platform.h"
#include "support/logging.h"
#include "kota/deco/deco.h"
@@ -12,20 +11,23 @@ namespace {
using kota::deco::decl::KVStyle;
struct TestOptions {
kota::zest::Options zest;
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--test-filter", "--test-filter="},
help = "Filter tests by name",
required = false)
<std::string> test_filter;
DecoKVStyled(KVStyle::JoinedOrSeparate, help = "log level: trace/debug/info/warn/err";
required = false)
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--log-level", "--log-level="},
help = "Log level: trace/debug/info/warn/err",
required = false)
<std::string> log_level;
DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = "<DIR>"; help = "test data directory";
required = false)
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--test-dir", "--test-dir="},
help = "Test data directory",
required = false)
<std::string> test_dir;
DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = "<DIR>";
help = "corpus directory for snapshot glob tests";
required = false)
<std::string> corpus_dir;
};
} // namespace
@@ -34,20 +36,13 @@ int main(int argc, const char** argv) {
auto args = kota::deco::util::argvify(argc, argv);
auto parsed = kota::deco::cli::parse<TestOptions>(args);
if(!parsed.has_value()) {
return 1;
std::string_view filter = {};
if(parsed.has_value() && parsed->options.test_filter.has_value()) {
filter = *parsed->options.test_filter;
}
auto& opts = parsed->options;
if(opts.test_dir.has_value())
clice::testing::test_dir = *opts.test_dir;
if(opts.corpus_dir.has_value())
clice::testing::corpus_dir = *opts.corpus_dir;
if(opts.log_level.has_value()) {
auto level = *opts.log_level;
if(parsed.has_value() && parsed->options.log_level.has_value()) {
auto level = *parsed->options.log_level;
if(level == "trace") {
clice::logging::options.level = clice::logging::Level::trace;
} else if(level == "debug") {
@@ -63,5 +58,5 @@ int main(int argc, const char** argv) {
clice::logging::stderr_logger("test", clice::logging::options);
return kota::zest::run_tests(std::move(opts.zest));
return kota::zest::Runner::instance().run_tests(filter);
}