import * as child_process from "node:child_process"; import * as path from "node:path"; import * as Protocol from "vscode-languageserver-protocol"; import * as Types from "vscode-languageserver-types"; import { URI } from "vscode-uri"; import * as LanguageServer from "./../src/LanguageServer"; const createWorkspaces = (names: string[]) => { const createSingleWorkspace = (name: string) => { const workspacePath = path.join(__dirname, name); const uri = LanguageServer.toURI(workspacePath); const folder: Protocol.WorkspaceFolder = { name, uri }; return { name, path: workspacePath, uri, folder, }; }; const workspaces = names.map(createSingleWorkspace); const toTestResult = ( symbol: Types.SymbolInformation | Types.WorkspaceSymbol, ) => { const location = Types.Location.is(symbol.location) ? `${symbol.location.range.start.line}:${symbol.location.range.start.character} ${symbol.location.range.end.line}:${symbol.location.range.end.character}` : ""; return `${symbol.name} ${symbol.kind} ${workspaces.reduce( (acc, { path: workspacePath, name: workspaceName }) => { const uri = symbol.location.uri; const fsPath = URI.parse(uri).fsPath; if (!fsPath.includes(workspaceName)) return acc; return URI.file( path.relative(path.resolve(workspacePath, ".."), fsPath), ).path; }, "", )} ${location}`; }; return { workspaces, toTestResult, }; }; const buildProject = (cwd: string): void => { child_process.execSync("dune build --root . @check", { cwd: cwd }); }; describe("workspace/symbol", () => { let languageServer: LanguageServer.LanguageServer; const { workspaces: [workspaceA, workspaceB], toTestResult, } = createWorkspaces(["workspace_symbol_A", "workspace_symbol_B"]); afterEach(async () => { await LanguageServer.exit(languageServer); }); async function queryWorkspaceSymbol(params: Protocol.WorkspaceSymbolParams) { return ( (await languageServer.sendRequest( Protocol.WorkspaceSymbolRequest.type, params, )) ?? [] ); } it("returns all symbols from workspace", async () => { languageServer = await LanguageServer.startAndInitialize({ workspaceFolders: [workspaceA.folder], }); buildProject(workspaceA.path); const symbols = await queryWorkspaceSymbol({ query: "", }); expect(symbols.map(toTestResult)).toMatchInlineSnapshot(` [ "stack_of_ints 5 /workspace_symbol_A/bin/a.ml 51:0 65:5", "size 6 /workspace_symbol_A/bin/a.ml 64:11 64:15", "peek 6 /workspace_symbol_A/bin/a.ml 62:11 62:15", "pop 6 /workspace_symbol_A/bin/a.ml 57:11 57:14", "push 6 /workspace_symbol_A/bin/a.ml 55:11 55:15", "the_list 12 /workspace_symbol_A/bin/a.ml 53:16 53:24", "Foo 9 /workspace_symbol_A/bin/a.ml 49:0 49:23", "Increment 2 /workspace_symbol_A/bin/a.ml 45:0 47:3", "increment_x 12 /workspace_symbol_A/bin/a.ml 46:2 46:27", "X_int 2 /workspace_symbol_A/bin/a.ml 41:0 43:3", "x 12 /workspace_symbol_A/bin/a.ml 42:2 42:13", "A_Mod 2 /workspace_symbol_A/bin/a.ml 29:0 39:3", "compare 12 /workspace_symbol_A/bin/a.ml 38:2 38:30", "private_mod_fn 12 /workspace_symbol_A/bin/a.ml 36:2 36:33", "t 15 /workspace_symbol_A/bin/a.ml 34:2 34:14", "My_string 2 /workspace_symbol_A/bin/a.ml 27:0 27:25", "StringMap 2 /workspace_symbol_A/bin/a.ml 26:0 26:36", "a_i 12 /workspace_symbol_A/bin/a.ml 22:0 24:7", "a_arr 12 /workspace_symbol_A/bin/a.ml 18:0 18:14", "a_u 12 /workspace_symbol_A/bin/a.ml 16:0 16:15", "user 15 /workspace_symbol_A/bin/a.ml 12:0 14:12", "NotAdmin 9 /workspace_symbol_A/bin/a.ml 14:2 14:12", "Admin 9 /workspace_symbol_A/bin/a.ml 13:2 13:9", "a_d 12 /workspace_symbol_A/bin/a.ml 7:0 10:14", "A_B 2 /workspace_symbol_A/bin/a.ml 2:0 5:3", "a_b 12 /workspace_symbol_A/bin/a.ml 4:2 4:19", "a_b_t 15 /workspace_symbol_A/bin/a.ml 3:2 3:21", "a_x 12 /workspace_symbol_A/bin/a.ml 0:0 0:11", "main_y 12 /workspace_symbol_A/bin/main.ml 0:0 0:22", "vendored_x 12 /workspace_symbol_A/lib/lib.ml 14:0 14:31", "lib_private_fn 12 /workspace_symbol_A/lib/lib.ml 10:0 10:38", "hd 12 /workspace_symbol_A/lib/lib.ml 8:0 8:16", "lib_x 12 /workspace_symbol_A/lib/lib.ml 6:0 6:14", "user 15 /workspace_symbol_A/lib/lib.ml 3:0 5:1", "name 7 /workspace_symbol_A/lib/lib.ml 4:2 4:14", "t 15 /workspace_symbol_A/lib/LibTypes.mli 0:0 0:15", "x 12 /workspace_symbol_A/vendor/vendored_lib.ml 0:0 0:9", ] `); }); it("returns filtered symbols from workspace", async () => { languageServer = await LanguageServer.startAndInitialize({ workspaceFolders: [workspaceA.folder], }); buildProject(workspaceA.path); const symbols = await queryWorkspaceSymbol({ query: "a_", }); expect(symbols.map(toTestResult)).toMatchInlineSnapshot(` [ "a_i 12 /workspace_symbol_A/bin/a.ml 22:0 24:7", "a_arr 12 /workspace_symbol_A/bin/a.ml 18:0 18:14", "a_u 12 /workspace_symbol_A/bin/a.ml 16:0 16:15", "a_d 12 /workspace_symbol_A/bin/a.ml 7:0 10:14", "a_b 12 /workspace_symbol_A/bin/a.ml 4:2 4:19", "a_b_t 15 /workspace_symbol_A/bin/a.ml 3:2 3:21", "a_x 12 /workspace_symbol_A/bin/a.ml 0:0 0:11", ] `); }); it("handles mutiple wokspaces", async () => { languageServer = await LanguageServer.startAndInitialize({ workspaceFolders: [workspaceA.folder, workspaceB.folder], }); buildProject(workspaceA.path); buildProject(workspaceB.path); const symbols = await queryWorkspaceSymbol({ query: "", }); /* FIXME: symbol lib_type from lib.ml is missing */ expect(symbols.map(toTestResult)).toMatchInlineSnapshot(` [ "stack_of_ints 5 /workspace_symbol_A/bin/a.ml 51:0 65:5", "size 6 /workspace_symbol_A/bin/a.ml 64:11 64:15", "peek 6 /workspace_symbol_A/bin/a.ml 62:11 62:15", "pop 6 /workspace_symbol_A/bin/a.ml 57:11 57:14", "push 6 /workspace_symbol_A/bin/a.ml 55:11 55:15", "the_list 12 /workspace_symbol_A/bin/a.ml 53:16 53:24", "Foo 9 /workspace_symbol_A/bin/a.ml 49:0 49:23", "Increment 2 /workspace_symbol_A/bin/a.ml 45:0 47:3", "increment_x 12 /workspace_symbol_A/bin/a.ml 46:2 46:27", "X_int 2 /workspace_symbol_A/bin/a.ml 41:0 43:3", "x 12 /workspace_symbol_A/bin/a.ml 42:2 42:13", "A_Mod 2 /workspace_symbol_A/bin/a.ml 29:0 39:3", "compare 12 /workspace_symbol_A/bin/a.ml 38:2 38:30", "private_mod_fn 12 /workspace_symbol_A/bin/a.ml 36:2 36:33", "t 15 /workspace_symbol_A/bin/a.ml 34:2 34:14", "My_string 2 /workspace_symbol_A/bin/a.ml 27:0 27:25", "StringMap 2 /workspace_symbol_A/bin/a.ml 26:0 26:36", "a_i 12 /workspace_symbol_A/bin/a.ml 22:0 24:7", "a_arr 12 /workspace_symbol_A/bin/a.ml 18:0 18:14", "a_u 12 /workspace_symbol_A/bin/a.ml 16:0 16:15", "user 15 /workspace_symbol_A/bin/a.ml 12:0 14:12", "NotAdmin 9 /workspace_symbol_A/bin/a.ml 14:2 14:12", "Admin 9 /workspace_symbol_A/bin/a.ml 13:2 13:9", "a_d 12 /workspace_symbol_A/bin/a.ml 7:0 10:14", "A_B 2 /workspace_symbol_A/bin/a.ml 2:0 5:3", "a_b 12 /workspace_symbol_A/bin/a.ml 4:2 4:19", "a_b_t 15 /workspace_symbol_A/bin/a.ml 3:2 3:21", "a_x 12 /workspace_symbol_A/bin/a.ml 0:0 0:11", "main_y 12 /workspace_symbol_A/bin/main.ml 0:0 0:22", "vendored_x 12 /workspace_symbol_A/lib/lib.ml 14:0 14:31", "lib_private_fn 12 /workspace_symbol_A/lib/lib.ml 10:0 10:38", "hd 12 /workspace_symbol_A/lib/lib.ml 8:0 8:16", "lib_x 12 /workspace_symbol_A/lib/lib.ml 6:0 6:14", "user 15 /workspace_symbol_A/lib/lib.ml 3:0 5:1", "name 7 /workspace_symbol_A/lib/lib.ml 4:2 4:14", "t 15 /workspace_symbol_A/lib/LibTypes.mli 0:0 0:15", "x 12 /workspace_symbol_A/vendor/vendored_lib.ml 0:0 0:9", "workspace_B 12 /workspace_symbol_B/main.ml 0:0 0:31", ] `); }); it("shows a notification message if no build directory found", async () => { languageServer = await LanguageServer.startAndInitialize({ workspaceFolders: [workspaceA.folder, workspaceB.folder], }); child_process.execSync("dune clean", { cwd: workspaceA.path }); child_process.execSync("dune clean", { cwd: workspaceB.path }); type StarNotificationHandlerParams = Parameters; const receivedNotification: Promise<{ method: StarNotificationHandlerParams[0]; params: StarNotificationHandlerParams[1]; }> = new Promise((resolve, _reject) => languageServer.onNotification((method, params) => resolve({ method, params }), ), ); await queryWorkspaceSymbol({ query: "", }); const notification = await receivedNotification; expect(notification.method).toBe( Protocol.ShowMessageNotification.type.method, ); // @ts-ignore expect(notification.params.type).toEqual(Protocol.MessageType.Warning); // @ts-ignore expect(notification.params.message).toBe( `No build directory found in workspace(s): ${workspaceA.name}, ${workspaceB.name}`, ); }); });