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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# VS Code clangd Extension API

The VS Code clangd extension exposes an API that other extensions can consume:

```typescript
import * as vscode from 'vscode';
import type { ClangdExtension, ASTParams, ASTNode } from '@clangd/vscode-clangd';

const CLANGD_EXTENSION = 'llvm-vs-code-extensions.vscode-clangd';
const CLANGD_API_VERSION = 1;

const ASTRequestMethod = 'textDocument/ast';

const provideHover = async (document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.Hover | undefined> => {

const clangdExtension = vscode.extensions.getExtension<ClangdExtension>(CLANGD_EXTENSION);

if (clangdExtension) {
const api = (await clangdExtension.activate()).getApi(CLANGD_API_VERSION);
Copy link
Contributor

@HighCommander4 HighCommander4 Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably discourage consumers unconditionally calling activate(), since the clangd extension may already be activated and it doesn't expect activate() to be called again in that state.

I believe the right pattern here is for consuming code to:

  • use clangdExtension.exports to access the ClangdExtension object
  • if it's unsure whether the clangd extension is active yet at that point, then guard the access to .exports with a clangdExtension.isActive check
    • if the check fails, either bail or call activate() at that point

This is what I've done in my example plugin, and I'd prefer the README does the same. (Sorry to be picky on this point, I worry that users who may not have much familiarity with VSCode extensions may look to this README for how to use the API and assume that what it shows is a good way to do things.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vscode.Extension.activate() is designed to be called multiple times and only calls into the extension's activate() method once. Subsequent calls return the resolved extension. This is a common pattern and (just to be sure) I tested this scenario by activating it multiple times with logging in the extension activate() function. It worked as expected and only activates once.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking on this, I think this is fine then.


const textDocument = api.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document);
const range = api.languageClient.code2ProtocolConverter.asRange(new vscode.Range(position, position));
const params: ASTParams = { textDocument, range };

const ast: ASTNode | undefined = await api.languageClient.sendRequest(ASTRequestMethod, params);

if (!ast) {
return undefined;
}

return {
contents: [ast.kind]
};
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we show the registerHoverProvider() call in the README, just to make it a more complete example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added


vscode.languages.registerHoverProvider(['c', 'cpp'], { provideHover });
```
15 changes: 15 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@clangd/vscode-clangd",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a "license" field (MIT, like the main plugin)

Also, maybe a "homepage" link to a hosted version of the README (something like https://github.com/clangd/vscode-clangd/blob/master/api/README.md) would make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both added

"version": "0.0.0",
"description": "API for the llvm-vs-code-extensions.vscode-clangd VS Code extension",
"types": "vscode-clangd.d.ts",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
},
"repository": "https://github.com/clangd/vscode-clangd",
"license": "MIT",
"homepage": "https://github.com/clangd/vscode-clangd/blob/master/api/README.md",
"dependencies": {
"vscode-languageclient": "8.0.2"
}
}
36 changes: 36 additions & 0 deletions api/vscode-clangd.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {BaseLanguageClient} from 'vscode-languageclient';
import * as vscodelc from 'vscode-languageclient/node';

export interface ClangdApiV1 {
// vscode-clangd's language client which can be used to send requests to the
// clangd language server
// Standard requests:
// https://microsoft.github.io/language-server-protocol/specifications/specification-current
// clangd custom requests:
// https://clangd.llvm.org/extensions
languageClient: BaseLanguageClient
}

export interface ClangdExtension {
getApi(version: 1): ClangdApiV1;
}

// clangd custom request types
// (type declarations for other requests may be added later)

// textDocument/ast wire format
// Send: position
export interface ASTParams {
textDocument: vscodelc.TextDocumentIdentifier;
range: vscodelc.Range;
}

// Receive: tree of ASTNode
export interface ASTNode {
role: string; // e.g. expression
kind: string; // e.g. BinaryOperator
detail?: string; // e.g. ||
arcana?: string; // e.g. BinaryOperator <0x12345> <col:12, col:1> 'bool' '||'
children?: Array<ASTNode>;
range?: vscodelc.Range;
}
16 changes: 16 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {BaseLanguageClient} from 'vscode-languageclient';

import {ClangdApiV1, ClangdExtension} from '../api/vscode-clangd';

export class ClangdExtensionImpl implements ClangdExtension {
constructor(private readonly client: BaseLanguageClient) {}

public getApi(version: 1): ClangdApiV1;
public getApi(version: number): unknown {
if (version === 1) {
return {languageClient: this.client};
}

throw new Error(`No API version ${version} found`);
}
}
18 changes: 4 additions & 14 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,17 @@ import * as vscode from 'vscode';
import * as vscodelc from 'vscode-languageclient/node';

import {ClangdContext} from './clangd-context';
import type {ASTParams, ASTNode} from '../api/vscode-clangd';

const ASTRequestMethod = 'textDocument/ast';

export function activate(context: ClangdContext) {
const feature = new ASTFeature(context);
context.client.registerFeature(feature);
}

// The wire format: we send a position, and get back a tree of ASTNode.
interface ASTParams {
textDocument: vscodelc.TextDocumentIdentifier;
range: vscodelc.Range;
}
interface ASTNode {
role: string; // e.g. expression
kind: string; // e.g. BinaryOperator
detail?: string; // e.g. ||
arcana?: string; // e.g. BinaryOperator <0x12345> <col:12, col:1> 'bool' '||'
children?: Array<ASTNode>;
range?: vscodelc.Range;
}
const ASTRequestType =
new vscodelc.RequestType<ASTParams, ASTNode|null, void>('textDocument/ast');
new vscodelc.RequestType<ASTParams, ASTNode|null, void>(ASTRequestMethod);

class ASTFeature implements vscodelc.StaticFeature {
constructor(private context: ClangdContext) {
Expand Down
8 changes: 7 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import * as vscode from 'vscode';

import {ClangdExtension} from '../api/vscode-clangd';

import {ClangdExtensionImpl} from './api';
import {ClangdContext} from './clangd-context';

/**
* This method is called when the extension is activated. The extension is
* activated the very first time a command is executed.
*/
export async function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext):
Promise<ClangdExtension> {
const outputChannel = vscode.window.createOutputChannel('clangd');
context.subscriptions.push(outputChannel);

Expand Down Expand Up @@ -67,4 +71,6 @@ export async function activate(context: vscode.ExtensionContext) {
}
}, 5000);
}

return new ClangdExtensionImpl(clangdContext.client);
}