Skip to content

Commit

Permalink
Add support for dynamic text document content (#1532)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* Add refresh support and test bed code.
  • Loading branch information
dbaeumer authored Aug 7, 2024
1 parent a3b2c1d commit 03c455f
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 10 deletions.
21 changes: 21 additions & 0 deletions client-node-tests/src/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ suite('Client integration', () => {
},
willDelete: { filters: [{ scheme: fsProvider.scheme, pattern: { glob: '**/deleted-static/**{/,/*.txt}' } }] },
},
textDocumentContent: {
scheme: 'content-test'
}
},
linkedEditingRangeProvider: true,
diagnosticProvider: {
Expand Down Expand Up @@ -1536,6 +1539,24 @@ suite('Client integration', () => {
rangeEqual(symbol.location.range, 1, 2, 3, 4);
});

test('Text Document Content', async () => {
const providers = client.getFeature(lsclient.TextDocumentContentRequest.method)?.getProviders();
isDefined(providers);
assert.strictEqual(providers.length, 1);
const provider = providers[0].provider;
const result = await provider.provideTextDocumentContent(vscode.Uri.parse('content-test:///test.txt'), tokenSource.token);
assert.strictEqual(result, 'Some test content');

let middlewareCalled: boolean = false;
middleware.provideTextDocumentContent = (uri, token, next) => {
middlewareCalled = true;
return next(uri, token);
};
await provider.provideTextDocumentContent(vscode.Uri.parse('content-test:///test.txt'), tokenSource.token);
middleware.provideTextDocumentContent = undefined;
assert.strictEqual(middlewareCalled, true);
});

test('General middleware', async () => {
let middlewareCallCount = 0;

Expand Down
7 changes: 7 additions & 0 deletions client-node-tests/src/servers/testServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ connection.onInitialize((params: InitializeParams): any => {
filters: [{ scheme: 'file-test', pattern: { glob: '**/deleted-static/**{/,/*.txt}' } }]
},
},
textDocumentContent: {
scheme: 'content-test'
}
},
linkedEditingRangeProvider: true,
diagnosticProvider: {
Expand Down Expand Up @@ -522,6 +525,10 @@ connection.languages.inlineCompletion.on((_params) => {
];
});

connection.workspace.textDocumentContent.on((_params) => {
return 'Some test content';
});

connection.onRequest(
new ProtocolRequestType<null, null, never, any, any>('testing/sendSampleProgress'),
async (_, __) => {
Expand Down
10 changes: 7 additions & 3 deletions client/src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
ConnectionOptions, PositionEncodingKind, DocumentDiagnosticRequest, NotebookDocumentSyncRegistrationType, NotebookDocumentSyncRegistrationOptions, ErrorCodes,
MessageStrategy, DidOpenTextDocumentParams, CodeLensResolveRequest, CompletionResolveRequest, CodeActionResolveRequest, InlayHintResolveRequest, DocumentLinkResolveRequest, WorkspaceSymbolResolveRequest,
CancellationToken as ProtocolCancellationToken, InlineCompletionRequest, InlineCompletionRegistrationOptions, ExecuteCommandRequest, ExecuteCommandOptions, HandlerResult,
type DidCloseTextDocumentParams
type DidCloseTextDocumentParams, type TextDocumentContentRequest
} from 'vscode-languageserver-protocol';

import * as c2p from './codeConverter';
Expand Down Expand Up @@ -94,6 +94,7 @@ import { InlayHintsFeature, InlayHintsMiddleware, InlayHintsProviderShape } from
import { WorkspaceFoldersFeature, WorkspaceFolderMiddleware } from './workspaceFolder';
import { DidCreateFilesFeature, DidDeleteFilesFeature, DidRenameFilesFeature, WillCreateFilesFeature, WillDeleteFilesFeature, WillRenameFilesFeature, FileOperationsMiddleware } from './fileOperations';
import { InlineCompletionItemFeature, InlineCompletionMiddleware } from './inlineCompletion';
import { TextDocumentContentFeature, type TextDocumentContentMiddleware, type TextDocumentContentProviderShape } from './textDocumentContent';
import { FileSystemWatcherFeature } from './fileSystemWatcher';
import { ProgressFeature } from './progress';

Expand Down Expand Up @@ -342,7 +343,8 @@ export type Middleware = _Middleware & TextDocumentSynchronizationMiddleware & C
DocumentHighlightMiddleware & DocumentSymbolMiddleware & WorkspaceSymbolMiddleware & ReferencesMiddleware & TypeDefinitionMiddleware & ImplementationMiddleware &
ColorProviderMiddleware & CodeActionMiddleware & CodeLensMiddleware & FormattingMiddleware & RenameMiddleware & DocumentLinkMiddleware & ExecuteCommandMiddleware &
FoldingRangeProviderMiddleware & DeclarationMiddleware & SelectionRangeProviderMiddleware & CallHierarchyMiddleware & SemanticTokensMiddleware &
LinkedEditingRangeMiddleware & TypeHierarchyMiddleware & InlineValueMiddleware & InlayHintsMiddleware & NotebookDocumentMiddleware & DiagnosticProviderMiddleware & InlineCompletionMiddleware & GeneralMiddleware;
LinkedEditingRangeMiddleware & TypeHierarchyMiddleware & InlineValueMiddleware & InlayHintsMiddleware & NotebookDocumentMiddleware & DiagnosticProviderMiddleware &
InlineCompletionMiddleware & TextDocumentContentMiddleware & GeneralMiddleware;

export type LanguageClientOptions = {
documentSelector?: DocumentSelector | string[];
Expand Down Expand Up @@ -1933,6 +1935,7 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
getFeature(request: typeof DocumentDiagnosticRequest.method): DynamicFeature<TextDocumentRegistrationOptions> & TextDocumentProviderFeature<DiagnosticProviderShape> & DiagnosticFeatureShape;
getFeature(request: typeof NotebookDocumentSyncRegistrationType.method): DynamicFeature<NotebookDocumentSyncRegistrationOptions> & NotebookDocumentProviderShape;
getFeature(request: typeof InlineCompletionRequest.method): (DynamicFeature<InlineCompletionRegistrationOptions> & TextDocumentProviderFeature<InlineCompletionItemProvider>) | undefined;
getFeature(request: typeof TextDocumentContentRequest.method): DynamicFeature<TextDocumentRegistrationOptions> & WorkspaceProviderFeature<TextDocumentContentProviderShape> | undefined;
getFeature(request: typeof ExecuteCommandRequest.method): DynamicFeature<ExecuteCommandOptions>;
public getFeature(request: string): DynamicFeature<any> | undefined {
return this._dynamicFeatures.get(request);
Expand Down Expand Up @@ -2462,7 +2465,8 @@ function createConnection(input: MessageReader, output: MessageWriter, errorHand
export namespace ProposedFeatures {
export function createAll(_client: FeatureClient<Middleware, LanguageClientOptions>): (StaticFeature | DynamicFeature<any>)[] {
const result: (StaticFeature | DynamicFeature<any>)[] = [
new InlineCompletionItemFeature(_client)
new InlineCompletionItemFeature(_client),
new TextDocumentContentFeature(_client)
];
return result;
}
Expand Down
84 changes: 84 additions & 0 deletions client/src/common/textDocumentContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import * as vscode from 'vscode';
import { StaticRegistrationOptions, TextDocumentContentRefreshRequest, TextDocumentContentRequest, type ClientCapabilities, type ServerCapabilities, type TextDocumentContentParams, type TextDocumentContentRegistrationOptions } from 'vscode-languageserver-protocol';

import { WorkspaceFeature, ensure, type FeatureClient } from './features';
import * as UUID from './utils/uuid';


export interface ProvideTextDocumentContentSignature {
(this: void, uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult<string>;
}

export interface TextDocumentContentMiddleware {
provideTextDocumentContent?: (this: void, uri: vscode.Uri, token: vscode.CancellationToken, next: ProvideTextDocumentContentSignature) => vscode.ProviderResult<string>;
}

export interface TextDocumentContentProviderShape {
provider: vscode.TextDocumentContentProvider;
onDidChangeEmitter: vscode.EventEmitter<vscode.Uri>;
}

export class TextDocumentContentFeature extends WorkspaceFeature<TextDocumentContentRegistrationOptions, TextDocumentContentProviderShape, TextDocumentContentMiddleware> {

constructor(client: FeatureClient<TextDocumentContentMiddleware>) {
super(client, TextDocumentContentRequest.type);
}

public fillClientCapabilities(capabilities: ClientCapabilities): void {
const textDocumentContent = ensure(ensure(capabilities, 'workspace')!, 'textDocumentContent')!;
textDocumentContent.dynamicRegistration = true;
}

public initialize(capabilities: ServerCapabilities): void {
const client = this._client;
client.onRequest(TextDocumentContentRefreshRequest.type, async (params) => {
const uri = client.protocol2CodeConverter.asUri(params.uri);
for (const provider of this.getProviders()) {
provider.onDidChangeEmitter.fire(uri);
}
});

if (!capabilities?.workspace?.textDocumentContent) {
return;
}
const capability = capabilities.workspace.textDocumentContent;
const id = StaticRegistrationOptions.hasId(capability) ? capability.id : UUID.generateUuid();
this.register({
id: id,
registerOptions: capability
});
}

protected registerLanguageProvider(options: TextDocumentContentRegistrationOptions): [vscode.Disposable, TextDocumentContentProviderShape] {
const eventEmitter: vscode.EventEmitter<vscode.Uri> = new vscode.EventEmitter<vscode.Uri>();
const provider: vscode.TextDocumentContentProvider = {
onDidChange: eventEmitter.event,
provideTextDocumentContent: (uri, token) => {
const client = this._client;
const provideTextDocumentContent: ProvideTextDocumentContentSignature = (uri, token) => {
const params: TextDocumentContentParams = {
uri: client.code2ProtocolConverter.asUri(uri)
};
return client.sendRequest(TextDocumentContentRequest.type, params, token).then((result) => {
if (token.isCancellationRequested) {
return null;
}
return result;
}, (error) => {
return client.handleFailedRequest(TextDocumentContentRequest.type, token, error, null);
});
};
const middleware = client.middleware;
return middleware.provideTextDocumentContent
? middleware.provideTextDocumentContent(uri, token, provideTextDocumentContent)
: provideTextDocumentContent(uri, token);
}
};
return [vscode.workspace.registerTextDocumentContentProvider(options.scheme, provider), { provider, onDidChangeEmitter: eventEmitter }];
}
}
98 changes: 98 additions & 0 deletions protocol/src/common/protocol.textDocumentContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import type { DocumentUri } from 'vscode-languageserver-types';
import type { RequestHandler } from 'vscode-jsonrpc';

import { MessageDirection, ProtocolRequestType } from './messages';
import type { StaticRegistrationOptions } from './protocol';

/**
* Client capabilities for a text document content provider.
*
* @since 3.18.0
* @proposed
*/
export type TextDocumentContentClientCapabilities = {
/**
* Text document content provider supports dynamic registration.
*/
dynamicRegistration?: boolean;
};

/**
* Text document content provider options.
*
* @since 3.18.0
* @proposed
*/
export type TextDocumentContentOptions = {
/**
* The scheme for which the server provides content.
*/
scheme: string;
};

/**
* Text document content provider registration options.
*
* @since 3.18.0
* @proposed
*/
export type TextDocumentContentRegistrationOptions = TextDocumentContentOptions & StaticRegistrationOptions;

/**
* Parameters for the `workspace/textDocumentContent` request.
*
* @since 3.18.0
* @proposed
*/
export interface TextDocumentContentParams {
/**
* The uri of the text document.
*/
uri: DocumentUri;
}

/**
* The `workspace/textDocumentContent` request is sent from the client to the
* server to request the content of a text document.
*
* @since 3.18.0
* @proposed
*/
export namespace TextDocumentContentRequest {
export const method: 'workspace/textDocumentContent' = 'workspace/textDocumentContent';
export const messageDirection: MessageDirection = MessageDirection.clientToServer;
export const type = new ProtocolRequestType<TextDocumentContentParams, string, void, void, TextDocumentContentRegistrationOptions>(method);
export type HandlerSignature = RequestHandler<TextDocumentContentParams, string, void>;
}

/**
* Parameters for the `workspace/textDocumentContent/refresh` request.
*
* @since 3.18.0
* @proposed
*/
export interface TextDocumentContentRefreshParams {
/**
* The uri of the text document to refresh.
*/
uri: DocumentUri;
}

/**
* The `workspace/textDocumentContent` request is sent from the server to the client to refresh
* the content of a specific text document.
*
* @since 3.18.0
* @proposed
*/
export namespace TextDocumentContentRefreshRequest {
export const method: `workspace/textDocumentContent/refresh` = `workspace/textDocumentContent/refresh`;
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
export const type = new ProtocolRequestType<TextDocumentContentRefreshParams, void, void, void, void>(method);
export type HandlerSignature = RequestHandler<TextDocumentContentRefreshParams, void, void>;
}
28 changes: 27 additions & 1 deletion protocol/src/common/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,14 @@ import {
DidCloseNotebookDocumentNotification, NotebookDocumentFilterWithCells, NotebookDocumentFilterWithNotebook
} from './protocol.notebook';

import { InlineCompletionClientCapabilities, InlineCompletionOptions, InlineCompletionParams, InlineCompletionRegistrationOptions, InlineCompletionRequest } from './protocol.inlineCompletion';
import {
InlineCompletionClientCapabilities, InlineCompletionOptions, InlineCompletionParams, InlineCompletionRegistrationOptions, InlineCompletionRequest
} from './protocol.inlineCompletion';

import {
TextDocumentContentClientCapabilities, TextDocumentContentOptions, TextDocumentContentRegistrationOptions, TextDocumentContentParams,
TextDocumentContentRequest, TextDocumentContentRefreshParams, TextDocumentContentRefreshRequest
} from './protocol.textDocumentContent';

// @ts-ignore: to avoid inlining LocationLink as dynamic import
let __noDynamicImport: LocationLink | undefined;
Expand Down Expand Up @@ -654,6 +661,14 @@ export interface WorkspaceClientCapabilities {
* @proposed
*/
foldingRange?: FoldingRangeWorkspaceClientCapabilities;

/**
* Capabilities specific to the `workspace/textDocumentContent` request.
*
* @since 3.18.0
* @proposed
*/
textDocumentContent?: TextDocumentContentClientCapabilities;
}

/**
Expand Down Expand Up @@ -1186,6 +1201,14 @@ export type WorkspaceOptions = {
* @since 3.16.0
*/
fileOperations?: FileOperationOptions;

/**
* The server supports the `workspace/textDocumentContent` request.
*
* @since 3.18.0
* @proposed
*/
textDocumentContent?: TextDocumentContentOptions | TextDocumentContentRegistrationOptions;
};

/**
Expand Down Expand Up @@ -4286,6 +4309,9 @@ export {
DidCloseNotebookDocumentNotification, NotebookDocumentFilterWithCells, NotebookDocumentFilterWithNotebook,
// Inline Completions
InlineCompletionClientCapabilities, InlineCompletionOptions, InlineCompletionParams, InlineCompletionRegistrationOptions, InlineCompletionRequest,
// Text Document Content
TextDocumentContentClientCapabilities, TextDocumentContentOptions, TextDocumentContentRegistrationOptions, TextDocumentContentParams, TextDocumentContentRequest,
TextDocumentContentRefreshParams, TextDocumentContentRefreshRequest
};

// To be backwards compatible
Expand Down
6 changes: 4 additions & 2 deletions server/src/common/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SemanticTokensBuilder } from './semanticTokens';
import type { WorkDoneProgressReporter, WorkDoneProgressServerReporter, ResultProgressReporter } from './progress';

import * as ic from './inlineCompletion.proposed';
import * as tdc from './textDocumentContent';

export * from 'vscode-languageserver-protocol';
export { WorkDoneProgressReporter, WorkDoneProgressServerReporter, ResultProgressReporter };
Expand All @@ -19,10 +20,11 @@ export { NotebookDocuments };
export * from './server';

export namespace ProposedFeatures {
export const all: Features<_, _, _, _, _, _, ic.InlineCompletionFeatureShape, _> = {
export const all: Features<_, _, _, _, _, tdc.TextDocumentContentFeatureShape, ic.InlineCompletionFeatureShape, _> = {
__brand: 'features',
workspace: tdc.TextDocumentContentFeature,
languages: ic.InlineCompletionFeature
};

export type Connection = _Connection<_, _, _, _, _, _, ic.InlineCompletionFeatureShape, _>;
export type Connection = _Connection<_, _, _, _, _, tdc.TextDocumentContentFeatureShape, ic.InlineCompletionFeatureShape, _>;
}
Loading

0 comments on commit 03c455f

Please sign in to comment.