Skip to content

Commit

Permalink
Add some basic test that the LSP server appeared in DTD
Browse files Browse the repository at this point in the history
Currently disabled, since this only works running from source (the analysis server changes haven't landed yet).

See #5090
  • Loading branch information
DanTup committed Aug 27, 2024
1 parent 590831c commit 579e934
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/extension/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ class Config {
get openDevTools(): "never" | "flutter" | "always" { return this.getConfig<"never" | "flutter" | "always">("openDevTools", "never"); }
get openTestView(): Array<"testRunStart" | "testFailure"> { return this.getConfig<Array<"testRunStart" | "testFailure">>("openTestView", ["testRunStart"]); }
get previewCommitCharacters(): boolean { return this.getConfig<boolean>("previewCommitCharacters", false); }
// TODO(dantup): When removing this flag, be sure to update the test
// "should expose LSP methods via the analyzer"
get previewDtdLspIntegration(): boolean { return this.getConfig<boolean>("previewDtdLspIntegration", false); }
get previewFlutterUiGuides(): boolean { return this.getConfig<boolean>("previewFlutterUiGuides", false); }
get previewFlutterUiGuidesCustomTracking(): boolean { return this.getConfig<boolean>("previewFlutterUiGuidesCustomTracking", false); }
Expand Down
21 changes: 21 additions & 0 deletions src/shared/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,25 @@ export class EventEmitter<T> implements IAmDisposable {
}
}

export class EventsEmitter<T> implements IAmDisposable {
private emitter = new evt.EventEmitter();

public fire(event: string, x: T): void {
this.emitter.emit(event, x);
}

public listen(event: string, listener: (e: T) => any, thisArgs?: any): IAmDisposable {
if (thisArgs)
listener = listener.bind(thisArgs);
this.emitter.on(event, listener);
return {
dispose: () => { this.emitter.removeListener(event, listener); },
};
}

public dispose() {
this.emitter.removeAllListeners();
}
}

export type Event<T> = (listener: (e: T) => any, thisArgs?: any) => IAmDisposable;
36 changes: 34 additions & 2 deletions src/shared/services/tooling_daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as path from "path";
import * as ws from "ws";
import { dartVMPath, tenMinutesInMs } from "../constants";
import { LogCategory } from "../enums";
import { EventsEmitter } from "../events";
import { DartSdks, IAmDisposable, Logger } from "../interfaces";
import { CategoryLogger } from "../logging";
import { PromiseCompleter, PromiseOr, disposeAll } from "../utils";
import { UnknownNotification } from "./interfaces";
import { StdIOService } from "./stdio_service";
import { DebugSessionChangedEvent, DebugSessionStartedEvent, DebugSessionStoppedEvent, DeviceAddedEvent, DeviceChangedEvent, DeviceRemovedEvent, DeviceSelectedEvent, DtdMessage, DtdRequest, DtdResponse, DtdResult, EnablePlatformTypeParams, Event, EventKind, GetDebugSessionsResult, GetDevicesResult, GetIDEWorkspaceRootsParams, GetIDEWorkspaceRootsResult, HotReloadParams, HotRestartParams, OpenDevToolsPageParams, ReadFileAsStringParams, ReadFileAsStringResult, RegisterServiceParams, RegisterServiceResult, SelectDeviceParams, Service, ServiceMethod, SetIDEWorkspaceRootsParams, SetIDEWorkspaceRootsResult, Stream, SuccessResult } from "./tooling_daemon_services";
import { DebugSessionChangedEvent, DebugSessionStartedEvent, DebugSessionStoppedEvent, DeviceAddedEvent, DeviceChangedEvent, DeviceRemovedEvent, DeviceSelectedEvent, DtdMessage, DtdNotification, DtdRequest, DtdResponse, DtdResult, EnablePlatformTypeParams, Event, EventKind, GetDebugSessionsResult, GetDevicesResult, GetIDEWorkspaceRootsParams, GetIDEWorkspaceRootsResult, HotReloadParams, HotRestartParams, OpenDevToolsPageParams, ReadFileAsStringParams, ReadFileAsStringResult, RegisterServiceParams, RegisterServiceResult, SelectDeviceParams, Service, ServiceMethod, ServiceRegisteredEventData, ServiceUnregisteredEventData, SetIDEWorkspaceRootsParams, SetIDEWorkspaceRootsResult, Stream, SuccessResult } from "./tooling_daemon_services";

export class DartToolingDaemon implements IAmDisposable {
protected readonly disposables: IAmDisposable[] = [];
Expand All @@ -26,6 +27,8 @@ export class DartToolingDaemon implements IAmDisposable {
private connectedCompleter = new PromiseCompleter<ConnectionInfo | undefined>();
public get connected() { return this.connectedCompleter.promise; }

private readonly notificationsEmitters: { [key: string]: EventsEmitter<any> } = {};

constructor(
logger: Logger,
sdks: DartSdks,
Expand Down Expand Up @@ -80,7 +83,11 @@ export class DartToolingDaemon implements IAmDisposable {
const id = json.id;
const method = json.method;

if (id !== undefined && method) {
if (method === "streamNotify") {
const notification = json as DtdNotification;
this.notificationsEmitters[notification.params?.streamId]?.fire(notification.params.eventKind, notification.params.eventData);

} else if (id !== undefined && method) {
const request = json as DtdRequest;
// Handle service request.
const serviceHandler = this.serviceHandlers[method];
Expand Down Expand Up @@ -109,6 +116,21 @@ export class DartToolingDaemon implements IAmDisposable {
}
}

public onNotification(stream: string, eventKind: string, listener: (e: any) => any, thisArgs?: any): IAmDisposable {
if (!this.notificationsEmitters[stream])
this.notificationsEmitters[stream] = new EventsEmitter();

return this.notificationsEmitters[stream].listen(eventKind, listener, thisArgs);
}

public onServiceRegistered(listener: (e: ServiceRegisteredEventData) => any, thisArgs?: any): IAmDisposable {
return this.onNotification("Service", "ServiceRegistered", (e) => listener(e as ServiceRegisteredEventData), thisArgs);
}

public onServiceUnregistered(listener: (e: ServiceUnregisteredEventData) => any, thisArgs?: any): IAmDisposable {
return this.onNotification("Service", "ServiceUnregistered", (e) => listener(e as ServiceUnregisteredEventData), thisArgs);
}

public async registerService(service: Service.Editor, method: "getDevices", capabilities: object | undefined, f: () => PromiseOr<DtdResult & GetDevicesResult>): Promise<void>;
public async registerService(service: Service.Editor, method: "selectDevice", capabilities: object | undefined, f: (params: SelectDeviceParams) => PromiseOr<DtdResult & SuccessResult>): Promise<void>;
public async registerService(service: Service.Editor, method: "enablePlatformType", capabilities: object | undefined, f: (params: EnablePlatformTypeParams) => PromiseOr<DtdResult & SuccessResult>): Promise<void>;
Expand All @@ -130,6 +152,8 @@ export class DartToolingDaemon implements IAmDisposable {
public callMethod(service: ServiceMethod.setIDEWorkspaceRoots, params: SetIDEWorkspaceRootsParams): Promise<SetIDEWorkspaceRootsResult>;
public callMethod(service: ServiceMethod.getIDEWorkspaceRoots, params: GetIDEWorkspaceRootsParams): Promise<GetIDEWorkspaceRootsResult>;
public callMethod(service: ServiceMethod.readFileAsString, params: ReadFileAsStringParams): Promise<ReadFileAsStringResult>;
public callMethod(service: ServiceMethod.streamListen, params: { streamId: string }): Promise<DtdResult>;
public callMethod(service: ServiceMethod.streamCancel, params: { streamId: string }): Promise<DtdResult>;
public callMethod(service: string, params?: unknown): Promise<DtdResult>;
public async callMethod(method: ServiceMethod, params?: unknown): Promise<DtdResult> {
if (!this.connection)
Expand All @@ -149,6 +173,14 @@ export class DartToolingDaemon implements IAmDisposable {
return completer.promise;
}

public async streamListen(streamId: string): Promise<DtdResult> {
return this.callMethod(ServiceMethod.streamListen, { streamId });
}

public async streamCancel(streamId: string): Promise<DtdResult> {
return this.callMethod(ServiceMethod.streamCancel, { streamId });
}

public sendEvent(stream: Stream.Editor, params: DeviceAddedEvent | DeviceRemovedEvent | DeviceChangedEvent | DeviceSelectedEvent): void;
public sendEvent(stream: Stream.Editor, params: DebugSessionStartedEvent | DebugSessionStoppedEvent | DebugSessionChangedEvent): void;
public sendEvent(stream: Stream, params: Event): void {
Expand Down
26 changes: 26 additions & 0 deletions src/shared/services/tooling_daemon_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,45 @@ export interface DtdRequest {
params?: object;
}

export interface DtdNotification {
jsonrpc: "2.0",
id: string;
method: "streamNotify";
params: {
streamId: string,
eventKind: string,
eventData: ServiceRegisteredEventData | ServiceUnregisteredEventData | any,
};
}

export interface ServiceRegisteredEventData {
service: string,
method: string,
capabilities?: any,
}

export interface ServiceUnregisteredEventData {
service: string,
method: string,
}

export enum Service {
Editor,
}

export enum Stream {
Editor,
Service,
Lsp,
}

export enum ServiceMethod {
registerService = "registerService",
setIDEWorkspaceRoots = "FileSystem.setIDEWorkspaceRoots",
getIDEWorkspaceRoots = "FileSystem.getIDEWorkspaceRoots",
readFileAsString = "FileSystem.readFileAsString",
streamListen = "streamListen",
streamCancel = "streamCancel",
}

export interface RegisterServiceParams {
Expand Down
24 changes: 24 additions & 0 deletions src/test/dart/tooling_daemon/tooling_daemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from "path";
import * as vs from "vscode";
import { isWin } from "../../../shared/constants";
import { ServiceMethod } from "../../../shared/services/tooling_daemon_services";
import { waitFor } from "../../../shared/utils/promises";
import { activate, delay, extApi, flutterHelloWorldMainFile, helloWorldMainFile } from "../../helpers";

describe("dart tooling daemon", () => {
Expand Down Expand Up @@ -80,6 +81,29 @@ describe("dart tooling daemon", () => {
assert.equal(e.message, "Permission denied");
}
});

it("should expose LSP methods via the analyzer", async function () {
// TODO(dantup): Unskip when we have a version number of capability for this functionality.
this.skip();

const daemon = extApi.toolingDaemon;
assert.ok(daemon);

// Wait for daemon to be up.
await daemon.connected;
await delay(50);

// Collect all available services.
const services: string[] = [];
const servicesSub = daemon.onServiceRegistered((e) => services.push(`${e.service}.${e.method}`));
await daemon.streamListen("Service");
await waitFor(() => services.length); // Wait until we start getting services.
await delay(100); // And then slightly more.
await daemon.streamCancel("Service");
await servicesSub.dispose();

assert.ok(services.includes("Lsp.textDocument/hover"), `Did not find "Lsp.textDocument/hover" in ${services.join(", ")}`);
});
});

export function forceWindowsDriveLetterToLowercase<T extends string | undefined>(p: T): string | (undefined extends T ? undefined : never) {
Expand Down

0 comments on commit 579e934

Please sign in to comment.