Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: eclipse-cdt/cdt-lsp
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: CDT_LSP_1_0_0
Choose a base ref
...
head repository: eclipse-cdt/cdt-lsp
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: CDT_LSP_1_1_0
Choose a head ref

Commits on Sep 18, 2023

  1. Version bumps for CDT LSP 1.1.0 (#211)

    Part of #210
    jonahgraham authored Sep 18, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    80d3ea4 View commit details

Commits on Sep 19, 2023

  1. Copy the full SHA
    28a9c65 View commit details

Commits on Oct 9, 2023

  1. Copy the full SHA
    e888082 View commit details

Commits on Oct 11, 2023

  1. Use restart() method from LanguageServerWrapper

    Since the LanguageServerWrapper provides a restart method we should use
    this method instead of stop(). See
    eclipse-lsp4e/lsp4e#839
    ghentschke committed Oct 11, 2023
    Copy the full SHA
    3b9393b View commit details

Commits on Oct 25, 2023

  1. Copy the full SHA
    a41e165 View commit details

Commits on Jan 8, 2024

  1. Adding pull-requests right

    Adding pull-requests right according to https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/issues/3996
    ghentschke committed Jan 8, 2024
    Copy the full SHA
    ad8247d View commit details

Commits on Jan 11, 2024

  1. [#224] remove obsolete tm4e Cpp content type binding

    Since this eclipse-tm4e/tm4e#500 lsp4e issue has been
    closed by this platform fix:
    eclipse-platform/eclipse.platform#151 we can
    now remove the contentTypeBinding to the lsp4e C++ content type
    definition.
    
    fixes #224
    ghentschke committed Jan 11, 2024
    Copy the full SHA
    84fd92c View commit details

Commits on Jan 17, 2024

  1. Copy the full SHA
    675c87f View commit details

Commits on Jan 26, 2024

  1. [#227] Refactor ClangdConfigurationManager

    - Allows vendors to overwrite default behavior
    
    fixes #227
    ghentschke committed Jan 26, 2024
    Copy the full SHA
    67c4d24 View commit details
  2. [#227] Refactor ClangdConfigurationManager

    - Fix unit tests
    
    fixes #227
    ghentschke committed Jan 26, 2024
    Copy the full SHA
    dc40859 View commit details
  3. [#227] Refactor ClangdConfigurationManager

    - Support cmake projects as well
    
    fixes #227
    ghentschke committed Jan 26, 2024
    Copy the full SHA
    9aba9db View commit details
  4. [#227] Refactor ClangdConfigurationManager

    - Add org.eclipse.cdt.cmake.core to target definition
    
    fixes #227
    ghentschke committed Jan 26, 2024
    Copy the full SHA
    0a9ec78 View commit details
  5. [#227] Refactor ClangdConfigurationManager

    - Fix bundle activator/Service Component Runtime (SCR) problem: The
    Bundle Activator's start method must not rely upon SCR having activated
    any of the bundle's components. However, the components can rely upon
    the Bundle Activator's start method having been called. That is, there
    is a happens-before relationship between the Bundle Activator's start
    method being run and the components being activated.
    
    fixes #227
    ghentschke committed Jan 26, 2024
    Copy the full SHA
    03354bf View commit details
  6. [#227] Refactor ClangdConfigurationManager

    - make methods protected since they aren't part of the interface
    anymore.
    
    fixes #227
    ghentschke committed Jan 26, 2024
    Copy the full SHA
    2fe1c1d View commit details

Commits on Feb 6, 2024

  1. [#245] catch all exceptions during yaml parsing (#246)

    * [#245] catch all exceptions during yaml parsing
    
    - do not set database path in .clangd file with invalid yaml syntax. The
    user has to fix it first.
    ghentschke authored Feb 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    61be8ca View commit details

Commits on Feb 9, 2024

  1. [#247] Add .clangd configuration file syntax checker (#249)

    [#247] Add .clangd configuration file syntax checker
    
    - Inform the user via markers in the .clangd file when the syntax cannot
    be parsed, because this leads to problems in the
    ClangdConfigurationFileManager.
    - Works for UTF-8 characters in text file.
    
    fixes #247
    ghentschke authored Feb 9, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d4bca9e View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    4502b0b View commit details

Commits on Feb 19, 2024

  1. Add spelling support (#244)

    [222] add spell checking support
    
    Basic spelling support for the new LSP based C/C++ Editor.
    
    fixes #222
    ghentschke authored Feb 19, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d5941b9 View commit details
  2. Add clangd extensions textDocument/symbolInfo and textDocument/ast (#256

    )
    travkin79 authored Feb 19, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    51ffc73 View commit details

Commits on Feb 20, 2024

  1. Adapt target platform definition for using release versions (#260)

    travkin79 authored Feb 20, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    4fdaf5c View commit details
  2. Update docs in preparation for 1.1.0 release (#259)

    fixes #258
    ghentschke authored Feb 20, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    8ed9857 View commit details
  3. Improve readme (#263)

    ghentschke authored Feb 20, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    4bdcfab View commit details
  4. Fix markdown syntax error in README (#264)

    ghentschke authored Feb 20, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    de6e9a5 View commit details
Showing with 2,168 additions and 473 deletions.
  1. +3 −1 .github/workflows/licensecheck.yml
  2. +8 −0 CHANGELOG.md
  3. +24 −11 README.md
  4. +6 −3 bundles/org.eclipse.cdt.lsp.clangd/META-INF/MANIFEST.MF
  5. +9 −0 ...org.eclipse.cdt.lsp.clangd/OSGI-INF/org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager.xml
  6. +11 −0 bundles/org.eclipse.cdt.lsp.clangd/plugin.xml
  7. +32 −0 .../org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/clangd/ClangdCProjectDescriptionListener.java
  8. +232 −0 ...les/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/clangd/ClangdConfigurationFileManager.java
  9. +31 −0 bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/clangd/MacroResolver.java
  10. +12 −50 ...les/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/CProjectChangeMonitor.java
  11. +127 −0 ...s/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileChecker.java
  12. +87 −0 ...s/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileMonitor.java
  13. +0 −93 ...rg.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigurationManager.java
  14. +13 −1 ....eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdLanguageServerProvider.java
  15. +11 −1 ...clipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/ClangdConfigurationPage.java
  16. +4 −0 bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/ClangdPlugin.java
  17. +78 −45 ...eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/CompileCommandsMonitor.java
  18. +0 −3 ...rg.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/LspEditorUiMessages.java
  19. +0 −3 ...ipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/LspEditorUiMessages.properties
  20. +0 −5 bundles/org.eclipse.cdt.lsp.test/src/org/eclipse/cdt/lsp/test/LspUtilsTest.java
  21. +7 −3 bundles/org.eclipse.cdt.lsp/META-INF/MANIFEST.MF
  22. +1 −0 bundles/org.eclipse.cdt.lsp/OSGI-INF/l10n/bundle.properties
  23. +9 −0 bundles/org.eclipse.cdt.lsp/OSGI-INF/org.eclipse.cdt.lsp.editor.format.FormatOnSave.xml
  24. +35 −9 bundles/org.eclipse.cdt.lsp/plugin.xml
  25. +1 −35 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/LspUtils.java
  26. +15 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/BuiltinEditorOptionsDefault.java
  27. +15 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorConfigurationPage.java
  28. +26 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorMetadata.java
  29. +27 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorMetadataDefaults.java
  30. +21 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorOptions.java
  31. +3 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorPreferenceInitializer.java
  32. +15 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorPreferredOptions.java
  33. +58 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/SaveActionsConfigurationArea.java
  34. +26 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/SaveActionsConfigurationPage.java
  35. +48 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/format/FormatOnSave.java
  36. +25 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/editor/CSpellingReconcileStrategy.java
  37. +32 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/editor/CSpellingReconciler.java
  38. +58 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/editor/SpellingEnabled.java
  39. +11 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/messages/LspUiMessages.java
  40. +12 −2 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/messages/LspUiMessages.properties
  41. +36 −1 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/services/ClangdLanguageServer.java
  42. +167 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/services/ast/AstNode.java
  43. +100 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/services/ast/AstParams.java
  44. +91 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/services/symbolinfo/RangeAndUri.java
  45. +160 −0 bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/services/symbolinfo/SymbolDetails.java
  46. +1 −1 features/org.eclipse.cdt.lsp.feature/feature.xml
  47. BIN images/editor.png
  48. BIN images/open-with.png
  49. BIN images/preferences.png
  50. BIN images/properties.png
  51. +1 −1 pom.xml
  52. +2 −2 releng/org.eclipse.cdt.lsp.repository/pom.xml
  53. +2 −2 releng/org.eclipse.cdt.lsp.target/org.eclipse.cdt.lsp.target.target
  54. +298 −0 ...cdt.lsp.clangd.tests/src/org/eclipse/cdt/lsp/clangd/tests/ClangdConfigurationFileManagerTest.java
  55. +0 −201 ...pse.cdt.lsp.clangd.tests/src/org/eclipse/cdt/lsp/clangd/tests/ClangdConfigurationManagerTest.java
  56. +28 −0 tests/org.eclipse.cdt.lsp.clangd.tests/src/org/eclipse/cdt/lsp/clangd/tests/MockMacroResolver.java
  57. +149 −0 ...t.lsp.clangd.tests/src/org/eclipse/cdt/lsp/internal/clangd/tests/ClangdConfigFileCheckerTest.java
4 changes: 3 additions & 1 deletion .github/workflows/licensecheck.yml
Original file line number Diff line number Diff line change
@@ -18,4 +18,6 @@ jobs:
with:
projectId: tools.cdt
secrets:
gitlabAPIToken: ${{ secrets.GITLAB_API_TOKEN }}
gitlabAPIToken: ${{ secrets.GITLAB_API_TOKEN }}
permissions:
pull-requests: write
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -5,3 +5,11 @@
- First release of CDT LSP

Fixed issues: <https://github.com/eclipse-cdt/cdt-lsp/milestone/1?closed=1>

### v1.1.0 (Feb 2024)

- Added basic spelling support
- Added .clangd configuration file syntax checker
- Added basic clangd LSP extension support for textDocument/ast and textDocument/symbolInfo

Fixed issues: <https://github.com/eclipse-cdt/cdt-lsp/milestone/2?closed=1>
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Eclipse CDT LSP - LSP based C/C++ Editor

**Target audience** are Eclipse plugin developers who want to use/develop a LSP based C/C++ Editor.
**Target audience** CDT users who want to use a language server based C/C++ Editor which supports newer C/C++ standards and Eclipse plugin developers who want to use/develop a LSP based C/C++ Editor. The editor in this Eclipse feature is backed by the [LLVM clangd C/C++ language server](https://clangd.llvm.org/).

This plugin is based on the [LSP4E](https://github.com/eclipse/lsp4e) and [TM4E](https://github.com/eclipse/tm4e) Eclipse projects. The editor is based on the [`ExtensionBasedTextEditor`](https://github.com/eclipse-platform/eclipse.platform.ui/blob/master/bundles/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ExtensionBasedTextEditor.java#L55-L56) in Eclipse.

@@ -52,7 +52,7 @@ int main() {
-->

The Editors features depends on the support on client ([LSP4E](https://github.com/eclipse/lsp4e)) and server ([clangd](https://clangd.llvm.org/)) side.
Currently these feature are supported (clangd 15 and 16) and current LSP4E:
Currently these feature are supported (clangd 17) and current LSP4E:

- Auto completion
- Hovering
@@ -70,8 +70,11 @@ Not supported (yet):

### Activating LSP based C/C++ Editor

The `org.eclipse.cdt.lsp.clangd` plugin provides an activation UI for the LSP based C/C++ Editor on project and workspace level.
The clangd language server path and the arguments can be changed in the workspace preferences as well:
The `org.eclipse.cdt.lsp` plugin provides an activation UI for the LSP based C/C++ Editor on project and workspace level.

![image](images/editor.png "editor.png")

The clangd path and the arguments can be changed in the workspace preferences as well:

![image](images/preferences.png "preferences.png")

@@ -108,7 +111,17 @@ The following tools are needed on the `PATH` to operate the demo.

### Import an existing project

You can import an existing project that contains a `compile_commands.json` file, or follow these instructions to create a simple starting project.
You can import an existing project that contains a `compile_commands.json` file, or follow these instructions to create a simple starting project.
The language server (clangd) searches for a `compile_commands.json` file in the source file folder and its parents. Users can define a `.clangd` file in the project root to configure clangd (e.g. add include paths).
A [.clangd](https://clangd.llvm.org/config#files) is a text file with YAML syntax. A `compile_commands.json` file can be generated by CMake.

> [!TIP]
> This configuration entry in the `.clangd` file would tell clangd to use the `compile_commands.json` file in the build/default folder:
```yaml
CompileFlags:
CompilationDatabase: build/default
```
### Create an example CMake project
@@ -126,14 +139,13 @@ This file may be hidden by default, therefore to see the file uncheck the *.\* r
### Open a file

By default C/C++ will be opened with the standard CEditor.
The default can be changed per project or per workspace with the *C/C++ General* -> *Editor (LSP)* -> *Prefer C/C++ Editor (LSP)* checkbox in the project setting or preferences.

- Note: The workspace setting will be used for projects that have not checked the *Enable project specific settings* checkbox in the project properties -> *C/C++ General* -> *Editor (LSP)* page.

Alternatively, you can choose which editor to open the file by using *Open With*:
The default can be changed per project or per workspace with the *C/C++ General* -> *Editor (LSP)* -> *Set C/C++ Editor (LSP) as default* checkbox in the project properties or workspace preference page.

![open-with.png](images/open-with.png "open-with.png")
> [!TIP]
> The workspace setting will be used for projects that have not checked the *Enable project specific settings* checkbox in the project properties -> *C/C++ General* -> *Editor (LSP)* page.

> [!IMPORTANT]
> Opening a C/C++ file using *Open With* in the context menu of a file won't work for the LSP based editor, because the language server won't be started if *Set C/C++ Editor (LSP) as default* is not enabled!

With the *C/C++ Editor (LSP)* open, the presentation of the C++ file will follow the LSP4E conventions augmented by the information returned from clangd.

@@ -148,6 +160,7 @@ For plug-in dependencies the MANIFEST.MF's dependency information will provide t
| CDT LSP Version | clangd | cmake* | Eclipse IDE Release |
|:-:|:-:|:-:|:-:|
| 1.0.x | 15.0.x | 3.x | 2023-09 |
| 1.1.x | 17.0.x | 3.x | 2023-12 |

\* cmake is required to run through the demo flow, but any tool that can create compile_commands.json or otherwise feed settings to clangd is suitable.

9 changes: 6 additions & 3 deletions bundles/org.eclipse.cdt.lsp.clangd/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
@@ -24,14 +24,17 @@ Require-Bundle: org.eclipse.cdt.lsp;bundle-version="0.0.0",
org.eclipse.core.runtime;bundle-version="0.0.0",
org.eclipse.jface;bundle-version="0.0.0",
org.eclipse.jface.text;bundle-version="0.0.0",
org.eclipse.lsp4e;bundle-version="0.16.1",
org.eclipse.lsp4e;bundle-version="0.18.0",
org.eclipse.lsp4j;bundle-version="0.0.0",
org.eclipse.ui.editors;bundle-version="0.0.0",
org.eclipse.ui.ide;bundle-version="0.0.0",
org.eclipse.ui.workbench;bundle-version="0.0.0",
org.eclipse.ui.workbench.texteditor;bundle-version="0.0.0"
Bundle-Activator: org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin
org.eclipse.ui.workbench.texteditor;bundle-version="0.0.0",
org.eclipse.core.variables;bundle-version="0.0.0",
org.yaml.snakeyaml;bundle-version="0.0.0"
Service-Component: OSGI-INF/org.eclipse.cdt.lsp.clangd.BuiltinClangdOptionsDefaults.xml,
OSGI-INF/org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager.xml,
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdConfigurationAccess.xml,
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdFallbackManager.xml,
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdMetadataDefaults.xml
Bundle-Activator: org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager">
<property name="service.ranking" type="Integer" value="0"/>
<service>
<provide interface="org.eclipse.cdt.lsp.clangd.ClangdCProjectDescriptionListener"/>
</service>
<reference cardinality="1..1" field="build" interface="org.eclipse.cdt.core.build.ICBuildConfigurationManager" name="build"/>
<implementation class="org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager"/>
</scr:component>
11 changes: 11 additions & 0 deletions bundles/org.eclipse.cdt.lsp.clangd/plugin.xml
Original file line number Diff line number Diff line change
@@ -110,5 +110,16 @@
</command>
</menuContribution>
</extension>
<extension
id="org.eclipse.cdt.lsp.clangd.config.marker"
name=".clangd yaml Problem"
point="org.eclipse.core.resources.markers">
<super
type="org.eclipse.core.resources.problemmarker">
</super>
<persistent
value="true">
</persistent>
</extension>

</plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.clangd;

import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;

/**
* Vendors may implement this interface as OSGi service
* with a service.ranking property > 0 to implement custom behavior
* and to replace the {@link ClangdConfigurationFileManager}
*/
public interface ClangdCProjectDescriptionListener {

/**
* Called when the configuration of a CDT C/C++ project changes.
* @param event
* @param macroResolver helper class to resolve macros in builder CWD
*/
void handleEvent(CProjectDescriptionEvent event, MacroResolver macroResolver);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.clangd;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Optional;

import org.eclipse.cdt.core.build.CBuildConfiguration;
import org.eclipse.cdt.core.build.ICBuildConfiguration;
import org.eclipse.cdt.core.build.ICBuildConfigurationManager;
import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.lsp.LspPlugin;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.scanner.ScannerException;

/**
* Default implementation of the {@link ClangdCProjectDescriptionListener}.
* Can be extended by vendors if needed. This implementation sets the path to
* the compile_commands.json in the .clangd file in the projects root directory.
* This is needed by CDT projects since the compile_commands.json is generated in the build folder.
* When the active build configuration changes in managed build projects, this manager updates the path to the database in
* the .clangd file to ensure that clangd uses the compile_commads.json of the active build configuration.
*
* This class can be extended by vendors.
*/
@Component(property = { "service.ranking:Integer=0" })
public class ClangdConfigurationFileManager implements ClangdCProjectDescriptionListener {
public static final String CLANGD_CONFIG_FILE_NAME = ".clangd"; //$NON-NLS-1$
private static final String COMPILE_FLAGS = "CompileFlags"; //$NON-NLS-1$
private static final String COMPILATTION_DATABASE = "CompilationDatabase"; //$NON-NLS-1$
private static final String SET_COMPILATION_DB = COMPILE_FLAGS + ": {" + COMPILATTION_DATABASE + ": %s}"; //$NON-NLS-1$ //$NON-NLS-2$
private static final String EMPTY = ""; //$NON-NLS-1$

@Reference
private ICBuildConfigurationManager build;

@Override
public void handleEvent(CProjectDescriptionEvent event, MacroResolver macroResolver) {
setCompilationDatabasePath(event.getProject(), event.getNewCProjectDescription(), macroResolver);
}

/**
* Set the <code>CompilationDatabase</code> entry in the <code>.clangd</code> file which is located in the <code>project</code> root,
* if the yaml file syntax can be parsed.
* The <code>.clangd</code> file will be created, if it's not existing.
* The <code>CompilationDatabase</code> points to the build folder of the active build configuration
* (in case <code>project</code> is a managed C/C++ project).
*
* In the following example clangd uses the compile_commands.json file in the Debug folder:
* <pre>CompileFlags: {CompilationDatabase: Debug}</pre>
*
* @param project C/C++ project
* @param newCProjectDescription new CProject description
* @param macroResolver helper to resolve macros in the CWD path of the builder
*/
protected void setCompilationDatabasePath(IProject project, ICProjectDescription newCProjectDescription,
MacroResolver macroResolver) {
if (project != null && newCProjectDescription != null) {
if (enableSetCompilationDatabasePath(project)) {
var relativeDatabasePath = getRelativeDatabasePath(project, newCProjectDescription, macroResolver);
if (!relativeDatabasePath.isEmpty()) {
setCompilationDatabase(project, relativeDatabasePath);
} else {
Platform.getLog(getClass()).error("Cannot determine path to compile_commands.json"); //$NON-NLS-1$
}
}
}
}

/**
* Enabler for {@link setCompilationDatabasePath}. Can be overriden for customization.
* @param project
* @return true if the database path should be written to .clangd file in the project root.
*/
protected boolean enableSetCompilationDatabasePath(IProject project) {
return Optional.ofNullable(LspPlugin.getDefault()).map(LspPlugin::getCLanguageServerProvider)
.map(provider -> provider.isEnabledFor(project)).orElse(Boolean.FALSE);
}

/**
* Get project relative path to compile_commands.json file.
* By de
* @param project
* @param newCProjectDescription
* @param macroResolver
* @return project relative path to active build folder or empty String
*/
private String getRelativeDatabasePath(IProject project, ICProjectDescription newCProjectDescription,
MacroResolver macroResolver) {
if (project != null && newCProjectDescription != null) {
ICConfigurationDescription config = newCProjectDescription.getDefaultSettingConfiguration();
var cwdBuilder = config.getBuildSetting().getBuilderCWD();
var projectLocation = project.getLocation().addTrailingSeparator().toOSString();
if (cwdBuilder != null) {
try {
var cwdString = macroResolver.resolveValue(cwdBuilder.toOSString(), EMPTY, null, config);
return cwdString.replace(projectLocation, EMPTY);
} catch (CdtVariableException e) {
Platform.getLog(getClass()).log(e.getStatus());
}
} else {
//it is probably a cmake project:
return buildConfiguration(project)//
.filter(CBuildConfiguration.class::isInstance)//
.map(bc -> {
try {
return ((CBuildConfiguration) bc).getBuildContainer();
} catch (CoreException e) {
Platform.getLog(getClass()).log(e.getStatus());
}
return null;
})//
.map(c -> c.getLocation())//
.map(l -> l.toOSString().replace(projectLocation, EMPTY)).orElse(EMPTY);
}
}
return EMPTY;
}

private Optional<ICBuildConfiguration> buildConfiguration(IResource initial) {
try {
var active = initial.getProject().getActiveBuildConfig();
if (active != null && build != null) {
return Optional.ofNullable(build.getBuildConfiguration(active));
}
} catch (CoreException e) {
Platform.getLog(getClass()).error(e.getMessage(), e);
}
return Optional.empty();
}

/**
* Set the <code>CompilationDatabase</code> entry in the .clangd file in the given project root.
* The file will be created, if it's not existing.
* A ScannerException will be thrown if the configuration file contains invalid yaml syntax.
*
* @param project to write the .clangd file
* @param databasePath project relative path to .clangd file
* @throws IOException
* @throws ScannerException
* @throws CoreException
*/
@SuppressWarnings("unchecked")
public void setCompilationDatabase(IProject project, String databasePath) {
var configFile = project.getFile(CLANGD_CONFIG_FILE_NAME);
try {
if (createClangdConfigFile(configFile, project.getDefaultCharset(), databasePath, false)) {
return;
}
Map<String, Object> data = null;
Yaml yaml = new Yaml();
try (var inputStream = configFile.getContents()) {
//throws ScannerException and ParserException:
try {
data = yaml.load(inputStream);
} catch (Exception e) {
Platform.getLog(getClass()).error(e.getMessage(), e);
// return, since the file syntax is corrupted. The user has to fix it first:
return;
}
}
if (data == null) {
//empty file: (re)create .clangd file:
createClangdConfigFile(configFile, project.getDefaultCharset(), databasePath, true);
return;
}
Map<String, Object> map = (Map<String, Object>) data.get(COMPILE_FLAGS);
if (map != null) {
var cdb = map.get(COMPILATTION_DATABASE);
if (cdb != null && cdb instanceof String) {
if (cdb.equals(databasePath)) {
return;
}
}
map.put(COMPILATTION_DATABASE, databasePath);
data.put(COMPILE_FLAGS, map);
try (var yamlWriter = new PrintWriter(configFile.getLocation().toFile())) {
yaml.dump(data, yamlWriter);
}
}
} catch (CoreException e) {
Platform.getLog(getClass()).log(e.getStatus());
} catch (IOException e) {
Platform.getLog(getClass()).error(e.getMessage(), e);
}
}

private boolean createClangdConfigFile(IFile configFile, String charset, String databasePath,
boolean overwriteContent) {
if (!configFile.exists() || overwriteContent) {
try (final var data = new ByteArrayInputStream(
String.format(SET_COMPILATION_DB, databasePath).getBytes(charset))) {
if (overwriteContent) {
configFile.setContents(data, IResource.KEEP_HISTORY, new NullProgressMonitor());
} else {
configFile.create(data, false, new NullProgressMonitor());
}
return true;
} catch (CoreException e) {
Platform.getLog(getClass()).log(e.getStatus());
} catch (IOException e) {
Platform.getLog(getClass()).error(e.getMessage(), e);
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.clangd;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;

/**
* Helper class to resolve macros in builder CWD
*/
public class MacroResolver {

public String resolveValue(String value, String nonexistentMacrosValue, String listDelimiter,
ICConfigurationDescription cfg) throws CdtVariableException {
return CCorePlugin.getDefault().getCdtVariableManager().resolveValue(value, nonexistentMacrosValue,
listDelimiter, cfg);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 Bachmann electronic GmbH and others.
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -9,69 +9,31 @@
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
* Alexander Fedorov (ArSysOp) - use Platform for logging
*******************************************************************************/

package org.eclipse.cdt.lsp.internal.clangd;

import java.io.IOException;
import java.util.Optional;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.core.settings.model.ICProjectDescriptionListener;
import org.eclipse.cdt.lsp.LspPlugin;
import org.eclipse.cdt.lsp.LspUtils;
import org.eclipse.cdt.lsp.internal.clangd.editor.LspEditorUiMessages;
import org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.yaml.snakeyaml.scanner.ScannerException;
import org.eclipse.cdt.lsp.clangd.ClangdCProjectDescriptionListener;
import org.eclipse.cdt.lsp.clangd.MacroResolver;
import org.eclipse.core.runtime.ServiceCaller;

/**
* This monitor listens to C project description changes.
*/
public class CProjectChangeMonitor {
MacroResolver macroResolver = new MacroResolver();

private final ServiceCaller<ClangdCProjectDescriptionListener> clangdListener = new ServiceCaller<>(getClass(),
ClangdCProjectDescriptionListener.class);

private final ICProjectDescriptionListener listener = new ICProjectDescriptionListener() {

@Override
public void handleEvent(CProjectDescriptionEvent event) {
ICProjectDescription newCProjectDecription = event.getNewCProjectDescription();
if (newCProjectDecription != null) {
IProject project = event.getProject();
boolean isEnabled = Optional.ofNullable(LspPlugin.getDefault())
.map(LspPlugin::getCLanguageServerProvider).map(provider -> provider.isEnabledFor(project))
.orElse(Boolean.FALSE);
if (project != null && isEnabled) {
ICConfigurationDescription newConfig = newCProjectDecription.getDefaultSettingConfiguration();
var cwdBuilder = newConfig.getBuildSetting().getBuilderCWD();
if (cwdBuilder != null) {
try {
var cwdString = CCorePlugin.getDefault().getCdtVariableManager()
.resolveValue(cwdBuilder.toOSString(), "", null, newConfig);
var projectLocation = project.getLocation().addTrailingSeparator().toOSString();
var databasePath = cwdString.replace(projectLocation, "");
try {
ClangdConfigurationManager.setCompilationDatabase(project, databasePath);
} catch (ScannerException e) {
var status = new Status(IStatus.ERROR, ClangdPlugin.PLUGIN_ID, e.getMessage());
var configFile = ClangdConfigurationManager.CLANGD_CONFIG_FILE_NAME;
LspUtils.showErrorMessage(LspEditorUiMessages.CProjectChangeMonitor_yaml_scanner_error,
LspEditorUiMessages.CProjectChangeMonitor_yaml_scanner_error_message
+ projectLocation + configFile,
status);
}
} catch (CoreException e) {
Platform.getLog(getClass()).log(e.getStatus());
} catch (IOException e) {
Platform.getLog(getClass()).error(e.getMessage(), e);
}
}
}
}
clangdListener.call(c -> c.handleEvent(event, macroResolver));
}

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.internal.clangd;

import java.io.IOException;

import org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Platform;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.MarkedYAMLException;

/**
* Checks the <code>.clangd</code> file for syntax errors and notifies the user via error markers in the file and Problems view.
*/
public class ClangdConfigFileChecker {
public static final String CLANGD_MARKER = ClangdPlugin.PLUGIN_ID + ".config.marker"; //$NON-NLS-1$

/**
* Checks if the .clangd file contains valid yaml syntax. Adds error marker to the file if not.
* @param configFile
*/
public void checkConfigFile(IFile configFile) {
if (!configFile.exists()) {
return;
}
Yaml yaml = new Yaml();
try (var inputStream = configFile.getContents()) {
try {
removeMarkerFromClangdConfig(configFile);
//throws ScannerException and ParserException:
yaml.load(inputStream);
} catch (MarkedYAMLException yamlException) {
// re-read the file, because the buffer which comes along with MarkedYAMLException is limited to ~800 bytes.
try (var reReadStream = configFile.getContents()) {
addMarkerToClangdConfig(configFile, yamlException, reReadStream.readAllBytes());
}
} catch (Exception exception) {
//log unexpected exception:
Platform.getLog(getClass()).error("Expected MarkedYAMLException, but was: " + exception.getMessage(), //$NON-NLS-1$
exception);
}
} catch (IOException | CoreException e) {
Platform.getLog(getClass()).error(e.getMessage(), e);
}
}

private void addMarkerToClangdConfig(IFile configFile, MarkedYAMLException yamlException, byte[] buffer) {
try {
var configMarker = parseYamlException(yamlException, buffer);
var marker = configFile.createMarker(CLANGD_MARKER);
marker.setAttribute(IMarker.MESSAGE, configMarker.message);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
marker.setAttribute(IMarker.LINE_NUMBER, configMarker.line);
marker.setAttribute(IMarker.CHAR_START, configMarker.charStart);
marker.setAttribute(IMarker.CHAR_END, configMarker.charEnd);
} catch (CoreException core) {
Platform.getLog(getClass()).log(core.getStatus());
}
}

private void removeMarkerFromClangdConfig(IFile configFile) {
try {
configFile.deleteMarkers(CLANGD_MARKER, false, IResource.DEPTH_ZERO);
} catch (CoreException e) {
Platform.getLog(getClass()).log(e.getStatus());
}
}

private class ClangdConfigMarker {
public String message;
public int line = 1;
public int charStart = -1;
public int charEnd = -1;
}

/**
* Fetch line and char position information from exception to create a marker for the .clangd file.
* @param exception
* @param file
* @return
*/
private ClangdConfigMarker parseYamlException(MarkedYAMLException exception, byte[] buffer) {
var marker = new ClangdConfigMarker();
var context = exception.getContext();
marker.message = context != null ? context + " " + exception.getProblem() : exception.getProblem(); //$NON-NLS-1$
var problemMark = exception.getProblemMark();
if (problemMark == null) {
return marker;
}
marker.line = problemMark.getLine() + 1; //getLine() is zero based, IMarker wants 1-based
int index = problemMark.getIndex();
if (index == buffer.length) {
// When index == buffer.length() the marker index points to the non visible
// \r or \n character and the marker is not displayed in the editor.
// Or, even worse, there is no next line and index + 1 would be > buffer.length
// Therefore we have to find the last visible char:
index = getIndexOfLastVisibleChar(buffer);
}
marker.charStart = index;
marker.charEnd = index + 1;
return marker;
}

private int getIndexOfLastVisibleChar(byte[] buffer) {
for (int i = buffer.length - 1; i >= 0; i--) {
if ('\r' != ((char) buffer[i]) && '\n' != ((char) buffer[i])) {
return i;
}
}
return Math.max(0, buffer.length - 2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.internal.clangd;

import java.util.concurrent.ConcurrentLinkedQueue;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

/**
* Monitor changes in <code>.clangd</code> files in the workspace and triggers a yaml checker
* to add error markers to the <code>.clangd</code> file when the edits causes yaml loader failures.
*/
public class ClangdConfigFileMonitor {
private static final String CLANGD_CONFIG_FILE = ".clangd"; //$NON-NLS-1$
private final ConcurrentLinkedQueue<IFile> pendingFiles = new ConcurrentLinkedQueue<>();
private final IWorkspace workspace;
private final ClangdConfigFileChecker checker = new ClangdConfigFileChecker();

private final IResourceChangeListener listener = new IResourceChangeListener() {
@Override
public void resourceChanged(IResourceChangeEvent event) {
if (event.getDelta() != null && event.getType() == IResourceChangeEvent.POST_CHANGE) {
try {
event.getDelta().accept(delta -> {
if ((delta.getKind() == IResourceDelta.ADDED
|| (delta.getFlags() & IResourceDelta.CONTENT) != 0)
&& CLANGD_CONFIG_FILE.equals(delta.getResource().getName())) {
if (delta.getResource() instanceof IFile file) {
pendingFiles.add(file);
checkJob.schedule(100);
}
}
return true;
});
} catch (CoreException e) {
Platform.getLog(getClass()).log(e.getStatus());
}
}
}
};

public ClangdConfigFileMonitor(IWorkspace workspace) {
this.workspace = workspace;
}

private final WorkspaceJob checkJob = new WorkspaceJob("Check .clangd file") { //$NON-NLS-1$

@Override
public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
while (pendingFiles.peek() != null) {
checker.checkConfigFile(pendingFiles.poll());
}
return Status.OK_STATUS;
}

};

public ClangdConfigFileMonitor start() {
workspace.addResourceChangeListener(listener);
return this;
}

public void stop() {
workspace.removeResourceChangeListener(listener);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -19,14 +19,17 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.eclipse.cdt.lsp.clangd.ClangdConfiguration;
import org.eclipse.cdt.lsp.clangd.ClangdFallbackFlags;
import org.eclipse.cdt.lsp.editor.Configuration;
import org.eclipse.cdt.lsp.editor.LanguageServerEnable;
import org.eclipse.cdt.lsp.server.ICLanguageServerProvider;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ServiceCaller;
import org.eclipse.core.variables.VariablesPlugin;

public final class ClangdLanguageServerProvider implements ICLanguageServerProvider {

@@ -47,10 +50,19 @@ public Object getInitializationOptions(URI rootUri) {
@Override
public List<String> getCommands(URI rootUri) {
List<String> result = new ArrayList<>();
configuration.call(c -> result.addAll(c.commands(rootUri)));
configuration.call(c -> result.addAll(c.commands(rootUri).stream()
.map(ClangdLanguageServerProvider::resolveVariables).collect(Collectors.toList())));
return result;
}

private static String resolveVariables(String cmd) {
try {
return VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(cmd);
} catch (CoreException e) {
return cmd;
}
}

@Override
public boolean isEnabledFor(IProject project) {
boolean[] enabled = new boolean[1];
Original file line number Diff line number Diff line change
@@ -14,6 +14,8 @@

package org.eclipse.cdt.lsp.internal.clangd.editor;

import java.io.IOException;

import org.eclipse.cdt.lsp.LspUtils;
import org.eclipse.cdt.lsp.clangd.ClangdConfiguration;
import org.eclipse.cdt.lsp.clangd.ClangdMetadata;
@@ -22,11 +24,13 @@
import org.eclipse.cdt.lsp.editor.ConfigurationArea;
import org.eclipse.cdt.lsp.editor.EditorConfigurationPage;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.statushandlers.StatusManager;

public final class ClangdConfigurationPage extends EditorConfigurationPage {

@@ -93,7 +97,13 @@ private void openRestartDialog() {
new String[] { IDialogConstants.NO_LABEL, LspEditorUiMessages.LspEditorPreferencePage_restart_button },
1);
if (dialog.open() == 1) {
LspUtils.getLanguageServers().forEach(w -> w.stop());
LspUtils.getLanguageServers().forEach(w -> {
try {
w.restart();
} catch (IOException e) {
StatusManager.getManager().handle(Status.error("Could not restart language servers", e)); //$NON-NLS-1$
}
});
}
}

Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
package org.eclipse.cdt.lsp.internal.clangd.editor;

import org.eclipse.cdt.lsp.internal.clangd.CProjectChangeMonitor;
import org.eclipse.cdt.lsp.internal.clangd.ClangdConfigFileMonitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
@@ -27,6 +28,7 @@ public class ClangdPlugin extends AbstractUIPlugin {
private IWorkspace workspace;
private CompileCommandsMonitor compileCommandsMonitor;
private CProjectChangeMonitor cProjectChangeMonitor;
private ClangdConfigFileMonitor configFileMonitor;

// The plug-in ID
public static final String PLUGIN_ID = "org.eclipse.cdt.lsp.clangd"; //$NON-NLS-1$
@@ -49,13 +51,15 @@ public void start(BundleContext context) throws Exception {
workspace = workspaceTracker.getService();
compileCommandsMonitor = new CompileCommandsMonitor(workspace).start();
cProjectChangeMonitor = new CProjectChangeMonitor().start();
configFileMonitor = new ClangdConfigFileMonitor(workspace).start();
}

@Override
public void stop(BundleContext context) throws Exception {
plugin = null;
compileCommandsMonitor.stop();
cProjectChangeMonitor.stop();
configFileMonitor.stop();
super.stop(context);
}

Original file line number Diff line number Diff line change
@@ -13,9 +13,14 @@

package org.eclipse.cdt.lsp.internal.clangd.editor;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import org.eclipse.cdt.lsp.LspUtils;
@@ -28,45 +33,83 @@
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.core.runtime.Status;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IReusableEditor;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.statushandlers.StatusManager;

/**
* Detects changes (add/delete/content) of JSON Compilation Database Format
* Specification files ({@value #CDBF_SPECIFICATION_JSON_FILE}) in the
* {@link IWorkspace workspace} and
* {@link CompileCommandsMonitor#refreshEditor(IEditorPart) refreshes} open
* editors if their {@link IEditorPart#getEditorInput()} is affected.
* {@link IWorkspace workspace} and {@link #restartLanguageServers() restarts
* the language servers} if any cpp file from the affected projects is open
* in an editor.
*/
public class CompileCommandsMonitor {
private static final String CDBF_SPECIFICATION_JSON_FILE = "compile_commands.json";
private static final String CDBF_SPECIFICATION_JSON_FILE = "compile_commands.json"; //$NON-NLS-1$

private static final long DEBOUNCE_DELAY = 2000; // ms

private final IWorkspace workspace;

/**
* Utility class for postponing the execution of a {@link Runnable} to avoid
* unnecessary or frequent invocation.
*/
private static final class Debouncer {
private long debounceDelay;
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> debounceTimer;

public Debouncer(long debounceDelay) {
this.debounceDelay = debounceDelay;
}

public void run(Runnable runnable) {
if (debounceTimer != null && !debounceTimer.isDone()) {
debounceTimer.cancel(true);
}

debounceTimer = scheduler.schedule(runnable, debounceDelay, TimeUnit.MILLISECONDS);
}

public void start() {
scheduler = Executors.newScheduledThreadPool(1);
}

public void stop() {
scheduler.shutdown();
}
}

private final Debouncer debouncer;

private final IResourceChangeListener listener = new IResourceChangeListener() {
@Override
public void resourceChanged(IResourceChangeEvent event) {
Set<IProject> projects = collectAffectedProjects(event);

if (!projects.isEmpty()) {
// collect all open editors which have cpp files as input and refresh them
Arrays.stream(PlatformUI.getWorkbench().getWorkbenchWindows()).map(IWorkbenchWindow::getPages)
.flatMap(Arrays::stream).map(IWorkbenchPage::getEditorReferences).flatMap(Arrays::stream)
.flatMap(ref -> Stream.ofNullable(ref.getEditor(false))).forEach(editor -> {
IFile file = Adapters.adapt(editor.getEditorInput(), IFile.class);

if (isCppFile(file) && projects.contains(file.getProject())) {
refreshEditor(editor, file);
}
});
Set<IProject> affectedProjects = collectAffectedProjects(event);

if (!affectedProjects.isEmpty()) {
if (getEditors().map(editor -> Adapters.adapt(editor.getEditorInput(), IFile.class))
.anyMatch(file -> isCppFile(file) && affectedProjects.contains(file.getProject()))) {
debouncer.run(() -> restartLanguageServers());
}
}
}

/**
* Returns the editors in workbench without restoring them
*/
private Stream<IEditorPart> getEditors() {
return Arrays.stream(PlatformUI.getWorkbench().getWorkbenchWindows()).map(IWorkbenchWindow::getPages)
.flatMap(Arrays::stream).map(IWorkbenchPage::getEditorReferences).flatMap(Arrays::stream)
.flatMap(ref -> Stream.ofNullable(ref.getEditor(false)));
}

private boolean isCppFile(IResource resource) {
if (resource instanceof IFile) {
var contentTypes = Platform.getContentTypeManager().findContentTypesFor(((IFile) resource).getName());
@@ -103,41 +146,31 @@ private Set<IProject> collectAffectedProjects(IResourceChangeEvent event) {
}
};

private final IWorkspace workspace;

public CompileCommandsMonitor(IWorkspace workspace) {
this.workspace = workspace;
this.debouncer = new Debouncer(DEBOUNCE_DELAY);
}

protected void restartLanguageServers() {
LspUtils.getLanguageServers().forEach(w -> {
try {
w.restart();
} catch (IOException e) {
StatusManager.getManager().handle(
new Status(IStatus.ERROR, ClangdPlugin.PLUGIN_ID, "Could not restart language servers"),
StatusManager.LOG);
}
});
}

public CompileCommandsMonitor start() {
workspace.addResourceChangeListener(listener);
debouncer.start();
return this;
}

public void stop() {
workspace.removeResourceChangeListener(listener);
}

private static void refreshEditor(IEditorPart editor, IFile file) {
ITextViewer textViewer = Adapters.adapt(editor, ITextViewer.class);

// Notify clangd about the file change --> doesn't seem to work
// org.eclipse.lsp4e.LanguageServers.forDocument(textViewer.getDocument()).computeFirst(server -> {
// server.getWorkspaceService()
// .didChangeWatchedFiles(new DidChangeWatchedFilesParams(Arrays.asList(new FileEvent(
// file.getProject().getFile(CDBF_SPECIFICATION_JSON_FILE).getLocationURI().toASCIIString(),
// FileChangeType.Changed))));
// return new CompletableFuture<>();
// });

// Refresh the editors after 5 seconds -> see https://reviews.llvm.org/D92663
UIJob.create("Refresh Editors", monitor -> {
if (textViewer.getDocument() == null)
return;
int rangeOffset = textViewer.getTopIndexStartOffset();
int rangeLength = textViewer.getBottomIndexEndOffset() - rangeOffset;
editor.getSite().getPage().reuseEditor((IReusableEditor) editor, editor.getEditorInput());
textViewer.revealRange(rangeOffset, rangeLength);
}).schedule(5000);
debouncer.stop();
}
}
Original file line number Diff line number Diff line change
@@ -46,7 +46,4 @@ public class LspEditorUiMessages extends NLS {
public static String LspEditorPreferencePage_completion_default;
public static String LspEditorPreferencePage_select_clangd_executable;

public static String CProjectChangeMonitor_yaml_scanner_error;
public static String CProjectChangeMonitor_yaml_scanner_error_message;

}
Original file line number Diff line number Diff line change
@@ -33,6 +33,3 @@ LspEditorPreferencePage_completion_detailed=Detailed
LspEditorPreferencePage_completion_bundled=Bundled
LspEditorPreferencePage_completion_default=Default
LspEditorPreferencePage_select_clangd_executable=Select clangd executable

CProjectChangeMonitor_yaml_scanner_error=Yaml Scanner Error
CProjectChangeMonitor_yaml_scanner_error_message=Error while reading:
Original file line number Diff line number Diff line change
@@ -26,11 +26,6 @@ void testIsCContentType_EmptyId() {
assertTrue(!LspUtils.isCContentType(""));
}

@Test
void testIsCContentType_CppContentTypeFromTM4E() {
assertTrue(LspUtils.isCContentType("lng.cpp"));
}

@Test
void testIsCContentType_CONTENT_TYPE_CSOURCE() {
assertTrue(LspUtils.isCContentType(CCorePlugin.CONTENT_TYPE_CSOURCE));
10 changes: 7 additions & 3 deletions bundles/org.eclipse.cdt.lsp/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
@@ -7,12 +7,14 @@ Export-Package: org.eclipse.cdt.lsp,
org.eclipse.cdt.lsp.editor,
org.eclipse.cdt.lsp.server,
org.eclipse.cdt.lsp.server.enable,
org.eclipse.cdt.lsp.services
org.eclipse.cdt.lsp.services,
org.eclipse.cdt.lsp.services.ast,
org.eclipse.cdt.lsp.services.symbolinfo
Bundle-Activator: org.eclipse.cdt.lsp.LspPlugin
Bundle-Vendor: %Bundle-Vendor
Require-Bundle: org.eclipse.ui,
org.eclipse.core.runtime,
org.eclipse.lsp4e;bundle-version="0.17.1",
org.eclipse.lsp4e;bundle-version="0.17.3",
org.eclipse.tm4e.ui,
org.eclipse.tm4e.languageconfiguration,
org.eclipse.ui.genericeditor,
@@ -29,12 +31,14 @@ Require-Bundle: org.eclipse.ui,
org.eclipse.lsp4j,
org.eclipse.lsp4j.jsonrpc,
org.eclipse.cdt.codan.core,
org.eclipse.cdt.debug.ui
org.eclipse.cdt.debug.ui,
org.eclipse.ui.workbench.texteditor
Bundle-RequiredExecutionEnvironment: JavaSE-17
Automatic-Module-Name: org.eclipse.cdt.lsp
Bundle-ActivationPolicy: lazy
Service-Component: OSGI-INF/org.eclipse.cdt.lsp.editor.BuiltinEditorOptionsDefault.xml,
OSGI-INF/org.eclipse.cdt.lsp.editor.DefaultConfigurationVisibility.xml,
OSGI-INF/org.eclipse.cdt.lsp.editor.EditorConfigurationAccess.xml,
OSGI-INF/org.eclipse.cdt.lsp.editor.EditorMetadataDefaults.xml,
OSGI-INF/org.eclipse.cdt.lsp.editor.format.FormatOnSave.xml,
OSGI-INF/org.eclipse.cdt.lsp.internal.InitialFileManager.xml
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ which is available at https://www.eclipse.org/legal/epl-2.0/\n\
\n\
SPDX-License-Identifier: EPL-2.0\n\
SaveActionsPreferencePage.name=Save Actions
EditorPreferencePage.name=Editor (LSP)
CEditor.name=C/C++ Editor (LSP)
Server.name=C/C++ Language Server
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.eclipse.cdt.lsp.editor.format.FormatOnSave">
<property name="serverDefinitionId" type="String" value="org.eclipse.cdt.lsp.server"/>
<service>
<provide interface="org.eclipse.lsp4e.format.IFormatRegionsProvider"/>
</service>
<reference cardinality="1..1" field="configuration" interface="org.eclipse.cdt.lsp.editor.Configuration" name="configuration"/>
<implementation class="org.eclipse.cdt.lsp.editor.format.FormatOnSave"/>
</scr:component>
44 changes: 35 additions & 9 deletions bundles/org.eclipse.cdt.lsp/plugin.xml
Original file line number Diff line number Diff line change
@@ -33,16 +33,7 @@
<contentTypeBinding
contentTypeId="org.eclipse.cdt.core.cxxSource">
</contentTypeBinding>
<contentTypeBinding
contentTypeId="lng.cpp"> <!-- // TODO: The content type definition from TM4E "lng.cpp" can be omitted here if either https://github.com/eclipse-cdt/cdt/pull/310 or
// https://github.com/eclipse/tm4e/pull/500 has been merged. -->
</contentTypeBinding>
</editor>
<editorContentTypeBinding
contentTypeId="lng.cpp"
editorId="org.eclipse.cdt.ui.editor.CEditor"> <!-- // TODO: The content type definition from TM4E "lng.cpp" can be omitted here if either https://github.com/eclipse-cdt/cdt/pull/310 or
// https://github.com/eclipse/tm4e/pull/500 has been merged. -->
</editorContentTypeBinding>
</extension>
<extension
point="org.eclipse.lsp4e.languageServer">
@@ -224,6 +215,13 @@
properties="hasLanguageServer"
type="java.lang.Object">
</propertyTester>
<propertyTester
class="org.eclipse.cdt.lsp.internal.editor.SpellingEnabled"
id="org.eclipse.cdt.lsp.editor.spelling.EnabledTester"
namespace="org.eclipse.cdt.lsp.editor.spelling"
properties="enabled"
type="java.lang.Object">
</propertyTester>
</extension>
<extension
point="org.eclipse.core.expressions.definitions">
@@ -277,6 +275,14 @@
id="org.eclipse.cdt.lsp.editor.preferencePage"
name="%EditorPreferencePage.name">
</page>
<page
name="%SaveActionsPreferencePage.name"
category="org.eclipse.cdt.lsp.editor.preferencePage"
class="org.eclipse.cdt.lsp.editor.SaveActionsConfigurationPage"
id="org.eclipse.cdt.lsp.editor.SaveActionsPreferencePage">
<keywordReference id="org.eclipse.cdt.ui.saveactions"/>
<keywordReference id="org.eclipse.cdt.ui.common"/>
</page>
</extension>
<extension
point="org.eclipse.ui.propertyPages">
@@ -295,6 +301,26 @@
</adapt>
</enabledWhen>
</page>
<page
category="org.eclipse.cdt.lsp.editor.propertyPage"
class="org.eclipse.cdt.lsp.editor.SaveActionsConfigurationPage"
id="org.eclipse.cdt.lsp.editor.SaveActionsPropertyPage"
name="%SaveActionsPreferencePage.name">
<keywordReference id="org.eclipse.cdt.ui.saveactions"/>
<keywordReference id="org.eclipse.cdt.ui.common"/>
</page>
</extension>
<extension
point="org.eclipse.ui.genericeditor.reconcilers">
<reconciler
class="org.eclipse.cdt.lsp.internal.editor.CSpellingReconciler"
contentType="org.eclipse.core.runtime.text">
<enabledWhen>
<test
property="org.eclipse.cdt.lsp.editor.spelling.enabled">
</test>
</enabledWhen>
</reconciler>
</extension>
</plugin>

Original file line number Diff line number Diff line change
@@ -21,22 +21,16 @@
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.ServiceCaller;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.lsp4e.LanguageServerWrapper;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.progress.UIJob;

public class LspUtils {

@@ -47,35 +41,7 @@ public class LspUtils {
* @return {@code true} if C/C++ content type
*/
public static boolean isCContentType(String id) {
// TODO: The content type definition from TM4E "lng.cpp" can be omitted if either https://github.com/eclipse-cdt/cdt/pull/310 or
// https://github.com/eclipse/tm4e/pull/500 has been merged.
return (id.startsWith("org.eclipse.cdt.core.c") && (id.endsWith("Source") || id.endsWith("Header"))) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
|| "lng.cpp".equals(id); //$NON-NLS-1$
}

/**
* Show error dialog to user
* @param title
* @param errorText
* @param status
*/
public static void showErrorMessage(final String title, final String errorText, final Status status) {
UIJob job = new UIJob("LSP Utils") //$NON-NLS-1$
{
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
ErrorDialog.openError(getActiveShell(), title, errorText, status);
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule();
}

private static Shell getActiveShell() {
if (PlatformUI.getWorkbench() != null && PlatformUI.getWorkbench().getActiveWorkbenchWindow() != null)
return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
return null;
return (id.startsWith("org.eclipse.cdt.core.c") && (id.endsWith("Source") || id.endsWith("Header"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}

public static boolean isFileOpenedInLspEditor(URI uri) {
Original file line number Diff line number Diff line change
@@ -22,4 +22,19 @@ public boolean preferLspEditor() {
return false;
}

@Override
public boolean formatOnSave() {
return false;
}

@Override
public boolean formatAllLines() {
return true;
}

@Override
public boolean formatEditedLines() {
return false;
}

}
Original file line number Diff line number Diff line change
@@ -162,10 +162,25 @@ private Control createPreferenceContent(Composite parent, boolean isProjectScope
Composite composite = new Composite(parent, SWT.NONE);
composite.setLayout(GridLayoutFactory.fillDefaults().create());
composite.setFont(parent.getFont());
if (!isProjectScope) {
createSpellingPreferencesLink(composite);
Label line = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
line.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false, 2, 1));
}
area = getConfigurationArea(composite, isProjectScope);
return composite;
}

private Control createSpellingPreferencesLink(Composite parent) {
Link link = new Link(parent, SWT.NONE);
link.setText(LspUiMessages.LspEditorConfigurationPage_spelling_link);
link.addListener(SWT.Selection,
event -> PreferencesUtil.createPreferenceDialogOn(getShell(), event.text, null, null));
link.setToolTipText(LspUiMessages.LspEditorConfigurationPage_spelling_link_tooltip);
link.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
return link;
}

protected void refreshWidgets(Object options) {
setErrorMessage(null);
area.load(options, useProjectSettings() || !projectScope().isPresent());
Original file line number Diff line number Diff line change
@@ -25,4 +25,30 @@ public interface EditorMetadata {
*/
PreferenceMetadata<Boolean> preferLspEditor();

/**
* Returns the metadata for the "Format source code" option, must not return <code>null</code>.
*
* @return the metadata for the "Format source code" option
*
* @see EditorOptions#formatOnSave()
*/
PreferenceMetadata<Boolean> formatOnSave();

/**
* Returns the metadata for the "Format all lines" option, must not return <code>null</code>.
*
* @return the metadata for the "Format all lines" option
*
* @see EditorOptions#formatAllLines()
*/
PreferenceMetadata<Boolean> formatAllLines();

/**
* Returns the metadata for the "Format edited lines" option, must not return <code>null</code>.
*
* @return the metadata for the "Format edited lines" option
*
* @see EditorOptions#formatEditedLines()
*/
PreferenceMetadata<Boolean> formatEditedLines();
}
Original file line number Diff line number Diff line change
@@ -32,4 +32,31 @@ public PreferenceMetadata<Boolean> preferLspEditor() {
LspUiMessages.LspEditorConfigurationPage_preferLspEditor_description);
}

@Override
public PreferenceMetadata<Boolean> formatOnSave() {
return new PreferenceMetadata<>(Boolean.class, //
"format_source", //$NON-NLS-1$
defaults.formatOnSave(), //
LspUiMessages.SaveActionsConfigurationPage_FormatSourceCode,
LspUiMessages.SaveActionsConfigurationPage_FormatSourceCode_description);
}

@Override
public PreferenceMetadata<Boolean> formatAllLines() {
return new PreferenceMetadata<>(Boolean.class, //
"format_all_lines", //$NON-NLS-1$
defaults.formatAllLines(), //
LspUiMessages.SaveActionsConfigurationPage_FormatAllLines,
LspUiMessages.SaveActionsConfigurationPage_FormatAllLines_description);
}

@Override
public PreferenceMetadata<Boolean> formatEditedLines() {
return new PreferenceMetadata<>(Boolean.class, //
"format_edited_lines", //$NON-NLS-1$
defaults.formatEditedLines(), //
LspUiMessages.SaveActionsConfigurationPage_FormatEditedLines,
LspUiMessages.SaveActionsConfigurationPage_FormatEditedLines_description);
}

}
Original file line number Diff line number Diff line change
@@ -21,4 +21,25 @@ public interface EditorOptions {
*/
boolean preferLspEditor();

/**
* Format source code on file save action
*
* @return if source code should be formatted on file save action
*/
boolean formatOnSave();

/**
* Format all lines in source file
*
* @return if all lines should be formatted
*/
boolean formatAllLines();

/**
* Format edited lines only
*
* @return if only edited lines should be formatted
*/
boolean formatEditedLines();

}
Original file line number Diff line number Diff line change
@@ -28,6 +28,9 @@ private void initializeDefaults(Configuration configuration) {
EditorMetadata metadata = (EditorMetadata) configuration.metadata();
String qualifier = configuration.qualifier();
initializeBoolean(metadata.preferLspEditor(), qualifier);
initializeBoolean(metadata.formatOnSave(), qualifier);
initializeBoolean(metadata.formatAllLines(), qualifier);
initializeBoolean(metadata.formatEditedLines(), qualifier);
}

private void initializeBoolean(PreferenceMetadata<Boolean> preference, String qualifier) {
Original file line number Diff line number Diff line change
@@ -34,6 +34,21 @@ public boolean preferLspEditor() {
return booleanValue(metadata.preferLspEditor());
}

@Override
public boolean formatOnSave() {
return booleanValue(metadata.formatOnSave());
}

@Override
public boolean formatAllLines() {
return booleanValue(metadata.formatAllLines());
}

@Override
public boolean formatEditedLines() {
return booleanValue(metadata.formatEditedLines());
}

@Override
public boolean isEnabledFor(IProject project) {
if (enable != null) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.eclipse.cdt.lsp.editor;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.OsgiPreferenceMetadataStore;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;

public class SaveActionsConfigurationArea extends ConfigurationArea {

private final Button format;
private final Button formatAll;
private final Button formatEdited;

public SaveActionsConfigurationArea(Composite parent, EditorMetadata metadata, boolean isProjectScope) {
super(1);
Composite composite = new Composite(parent, SWT.NONE);
composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(columns).create());

this.format = createButton(metadata.formatOnSave(), composite, SWT.CHECK, 0);
this.formatAll = createButton(metadata.formatAllLines(), composite, SWT.RADIO, 15);
this.formatEdited = createButton(metadata.formatEditedLines(), composite, SWT.RADIO, 15);

final SelectionAdapter formatListener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
var selection = format.getSelection();
formatAll.setEnabled(selection);
formatEdited.setEnabled(selection);
}
};
this.format.addSelectionListener(formatListener);
}

@Override
public void load(Object options, boolean enable) {
if (options instanceof EditorOptions editorOptions) {
format.setSelection(editorOptions.formatOnSave());
formatAll.setSelection(editorOptions.formatAllLines());
formatEdited.setSelection(editorOptions.formatEditedLines());
format.setEnabled(enable);
formatAll.setEnabled(enable && format.getSelection());
formatEdited.setEnabled(enable && format.getSelection());
}
}

@Override
public void store(IEclipsePreferences prefs) {
OsgiPreferenceMetadataStore store = new OsgiPreferenceMetadataStore(prefs);
buttons.entrySet().forEach(e -> store.save(e.getValue().getSelection(), e.getKey()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.eclipse.cdt.lsp.editor;

import org.eclipse.swt.widgets.Composite;

public class SaveActionsConfigurationPage extends EditorConfigurationPage {
private final String id = "org.eclipse.cdt.lsp.editor.SaveActionsPreferencePage"; //$NON-NLS-1$

@Override
protected ConfigurationArea getConfigurationArea(Composite composite, boolean isProjectScope) {
return new SaveActionsConfigurationArea(composite, (EditorMetadata) configuration.metadata(), isProjectScope);
}

@Override
protected String getPreferenceId() {
return id;
}

@Override
protected boolean hasProjectSpecificOptions() {
return projectScope()//
.map(p -> p.getNode(configuration.qualifier()))//
.map(n -> n.get(((EditorMetadata) configuration.metadata()).formatAllLines().identifer(), null))//
.isPresent();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*******************************************************************************
* Copyright (c) 2023 Contributors to the Eclipse Foundation.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* See git history
*******************************************************************************/

package org.eclipse.cdt.lsp.editor.format;

import org.eclipse.cdt.lsp.editor.Configuration;
import org.eclipse.cdt.lsp.editor.EditorOptions;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.format.IFormatRegionsProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(property = { "serverDefinitionId:String=org.eclipse.cdt.lsp.server" })
public class FormatOnSave implements IFormatRegionsProvider {

@Reference
private Configuration configuration;

@Override
public IRegion[] getFormattingRegions(IDocument document) {
var file = LSPEclipseUtils.getFile(document);
if (file != null) {
var editorOptions = (EditorOptions) configuration.options(file);
if (editorOptions != null && editorOptions.formatOnSave()) {
if (editorOptions.formatAllLines()) {
return IFormatRegionsProvider.allLines(document);
}
if (editorOptions.formatEditedLines()) {
return IFormatRegionsProvider.calculateEditedLineRegions(document, new NullProgressMonitor());
}
}
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.internal.editor;

import org.eclipse.cdt.internal.ui.text.spelling.CSpellingService;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.ui.texteditor.spelling.SpellingReconcileStrategy;

public final class CSpellingReconcileStrategy extends SpellingReconcileStrategy {

public CSpellingReconcileStrategy(ISourceViewer viewer) {
super(viewer, CSpellingService.getInstance());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.internal.editor;

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.reconciler.Reconciler;
import org.eclipse.jface.text.source.ISourceViewer;

public final class CSpellingReconciler extends Reconciler {

@Override
public void install(ITextViewer textViewer) {
if (textViewer instanceof ISourceViewer sourceViewer) {
this.setReconcilingStrategy(new CSpellingReconcileStrategy(sourceViewer), IDocument.DEFAULT_CONTENT_TYPE);
}
// call super.install AFTER the CSpellingReconcileStrategy has been added to the super class via setReconcilingStrategy call,
// otherwise reconcilerDocumentChanged (which is called during super.install) would not be performed on our CSpellingReconcileStrategy
super.install(textViewer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*******************************************************************************
* Copyright (c) 2024 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
*******************************************************************************/

package org.eclipse.cdt.lsp.internal.editor;

import java.util.Optional;

import org.eclipse.cdt.lsp.LspUtils;
import org.eclipse.core.expressions.PropertyTester;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.texteditor.spelling.SpellingService;

public final class SpellingEnabled extends PropertyTester {
private static final String EMPTY = ""; //$NON-NLS-1$

@Override
public boolean test(Object receiver, String property, Object[] args, Object expectedValue) {
return isSpellingEnabled() && isCContentType(receiver);
}

private static boolean isSpellingEnabled() {
return EditorsUI.getPreferenceStore().getBoolean(SpellingService.PREFERENCE_SPELLING_ENABLED);
}

private static boolean isCContentType(Object receiver) {
if (receiver instanceof TextEditor editor) {
return LspUtils.isCContentType(getContentType(editor.getEditorInput()));
}
return false;
}

private static String getContentType(IEditorInput editorInput) {
if (editorInput instanceof IFileEditorInput fileEditorInput) {
try {
return Optional.ofNullable(fileEditorInput.getFile().getContentDescription())
.map(cd -> cd.getContentType()).map(ct -> ct.getId()).orElse(EMPTY);
} catch (CoreException e) {
// do nothing
}
}
return EMPTY;
}

}
Original file line number Diff line number Diff line change
@@ -24,9 +24,20 @@ public class LspUiMessages extends NLS {
}

public static String NavigatorView_ErrorOnLoad;

public static String LspEditorConfigurationPage_spelling_link;
public static String LspEditorConfigurationPage_spelling_link_tooltip;

public static String LspEditorConfigurationPage_enable_project_specific;
public static String LspEditorConfigurationPage_configure_ws_specific;
public static String LspEditorConfigurationPage_preferLspEditor;
public static String LspEditorConfigurationPage_preferLspEditor_description;

public static String SaveActionsConfigurationPage_FormatSourceCode;
public static String SaveActionsConfigurationPage_FormatSourceCode_description;
public static String SaveActionsConfigurationPage_FormatAllLines;
public static String SaveActionsConfigurationPage_FormatAllLines_description;
public static String SaveActionsConfigurationPage_FormatEditedLines;
public static String SaveActionsConfigurationPage_FormatEditedLines_description;

}
Original file line number Diff line number Diff line change
@@ -13,8 +13,18 @@

NavigatorView_ErrorOnLoad = Loading the symbols encountered an error; see the Error Log for more information

LspEditorConfigurationPage_spelling_link=Spelling preferences are set via <a href="org.eclipse.ui.editors.preferencePages.Spelling">Text Editors Spelling</a>.
LspEditorConfigurationPage_spelling_link_tooltip=Show the shared text editor spelling preferences

LspEditorConfigurationPage_enable_project_specific=Enable project-specific settings
LspEditorConfigurationPage_configure_ws_specific=Configure Workspace Settings...
LspEditorConfigurationPage_preferLspEditor=Prefer C/C++ Editor (LSP)
LspEditorConfigurationPage_preferLspEditor_description=Prefer to use language server based C/C++ Editor instead of classic C/C++ Editor
LspEditorConfigurationPage_preferLspEditor=Set C/C++ Editor (LSP) as default
LspEditorConfigurationPage_preferLspEditor_description=The language server based C/C++ Editor will be used to open C/C++ source files.

SaveActionsConfigurationPage_FormatSourceCode=Format source code
SaveActionsConfigurationPage_FormatSourceCode_description=Formats source code when file is saved
SaveActionsConfigurationPage_FormatAllLines=Format all lines
SaveActionsConfigurationPage_FormatAllLines_description=Formats all source code lines
SaveActionsConfigurationPage_FormatEditedLines=Format edited lines
SaveActionsConfigurationPage_FormatEditedLines_description=Formats edited source code lines only

Original file line number Diff line number Diff line change
@@ -9,24 +9,32 @@
*
* Contributors:
* Dominic Scharfe (COSEDA Technologies GmbH) - initial implementation
* Dietrich Travkin (Solunar GmbH) - extensions for AST and symbol info
*******************************************************************************/
package org.eclipse.cdt.lsp.services;

import java.util.concurrent.CompletableFuture;

import org.eclipse.cdt.lsp.services.ast.AstNode;
import org.eclipse.cdt.lsp.services.ast.AstParams;
import org.eclipse.cdt.lsp.services.symbolinfo.SymbolDetails;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
import org.eclipse.lsp4j.services.LanguageServer;

/**
* Interface extending the {@link LanguageServer} with clangd extensions.
* More details about LSP usage and extension see the
* <a href="https://github.com/eclipse-lsp4j/lsp4j/blob/main/documentation/jsonrpc.md">
* org.eclipse.lsp4j project's documentation</a>.
*
* @see https://clangd.llvm.org/extensions
*/
public interface ClangdLanguageServer extends LanguageServer {

/**
* The switchSourceHeader request is sent from the client to the server to
* The <em>textDocument/switchSourceHeader</em> request is sent from the client to the server to
* <ul>
* <li>get the corresponding header if a source file was provided</li>
* <li>get the source file if a header was provided</li>
@@ -39,4 +47,31 @@ public interface ClangdLanguageServer extends LanguageServer {
*/
@JsonRequest(value = "textDocument/switchSourceHeader")
CompletableFuture<String> switchSourceHeader(TextDocumentIdentifier textDocument);

/**
* The <em>textDocument/ast</em> request is sent from the client to the server in order to get
* details about the program structure (so called abstract syntax tree or AST) in a C++ file.
* The structure can be requested for the whole file or for a certain range.
*
* @param astParameters request parameters containing the document identifier and requested documented range
* @return the abstract syntax tree root node (with child hierarchy) for the requested document and range
*
* @see https://clangd.llvm.org/extensions#ast
*/
@JsonRequest(value = "textDocument/ast")
CompletableFuture<AstNode> getAst(AstParams astParameters);

/**
* The <em>textDocument/symbolInfo</em> request is sent from the client to the server in order to access
* details about the element under the cursor. The response provides details like the element's name,
* its parent container's name, and some clangd-specific element IDs (e.g. the "unified symbol resolution"
* identifier).
*
* @param positionParameters request parameters containing the document identifier and the current cursor position
* @return the details about the symbol on the given position
*
* @see https://clangd.llvm.org/extensions#symbol-info-request
*/
@JsonRequest(value = "textDocument/symbolInfo")
CompletableFuture<SymbolDetails[]> getSymbolInfo(TextDocumentPositionParams positionParameters);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*******************************************************************************
* Copyright (c) 2024 Advantest Europe GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Dietrich Travkin (Solunar GmbH) - Initial implementation
*******************************************************************************/
package org.eclipse.cdt.lsp.services.ast;

import java.util.Arrays;

import org.eclipse.cdt.lsp.services.ClangdLanguageServer;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import org.eclipse.lsp4j.util.Preconditions;
import org.eclipse.lsp4j.util.ToStringBuilder;

/**
* Return type for the <em>textDocument/ast</em> request.
* This class was generated by the <em>org.eclipse.lsp4j.generator</em> bundle
* using xtend (see {@link org.eclipse.lsp4j.generator.JsonRpcData JsonRpcData} and
* the <a href="https://github.com/eclipse-lsp4j/lsp4j/blob/main/documentation/jsonrpc.md">documentation</a>).
*
* @see {@link ClangdLanguageServer#getAst(AstParams)}
*/
public class AstNode {

@NonNull
private String role;

@NonNull
private String kind;

private String detail;

private String arcana;

@NonNull
private Range range;

private AstNode[] children;

public AstNode() {

}

@NonNull
public String getRole() {
return role;
}

public void setRole(@NonNull final String role) {
this.role = Preconditions.<String>checkNotNull(role, "role"); //$NON-NLS-1$
}

@NonNull
public String getKind() {
return this.kind;
}

public void setKind(@NonNull final String kind) {
this.kind = Preconditions.<String>checkNotNull(kind, "kind"); //$NON-NLS-1$
}

public String getDetail() {
return detail;
}

public void setDetail(final String detail) {
this.detail = detail;
}

public String getArcana() {
return arcana;
}

public void setArcana(final String arcana) {
this.arcana = arcana;
}

@NonNull
public Range getRange() {
return range;
}

public void setRange(@NonNull final Range range) {
this.range = Preconditions.<Range>checkNotNull(range, "range"); //$NON-NLS-1$
}

public AstNode[] getChildren() {
return children;
}

public void setChildren(final AstNode[] children) {
this.children = children;
}

@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this);
b.add("role", this.role); //$NON-NLS-1$
b.add("kind", this.kind); //$NON-NLS-1$
b.add("detail", this.detail); //$NON-NLS-1$
b.add("arcana", this.arcana); //$NON-NLS-1$
b.add("range", this.range); //$NON-NLS-1$
b.add("children", this.children); //$NON-NLS-1$
return b.toString();
}

@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AstNode other = (AstNode) obj;
if (this.role == null) {
if (other.role != null)
return false;
} else if (!this.role.equals(other.role))
return false;
if (this.kind == null) {
if (other.kind != null)
return false;
} else if (!this.kind.equals(other.kind))
return false;
if (this.detail == null) {
if (other.detail != null)
return false;
} else if (!this.detail.equals(other.detail))
return false;
if (this.arcana == null) {
if (other.arcana != null)
return false;
} else if (!this.arcana.equals(other.arcana))
return false;
if (this.range == null) {
if (other.range != null)
return false;
} else if (!this.range.equals(other.range))
return false;
if (this.children == null) {
if (other.children != null)
return false;
} else if (!Arrays.deepEquals(this.children, other.children))
return false;
return true;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.role == null) ? 0 : this.role.hashCode());
result = prime * result + ((this.kind == null) ? 0 : this.kind.hashCode());
result = prime * result + ((this.detail == null) ? 0 : this.detail.hashCode());
result = prime * result + ((this.arcana == null) ? 0 : this.arcana.hashCode());
result = prime * result + ((this.range == null) ? 0 : this.range.hashCode());
return prime * result + ((this.children == null) ? 0 : Arrays.deepHashCode(this.children));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright (c) 2024 Advantest Europe GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Dietrich Travkin (Solunar GmbH) - Initial implementation
*******************************************************************************/
package org.eclipse.cdt.lsp.services.ast;

import org.eclipse.cdt.lsp.services.ClangdLanguageServer;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import org.eclipse.lsp4j.util.Preconditions;
import org.eclipse.lsp4j.util.ToStringBuilder;

/**
* Parameter object type for the <em>textDocument/ast</em> request.
* This class was generated by the <em>org.eclipse.lsp4j.generator</em> bundle
* using xtend (see {@link org.eclipse.lsp4j.generator.JsonRpcData JsonRpcData} and
* the <a href="https://github.com/eclipse-lsp4j/lsp4j/blob/main/documentation/jsonrpc.md">documentation</a>).
*
* @see {@link ClangdLanguageServer#getAst(AstParams)}
*/
public class AstParams {

@NonNull
private TextDocumentIdentifier textDocument;

@NonNull
private Range range;

public AstParams() {
}

public AstParams(@NonNull final TextDocumentIdentifier textDocument, @NonNull final Range range) {
this.textDocument = Preconditions.<TextDocumentIdentifier>checkNotNull(textDocument, "textDocument"); //$NON-NLS-1$
this.range = Preconditions.<Range>checkNotNull(range, "range"); //$NON-NLS-1$
}

@NonNull
public TextDocumentIdentifier getTextDocument() {
return this.textDocument;
}

public void setTextDocument(@NonNull final TextDocumentIdentifier textDocument) {
this.textDocument = Preconditions.<TextDocumentIdentifier>checkNotNull(textDocument, "textDocument"); //$NON-NLS-1$
}

@NonNull
public Range getRange() {
return range;
}

public void setRange(@NonNull Range range) {
this.range = Preconditions.<Range>checkNotNull(range, "range"); //$NON-NLS-1$
}

@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this);
b.add("textDocument", getTextDocument()); //$NON-NLS-1$
b.add("range", getRange()); //$NON-NLS-1$
return b.toString();
}

@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AstParams other = (AstParams) obj;
if (this.getTextDocument() == null) {
if (other.getTextDocument() != null)
return false;
} else if (!this.getTextDocument().equals(other.getTextDocument()))
return false;
if (this.range == null) {
if (other.range != null)
return false;
} else if (!this.range.equals(other.range))
return false;
return true;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.getTextDocument() == null) ? 0 : this.getTextDocument().hashCode());
return prime * result + ((this.range == null) ? 0 : this.range.hashCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*******************************************************************************
* Copyright (c) 2024 Advantest Europe GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Dietrich Travkin (Solunar GmbH) - Initial implementation
*******************************************************************************/
package org.eclipse.cdt.lsp.services.symbolinfo;

import org.eclipse.cdt.lsp.services.ClangdLanguageServer;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import org.eclipse.lsp4j.util.Preconditions;
import org.eclipse.lsp4j.util.ToStringBuilder;

/**
* Data type used in {@link SymbolDetails} for the <em>textDocument/symbolInfo</em> request.
* This class was generated by the <em>org.eclipse.lsp4j.generator</em> bundle
* using xtend (see {@link org.eclipse.lsp4j.generator.JsonRpcData JsonRpcData} and
* the <a href="https://github.com/eclipse-lsp4j/lsp4j/blob/main/documentation/jsonrpc.md">documentation</a>).
*
* @see {@link SymbolDetails}
* @see {@link ClangdLanguageServer#getSymbolInfo(org.eclipse.lsp4j.TextDocumentPositionParams)}
*/
public class RangeAndUri {
@NonNull
private Range range;

@NonNull
private String uri;

@NonNull
public Range getRange() {
return this.range;
}

public void setRange(@NonNull final Range range) {
this.range = Preconditions.<Range>checkNotNull(range, "range"); //$NON-NLS-1$
}

@NonNull
public String getUri() {
return this.uri;
}

public void setUri(@NonNull final String uri) {
this.uri = Preconditions.<String>checkNotNull(uri, "uri"); //$NON-NLS-1$
}

@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this);
b.add("range", this.range); //$NON-NLS-1$
b.add("uri", this.uri); //$NON-NLS-1$
return b.toString();
}

@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RangeAndUri other = (RangeAndUri) obj;
if (this.range == null) {
if (other.range != null)
return false;
} else if (!this.range.equals(other.range))
return false;
if (this.uri == null) {
if (other.uri != null)
return false;
} else if (!this.uri.equals(other.uri))
return false;
return true;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.range == null) ? 0 : this.range.hashCode());
return prime * result + ((this.uri == null) ? 0 : this.uri.hashCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*******************************************************************************
* Copyright (c) 2024 Advantest Europe GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Dietrich Travkin (Solunar GmbH) - Initial implementation
*******************************************************************************/
package org.eclipse.cdt.lsp.services.symbolinfo;

import org.eclipse.cdt.lsp.services.ClangdLanguageServer;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import org.eclipse.lsp4j.util.Preconditions;
import org.eclipse.lsp4j.util.ToStringBuilder;

/**
* Return type for the <em>textDocument/symbolInfo</em> request.
* This class was generated by the <em>org.eclipse.lsp4j.generator</em> bundle
* using xtend (see {@link org.eclipse.lsp4j.generator.JsonRpcData JsonRpcData} and
* the <a href="https://github.com/eclipse-lsp4j/lsp4j/blob/main/documentation/jsonrpc.md">documentation</a>).
*
* @see {@link ClangdLanguageServer#getSymbolInfo(org.eclipse.lsp4j.TextDocumentPositionParams)}
*/
public class SymbolDetails {

@NonNull
private String name;

@NonNull
private String containerName;

@NonNull
private String usr;

private String id;

private RangeAndUri declarationRange;

private RangeAndUri definitionRange;

@NonNull
public String getName() {
return this.name;
}

public void setName(@NonNull final String name) {
this.name = Preconditions.<String>checkNotNull(name, "name"); //$NON-NLS-1$
}

@NonNull
public String getContainerName() {
return this.containerName;
}

public void setContainerName(@NonNull final String containerName) {
this.containerName = Preconditions.<String>checkNotNull(containerName, "containerName"); //$NON-NLS-1$
}

@NonNull
public String getUsr() {
return this.usr;
}

public void setUsr(@NonNull final String usr) {
this.usr = Preconditions.<String>checkNotNull(usr, "usr"); //$NON-NLS-1$
}

public String getId() {
return this.id;
}

public void setId(final String id) {
this.id = id;
}

public RangeAndUri getDeclarationRange() {
return this.declarationRange;
}

public void setDeclarationRange(final RangeAndUri declarationRange) {
this.declarationRange = declarationRange;
}

public RangeAndUri getDefinitionRange() {
return this.definitionRange;
}

public void setDefinitionRange(final RangeAndUri definitionRange) {
this.definitionRange = definitionRange;
}

@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this);
b.add("name", this.name); //$NON-NLS-1$
b.add("containerName", this.containerName); //$NON-NLS-1$
b.add("usr", this.usr); //$NON-NLS-1$
b.add("id", this.id); //$NON-NLS-1$
b.add("declarationRange", this.declarationRange); //$NON-NLS-1$
b.add("definitionRange", this.definitionRange); //$NON-NLS-1$
return b.toString();
}

@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SymbolDetails other = (SymbolDetails) obj;
if (this.name == null) {
if (other.name != null)
return false;
} else if (!this.name.equals(other.name))
return false;
if (this.containerName == null) {
if (other.containerName != null)
return false;
} else if (!this.containerName.equals(other.containerName))
return false;
if (this.usr == null) {
if (other.usr != null)
return false;
} else if (!this.usr.equals(other.usr))
return false;
if (this.id == null) {
if (other.id != null)
return false;
} else if (!this.id.equals(other.id))
return false;
if (this.declarationRange == null) {
if (other.declarationRange != null)
return false;
} else if (!this.declarationRange.equals(other.declarationRange))
return false;
if (this.definitionRange == null) {
if (other.definitionRange != null)
return false;
} else if (!this.definitionRange.equals(other.definitionRange))
return false;
return true;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
result = prime * result + ((this.containerName == null) ? 0 : this.containerName.hashCode());
result = prime * result + ((this.usr == null) ? 0 : this.usr.hashCode());
result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
result = prime * result + ((this.declarationRange == null) ? 0 : this.declarationRange.hashCode());
return prime * result + ((this.definitionRange == null) ? 0 : this.definitionRange.hashCode());
}
}
2 changes: 1 addition & 1 deletion features/org.eclipse.cdt.lsp.feature/feature.xml
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
<feature
id="org.eclipse.cdt.lsp.feature"
label="%featureName"
version="1.0.0.qualifier"
version="1.1.0.qualifier"
provider-name="%providerName"
license-feature="org.eclipse.license"
license-feature-version="0.0.0">
Binary file added images/editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed images/open-with.png
Binary file not shown.
Binary file modified images/preferences.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/properties.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.cdt.lsp</groupId>
<artifactId>org.eclipse.cdt.lsp.root</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<properties>
4 changes: 2 additions & 2 deletions releng/org.eclipse.cdt.lsp.repository/pom.xml
Original file line number Diff line number Diff line change
@@ -17,11 +17,11 @@
<parent>
<groupId>org.eclipse.cdt.lsp</groupId>
<artifactId>org.eclipse.cdt.lsp.root</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>org.eclipse.cdt.lsp.repository</artifactId>
<packaging>eclipse-repository</packaging>

Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@
<unit id="org.eclipse.linuxtools.docker.feature.feature.group" version="0.0.0" />
</location>
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="planner" includeSource="true" type="InstallableUnit">
<repository location="https://download.eclipse.org/lsp4e/snapshots/" />
<repository location="https://download.eclipse.org/lsp4e/releases/latest/" />
<unit id="org.eclipse.lsp4e" version="0.0.0" />
<unit id="org.eclipse.lsp4e.debug" version="0.0.0" />
</location>
@@ -37,7 +37,7 @@
<unit id="org.eclipse.swtbot.feature.group" version="0.0.0" />
</location>
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="planner" includeSource="true" type="InstallableUnit">
<repository location="https://download.eclipse.org/tm4e/snapshots/" />
<repository location="https://download.eclipse.org/tm4e/releases/latest/" />
<unit id="org.eclipse.tm4e.feature.feature.group" version="0.0.0" />
<unit id="org.eclipse.tm4e.language_pack.feature.feature.group" version="0.0.0" />
</location>
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
/*******************************************************************************
* Copyright (c) 2023 Bachmann electronic GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
* Alexander Fedorov (ArSysOp) - extract headless part
*******************************************************************************/

package org.eclipse.cdt.lsp.clangd.tests;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.util.Arrays;

import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;
import org.eclipse.cdt.core.settings.model.ICBuildSetting;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.internal.core.settings.model.CConfigurationDescriptionCache;
import org.eclipse.cdt.lsp.clangd.ClangdCProjectDescriptionListener;
import org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager;
import org.eclipse.cdt.lsp.clangd.MacroResolver;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.ui.PlatformUI;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.io.TempDir;

final class ClangdConfigurationFileManagerTest {

private static final String RELATIVE_DIR_PATH_BUILD_DEFAULT = "build" + File.separator + "default";
private static final String RELATIVE_DIR_PATH_BUILD_DEBUG = "build" + File.separator + "debug";
private static final String EXPANDED_CDB_SETTING = "CompileFlags: {Add: -ferror-limit=500, CompilationDatabase: %s, Compiler: g++}\nDiagnostics:\n ClangTidy: {Add: modernize*, Remove: modernize-use-trailing-return-type}\n";
private static final String DEFAULT_CDB_SETTING = "CompileFlags: {CompilationDatabase: %s}";
private static final String MODIFIED_DEFAULT_CDB_SETTING = DEFAULT_CDB_SETTING + "\n";
private static final String INVALID_YAML_SYNTAX_CONTAINS_TAB = "CompileFlags:\n\tCompilationDatabase: %s";
private static final String INVALID_YAML_SYNTAX_MISSING_BRACE = "CompileFlags: {CompilationDatabase: Release\r\n";
private final ClangdCProjectDescriptionListener clangdConfigurationManager = PlatformUI.getWorkbench()
.getService(ClangdCProjectDescriptionListener.class);
private final MacroResolver macroResolver = new MockMacroResolver();
private IProject project;

private static CProjectDescriptionEvent event = mock(CProjectDescriptionEvent.class);
private static ICProjectDescription description = mock(ICProjectDescription.class);
private static CConfigurationDescriptionCache config = mock(CConfigurationDescriptionCache.class);
private static ICBuildSetting setting = mock(ICBuildSetting.class);
private Path cwdBuilder;

@TempDir
private static File TEMP_DIR;

@BeforeAll
public static void setUp() throws Exception {
when(event.getNewCProjectDescription()).thenReturn(description);
when(description.getDefaultSettingConfiguration()).thenReturn(config);
when(config.getBuildSetting()).thenReturn(setting);
}

@BeforeEach
public void setUp(TestInfo testInfo) throws CoreException {
var projectName = TestUtils.getName(testInfo);
project = TestUtils.createCProject(projectName);
TestUtils.setLspPreferred(project, true);
when(event.getProject()).thenReturn(project);
}

@AfterEach
public void cleanUp() throws CoreException {
TestUtils.deleteProject(project);
}

private static File createFile(File parent, String format, String cdbDirectoryPath) throws FileNotFoundException {
return createFile(parent, ClangdConfigurationFileManager.CLANGD_CONFIG_FILE_NAME, format, cdbDirectoryPath);
}

private static File createFile(File parent, String fileName, String format, String cdbDirectoryPath)
throws FileNotFoundException {
var file = new File(parent, fileName);
try (var writer = new PrintWriter(file)) {
writer.printf(format, cdbDirectoryPath);
}
return file;
}

/**
* Creates a .clangd file in the current project
* @param format
* @param cdbDirectoryPath
* @return
* @throws UnsupportedEncodingException
* @throws IOException
* @throws CoreException
*/
private IFile createConfigFile(String format, String cdbDirectoryPath)
throws UnsupportedEncodingException, IOException, CoreException {
var file = project.getFile(ClangdConfigurationFileManager.CLANGD_CONFIG_FILE_NAME);
try (final var data = new ByteArrayInputStream(
String.format(format, cdbDirectoryPath).getBytes(project.getDefaultCharset()))) {
if (!file.exists()) {
file.create(data, false, new NullProgressMonitor());
} else {
file.setContents(data, IResource.KEEP_HISTORY, new NullProgressMonitor());
}
}
return file;
}

/**
* Test whether a new .clangd file will be created in the given project directory with the given
* configuration database (cdb) directory path (build/default) when the file does not exist.
*
* @throws IOException
* @throws CoreException
*/
@Test
void testCreateClangdConfigFileInProject() throws IOException, CoreException {
var projectDir = project.getLocation().toPortableString();
var configFile = new File(projectDir, ClangdConfigurationFileManager.CLANGD_CONFIG_FILE_NAME);
var refFile = createFile(TEMP_DIR, DEFAULT_CDB_SETTING, RELATIVE_DIR_PATH_BUILD_DEFAULT);
// The current working directory of the builder in the project is set to RELATIVE_DIR_PATH_BUILD_DEFAULT:
cwdBuilder = new Path(project.getLocation().append(RELATIVE_DIR_PATH_BUILD_DEFAULT).toPortableString());
when(setting.getBuilderCWD()).thenReturn(cwdBuilder);

// GIVEN a project without .clangd project configuration file:
assertTrue(configFile.length() == 0);
// WHEN the ClangdConfigurationManager.handleEvent method gets called:
clangdConfigurationManager.handleEvent(event, macroResolver);
// THEN a new file has been created in the project:
assertTrue(configFile.length() > 0);
// AND the file content is as expected:
assertTrue(Arrays.equals(Files.readAllBytes(configFile.toPath()), Files.readAllBytes(refFile.toPath())));

//clean up:
refFile.delete();
}

/**
* Test whether the new configuration database (cdb) directory path (build/debug) will be written to an existing but empty .clangd file
*
* @throws IOException
* @throws CoreException
*/
@Test
void testEmptyClangdConfigFileInProject() throws IOException, CoreException {
var refFile = createFile(TEMP_DIR, DEFAULT_CDB_SETTING, RELATIVE_DIR_PATH_BUILD_DEFAULT);
// The current working directory of the builder in the project is set to RELATIVE_DIR_PATH_BUILD_DEFAULT:
cwdBuilder = new Path(project.getLocation().append(RELATIVE_DIR_PATH_BUILD_DEFAULT).toPortableString());
when(setting.getBuilderCWD()).thenReturn(cwdBuilder);

// GIVEN an existing but empty .clangd configuration file in the project:
var emptyConfigFile = createConfigFile("%s", " ");
// WHEN the ClangdConfigurationManager.handleEvent method gets called with a new cdb path "build/debug":
clangdConfigurationManager.handleEvent(event, macroResolver);
// THEN the updated file matches the expected content with the given CompilationDatabase directory "build/debug":
assertTrue(Arrays.equals(Files.readAllBytes(emptyConfigFile.getLocation().toFile().toPath()),
Files.readAllBytes(refFile.toPath())));

//clean up:
refFile.delete();
}

/**
* Test whether the new configuration database (cdb) directory path (build/debug) will be written to an existing .clangd file
*
* @throws IOException
* @throws CoreException
*/
@Test
void testUpdateClangdConfigFileInProject() throws IOException, CoreException {
var projectDir = project.getLocation().toPortableString();
var configFile = new File(projectDir, ClangdConfigurationFileManager.CLANGD_CONFIG_FILE_NAME);
var refFileDefault = createFile(TEMP_DIR, ".clangdDefault", DEFAULT_CDB_SETTING,
RELATIVE_DIR_PATH_BUILD_DEFAULT);
// Use MODIFIED_DEFAULT_CDB_SETTING here, because the org.yaml.snakeyaml.Yaml.dump appends a '\n' at the last line:
var refFileDebug = createFile(TEMP_DIR, ".clangdDebug", MODIFIED_DEFAULT_CDB_SETTING,
RELATIVE_DIR_PATH_BUILD_DEBUG);
// The current working directory of the builder in the project is set to RELATIVE_DIR_PATH_BUILD_DEFAULT:
cwdBuilder = new Path(project.getLocation().append(RELATIVE_DIR_PATH_BUILD_DEFAULT).toPortableString());
when(setting.getBuilderCWD()).thenReturn(cwdBuilder);

// GIVEN a project without .clangd project configuration file:
assertTrue(configFile.length() == 0);
// AND the ClangdConfigurationManager.handleEvent method gets called:
clangdConfigurationManager.handleEvent(event, macroResolver);
// THEN a new file has been created in the project:
assertTrue(configFile.length() > 0);
// THEN the created file matches the expected content:
assertTrue(Arrays.equals(Files.readAllBytes(configFile.toPath()), Files.readAllBytes(refFileDefault.toPath())));

// WHEN the CWD in the build configuration changes to build/debug:
cwdBuilder = new Path(project.getLocation().append(RELATIVE_DIR_PATH_BUILD_DEBUG).toPortableString());
when(setting.getBuilderCWD()).thenReturn(cwdBuilder);
// AND the handleEvent gets called again:
clangdConfigurationManager.handleEvent(event, macroResolver);
// THEN the updated file matches the expected content:
assertTrue(Arrays.equals(Files.readAllBytes(configFile.toPath()), Files.readAllBytes(refFileDebug.toPath())));

//clean up:
refFileDefault.delete();
refFileDebug.delete();
}

/**
* Test whether the new configuration database (cdb) directory path (build/debug) will be written to an existing expanded .clangd file
*
* @throws IOException
* @throws CoreException
*/
@Test
void testUpdateExpandedClangdConfigFileInProject() throws IOException, CoreException {
var refFile = createFile(TEMP_DIR, EXPANDED_CDB_SETTING, RELATIVE_DIR_PATH_BUILD_DEBUG);

// GIVEN an existing expanded .clangd configuration file in the project pointing to "build/default":
var configFile = createConfigFile(EXPANDED_CDB_SETTING, RELATIVE_DIR_PATH_BUILD_DEFAULT);
// WHEN the ClangdConfigurationManager.handleEvent method gets called and the builder CWD points to "build/debug":
cwdBuilder = new Path(project.getLocation().append(RELATIVE_DIR_PATH_BUILD_DEBUG).toPortableString());
when(setting.getBuilderCWD()).thenReturn(cwdBuilder);
clangdConfigurationManager.handleEvent(event, macroResolver);
// THEN the updated file matches the expected content:
assertTrue(Arrays.equals(Files.readAllBytes(configFile.getLocation().toFile().toPath()),
Files.readAllBytes(refFile.toPath())));

//clean up:
refFile.delete();
}

/**
* Test whether a ScannerException will be thrown if the file contains invalid yaml syntax (here: tab)
*
* @throws IOException
* @throws CoreException
*/
@Test
void testInvalidYamlSyntax() throws IOException, CoreException {
// GIVEN an existing .clangd configuration file with invalid yaml syntax (contains tab):
var configFile = createConfigFile(INVALID_YAML_SYNTAX_CONTAINS_TAB, RELATIVE_DIR_PATH_BUILD_DEFAULT);
String beforeSet;
try (var inputStream = configFile.getContents()) {
beforeSet = new String(inputStream.readAllBytes());
}
// WHEN the ClangdConfigurationManager.setCompilationDatabasePath method gets called with a new cdb path "build/debug":
((ClangdConfigurationFileManager) clangdConfigurationManager).setCompilationDatabase(project,
RELATIVE_DIR_PATH_BUILD_DEBUG);
// THEN the file has not been changed, because the user shall fix the errors first:
try (var inputStream = configFile.getContents()) {
var afterSet = new String(inputStream.readAllBytes());
assertEquals(beforeSet, afterSet);
}
}

/**
* Test whether a ParserException will be thrown if the file contains invalid yaml syntax (here: missing })
*
* @throws IOException
* @throws CoreException
*/
@Test
void testInvalidYamlSyntax2() throws IOException, CoreException {
// GIVEN an existing .clangd configuration file with invalid yaml syntax (missing }):
var configFile = createConfigFile(INVALID_YAML_SYNTAX_MISSING_BRACE, RELATIVE_DIR_PATH_BUILD_DEFAULT);
String beforeSet;
try (var inputStream = configFile.getContents()) {
beforeSet = new String(inputStream.readAllBytes());
}
// WHEN the ClangdConfigurationManager.setCompilationDatabasePath method gets called with a new cdb path "build/debug":
((ClangdConfigurationFileManager) clangdConfigurationManager).setCompilationDatabase(project,
RELATIVE_DIR_PATH_BUILD_DEBUG);
// THEN the file has not been changed, because the user shall fix the errors first:
try (var inputStream = configFile.getContents()) {
var afterSet = new String(inputStream.readAllBytes());
assertEquals(beforeSet, afterSet);
}
}

}
Loading