Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ Status of the `main` branch. Changes prior to the next official version change w
- Make tool call errors surface explicitly as errors at the MCP protocol level

* Language Servers:
- C/C++ (clangd): add Unreal Engine 5 fixture and tests verifying that reflection-macro
code (`UCLASS`, `UFUNCTION`, `UPROPERTY`, `GENERATED_BODY`) yields correct symbols,
references, definitions, and rename edits in hand-written sources (never in
UHT-generated files); add an Unreal Engine setup guide.
- C/C++ (clangd): detect Unreal Engine projects by a `.uproject` file at the project
root and apply a default `ignored_paths` preset (`Binaries`, `DerivedDataCache`,
`Intermediate`, `Saved`) when autogenerating their configuration. When clangd starts
in such a project without a `compile_commands.json`, Serena logs a warning that points
to the Unreal Engine setup guide. #1566
- `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`.
The dict is forwarded to vtsls via `initializationOptions`, `workspace/didChangeConfiguration`,
and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing
Expand Down
3 changes: 2 additions & 1 deletion docs/01-about/020_programming-languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ Some languages require additional installations or setup steps, as noted.
* **C/C++**
(by default, uses the clangd language server (language `cpp`) but we also support ccls (language `cpp_ccls`);
for best results, provide a `compile_commands.json` at the repository root;
see the [C/C++ Setup Guide](../03-special-guides/cpp_setup) for details.)
see the [C/C++ Setup Guide](../03-special-guides/cpp_setup) for details;
for Unreal Engine 5 projects, see the [Unreal Engine Setup Guide](../03-special-guides/unreal_engine_setup_guide_for_serena).)
* **Clojure**
* **Crystal**
(requires [Crystalline](https://github.com/elbywan/crystalline) language server to be installed and available on PATH;
Expand Down
5 changes: 5 additions & 0 deletions docs/03-special-guides/cpp_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,8 @@ the language server is restarted.
- Clangd official documentation: https://clangd.llvm.org/
- Clangd project setup: https://clangd.llvm.org/installation#project-setup
- CCLS repository: https://github.com/MaskRay/ccls

## Unreal Engine projects

For Unreal Engine 5 projects (reflection macros, UnrealBuildTool), see the
[Unreal Engine Setup Guide](unreal_engine_setup_guide_for_serena.md).
107 changes: 107 additions & 0 deletions docs/03-special-guides/unreal_engine_setup_guide_for_serena.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Unreal Engine Setup Guide

This guide explains how to prepare an Unreal Engine 5 C++ project so that Serena's
clangd-based C/C++ support can provide full code intelligence: symbol search,
cross-file references, and symbol-level editing in your hand-written sources.

UE game code uses a macro-based reflection layer (`UCLASS`, `UFUNCTION`, `UPROPERTY`,
`GENERATED_BODY`) and engine types (`TArray`, `TMap`). clangd handles all of this,
provided it receives the compiler flags for your project via a `compile_commands.json`
at the project root. Unreal's build system (UnrealBuildTool) does not produce this
file by default; this guide shows how to obtain it.

---
## Prerequisites

- An Unreal Engine 5 C++ project that has been **built at least once** (the build
generates the `*.generated.h` headers that your sources include).
- No additional language server: Serena downloads clangd automatically.
- clangd never compiles your code. The compilation database is only a list of flags.

---
## Getting a compilation database

If clangd starts in a project that has a `.uproject` file but no `compile_commands.json`,
Serena logs a warning that points back to this guide. Pick one of the following routes to
create the database.

### Route 1: VSCode project files (no extra installs)

UnrealBuildTool's VSCode project generator emits per-project compile commands.
Run it from your engine installation (VSCode itself is not required):

<Engine>\Build\BatchFiles\Build.bat -projectfiles -project="<YourProject>.uproject" -game -VSCode

This produces `.vscode/compileCommands_<YourProject>.json` inside your project.
Copy or symlink it to the project root as `compile_commands.json`.

If you already use VSCode with UE, the file likely exists; the editor's
"Tools > Refresh Visual Studio Code Project" action maintains it.

### Route 2: UnrealBuildTool's clang database mode (requires LLVM installed)

<Engine>\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.exe -mode=GenerateClangDatabase -project="<YourProject>.uproject" <YourProject>Editor Win64 Development -OutputDir="<YourProject's directory>"

This emits clang-native commands (cleanest flags for clangd) but requires a Clang
toolchain installed on Windows.

---
## Recommended project configuration

Generated reflection code (`*.gen.cpp`, `*.generated.h`) legitimately references your
functions, so symbol results can include hits inside `Intermediate/`. UE's build
artifacts are therefore excluded from indexing in your project's `.serena/project.yml`:

ignored_paths:
- "Intermediate"
- "Saved"
- "Binaries"
- "DerivedDataCache"

When Serena generates the configuration for a project that has a `.uproject` file at its
root, it adds these entries automatically. Add them by hand only when editing an existing
`.serena/project.yml`.

---
## Known behavior

- **`GENERATED_BODY()` and `__LINE__`:** the macro expands using its line number.
After editing lines above it, clangd may report stale-macro diagnostics until the
next build regenerates headers. Symbol operations keep working, since clangd is
designed to operate on code with errors.
- **First index:** large projects take a few minutes to index once; afterwards
results are incremental. The index cache is kept under `.serena/.cache` inside
the project.
- **New `UFUNCTION`/`UCLASS` declarations** need a build before their generated
headers exist.
- **Symbol searches on large projects:** prefer passing `relative_path` to
`find_symbol`. An unscoped search visits every translation unit, and UE
files are expensive to parse because each pulls in large engine headers.

---
## Troubleshooting

Extra flags are easiest to add via a `.clangd` file at the project root, e.g.:

CompileFlags:
Add: [-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH, -ferror-limit=200]

- **`STL1000: Unexpected compiler version` errors:** recent MSVC STL headers
assert a minimum Clang version that may be newer than Serena's bundled clangd.
Defining `_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH` (see above) silences the
check; clangd only parses, so the mismatch is harmless.
- **Truncated symbol trees / symbols missing below a certain line:** clangd
aborts a file's parse after ~20 errors by default, which discards everything
declared after that point. Raising the limit with `-ferror-limit=200` keeps
the symbol tree intact even when diagnostics are noisy (common right after
edits, before the next UE build regenerates headers).
- **Stale results after changing the compilation database:** clangd's index
shards in `.serena/.cache` were built with the old flags. Delete that cache
directory and let the project re-index.

---
## Verifying the setup

After activating the project in Serena, a symbol overview of any `UCLASS` header
should list the class with its `UFUNCTION` methods and `UPROPERTY` fields, and
references to a method should resolve to your `Source/` files only.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,4 @@ markers = [
skip = '.git*,*.svg,*.lock,*.min.*,*test_memories_manager.py'
check-hidden = true
ignore-regex = '\.\w+'
ignore-words-list = 'paket,EDN,als'
ignore-words-list = 'paket,EDN,als,ue'
22 changes: 22 additions & 0 deletions src/serena/config/serena_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,15 @@ class SerenaConfigError(Exception):
"""


UNREAL_ENGINE_IGNORED_PATHS = ["Binaries", "DerivedDataCache", "Intermediate", "Saved"]
"""
Build and cache directories created by Unreal Engine. Applied to a C++ project's
``ignored_paths`` at autogeneration time when a ``.uproject`` file is present, so that
symbol indexing skips generated artifacts. Matched gitignore-style, so the bare names
also cover per-plugin and per-module copies (e.g. ``Plugins/Foo/Intermediate``).
"""


@dataclass(kw_only=True)
class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
project_name: str
Expand Down Expand Up @@ -400,6 +409,19 @@ def autogenerate(
config_with_comments["project_name"] = project_name
config_with_comments["languages"] = languages_to_use

# Apply the UE ignored_paths preset for C++ projects with a .uproject (#1566);
# only at autogeneration time, so an existing project.yml is never modified.
cpp_language_values = {Language.CPP.value, Language.CPP_CCLS.value}
if any(lang in cpp_language_values for lang in languages_to_use) and any(project_root.glob("*.uproject")):
ue_ignored = config_with_comments.get("ignored_paths")
if ue_ignored is None:
ue_ignored = []
config_with_comments["ignored_paths"] = ue_ignored
for path in UNREAL_ENGINE_IGNORED_PATHS:
if path not in ue_ignored:
ue_ignored.append(path)
log.info("Detected Unreal Engine project (.uproject present); applying UE ignored_paths preset.")

if save_to_disk:
project_yml_path = serena_config.get_project_yml_location(str(project_root))
log.info("Saving project configuration to %s", project_yml_path)
Expand Down
15 changes: 14 additions & 1 deletion src/solidlsp/language_servers/clangd_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def is_ignored_dirname(self, dirname: str) -> bool:
]
return super().is_ignored_dirname(dirname) or dirname in ignored_dirs

def _is_unreal_engine_project(self) -> bool:
""":return: whether the repository root contains an Unreal Engine ``.uproject`` file."""
return any(pathlib.Path(self.repository_root_path).glob("*.uproject"))

def _prepare_compile_commands(self) -> str | None:
"""
Prepare clangd compilation database with absolute directory paths.
Expand All @@ -128,7 +132,16 @@ def _prepare_compile_commands(self) -> str | None:
compile_db_path = os.path.join(self.repository_root_path, "compile_commands.json")

if not os.path.exists(compile_db_path):
# No compile_commands.json, nothing to do
# No compile_commands.json. For Unreal Engine projects, surface the setup guide
# instead of failing silently (clangd needs the DB to resolve engine code).
if self._is_unreal_engine_project():
log.warning(
"No compile_commands.json found at %s, but this looks like an Unreal Engine "
"project (.uproject present). clangd needs a compilation database to resolve "
"engine headers and macros. See docs/03-special-guides/"
"unreal_engine_setup_guide_for_serena.md to generate one.",
self.repository_root_path,
)
return None

try:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
// Test stub: trimmed to what the fixture needs.
// DECLARE_FUNCTION(execOnAbilityInput)
#pragma once

class AAbilityActor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
// Test stub: repeats the hand-written identifiers (as real thunks do) so a
// text search would surface this file; symbol-level tools must not.
//
// void UAbilityComponent::execTriggerAbility(...) { TriggerAbility(AbilityName); }
// void UAbilityComponent::execGetRemainingCooldown(...) { GetRemainingCooldown(AbilityName); }
// void AAbilityActor::execOnAbilityInput(...) { OnAbilityInput(AbilityName); }

namespace UnrealReflectionStub
{
const char* GeneratedSymbols[] = {
"UAbilityComponent",
"TriggerAbility",
"GetRemainingCooldown",
"AAbilityActor",
"OnAbilityInput",
"FAbilityInfo",
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
// Test stub: trimmed to what the fixture needs.
// DECLARE_FUNCTION(execTriggerAbility)
// DECLARE_FUNCTION(execGetRemainingCooldown)
#pragma once

class UAbilityComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
// Test stub: trimmed to what the fixture needs.
#pragma once

struct FAbilityInfo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
// Test stub: trimmed to what the fixture needs.
// DECLARE_FUNCTION(execReceiveDamage)
#pragma once

class UDamageable;
class IDamageable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
// Test stub: trimmed to what the fixture needs.
// DECLARE_FUNCTION(execHeal)
// DECLARE_FUNCTION(execReceiveDamage)
#pragma once

class AGameCharacter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "AbilityActor.h"

void AAbilityActor::BeginPlay()
{
AActor::BeginPlay();
if (AbilityComponent)
{
AbilityComponent->TriggerAbility(FName("Dash"));
}
}

void AAbilityActor::OnAbilityInput(const FName& AbilityName)
{
if (AbilityComponent)
{
AbilityComponent->TriggerAbility(AbilityName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AbilityComponent.h"
#include "AbilityActor.generated.h"

UCLASS(Blueprintable)
class TESTGAME_API AAbilityActor : public AActor
{
GENERATED_BODY()

public:
virtual void BeginPlay() override;

UFUNCTION(BlueprintCallable, Category = "Input")
void OnAbilityInput(const FName& AbilityName);

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UAbilityComponent> AbilityComponent = nullptr;

UPROPERTY(EditDefaultsOnly, Category = "Abilities")
TSubclassOf<UAbilityComponent> ComponentClass;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include "AbilityComponent.h"

void UAbilityComponent::TriggerAbility(const FName& AbilityName)
{
if (!ActiveCooldowns.Contains(AbilityName))
{
ActiveCooldowns.Add(AbilityName, 1.0f);
}
State = EAbilityState::Active;
OnAbilityTriggered.Broadcast(AbilityName);
}

float UAbilityComponent::GetRemainingCooldown(const FName& AbilityName) const
{
return 0.0f;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "AbilityTypes.h"
#include "AbilityComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAbilityTriggered, const FName&, AbilityName);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCooldownExpired, const FName&);

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class TESTGAME_API UAbilityComponent : public UActorComponent
{
GENERATED_BODY()

public:
UFUNCTION(BlueprintCallable, Category = "Abilities")
void TriggerAbility(const FName& AbilityName);

UFUNCTION(BlueprintPure, Category = "Abilities")
float GetRemainingCooldown(const FName& AbilityName) const;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Abilities")
TArray<FAbilityInfo> Abilities;

UPROPERTY(VisibleAnywhere, Category = "Abilities")
TMap<FName, float> ActiveCooldowns;

UPROPERTY(BlueprintAssignable, Category = "Abilities")
FOnAbilityTriggered OnAbilityTriggered;

UPROPERTY(EditAnywhere, Category = "Abilities")
EAbilityState State = EAbilityState::Idle;

UPROPERTY(EditAnywhere, Category = "Abilities")
TSet<FName> UnlockedAbilities;

// Non-dynamic delegates are not reflected and cannot be UPROPERTYs.
FOnCooldownExpired OnCooldownExpired;
};
Loading
Loading