diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b67c0..1d71419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [Unreleased] + +### Added + +- Added the "Rebuild code search index" which instructs Lexical to rebuild the + code search index for the current project. + ## [0.0.15] ### Added diff --git a/__mocks__/vscode.ts b/__mocks__/vscode.ts index bdb0ef6..0c6373e 100644 --- a/__mocks__/vscode.ts +++ b/__mocks__/vscode.ts @@ -48,3 +48,15 @@ export const l10n = { return message; }, }; + +export class CallHierarchyItem {} +export class CodeAction {} +export class CodeLens {} +export class CompletionItem {} +export class Converter {} +export class Diagnostic {} +export class DocumentLink {} +export class InlayHint {} +export class TypeHierarchyItem {} +export class SymbolInformation {} +export class CancellationError {} diff --git a/package-lock.json b/package-lock.json index 5c7c92b..470fe73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lexical", - "version": "0.0.12", + "version": "0.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lexical", - "version": "0.0.12", + "version": "0.0.15", "license": "Apache-2.0", "dependencies": { "axios": "1.6.7", @@ -32,7 +32,7 @@ "ovsx": "0.8.3", "prettier": "3.2.5", "ts-jest": "29.1.2", - "typescript": "5.3.3", + "typescript": "5.4.5", "vscode-uri": "3.0.8" }, "engines": { @@ -6920,9 +6920,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 143473f..ac09f03 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,11 @@ "command": "lexical.server.restart", "category": "Lexical", "title": "Restart Lexical's language server" + }, + { + "command": "lexical.server.reindexProject", + "category": "Lexical", + "title": "Rebuild code search index" } ], "configurationDefaults": { @@ -180,7 +185,7 @@ "ovsx": "0.8.3", "prettier": "3.2.5", "ts-jest": "29.1.2", - "typescript": "5.3.3", + "typescript": "5.4.5", "vscode-uri": "3.0.8" }, "dependencies": { diff --git a/src/commands/index.ts b/src/clientCommands/index.ts similarity index 100% rename from src/commands/index.ts rename to src/clientCommands/index.ts diff --git a/src/clientCommands/reindex-project.ts b/src/clientCommands/reindex-project.ts new file mode 100644 index 0000000..1bd2bb8 --- /dev/null +++ b/src/clientCommands/reindex-project.ts @@ -0,0 +1,26 @@ +import Commands from "."; +import Logger from "../logger"; +import { LanguageClient } from "vscode-languageclient/node"; +import ServerCommands from "../serverCommands/server-commands"; + +interface Context { + client: LanguageClient; +} + +const reindexProject: Commands.T = { + id: "lexical.server.reindexProject", + createHandler: ({ client }) => { + function handle() { + if (!client.isRunning()) { + Logger.error("Client is not running, cannot send command to server."); + return; + } + + ServerCommands.reindex(client); + } + + return handle; + }, +}; + +export default reindexProject; diff --git a/src/commands/restart-server.ts b/src/clientCommands/restart-server.ts similarity index 85% rename from src/commands/restart-server.ts rename to src/clientCommands/restart-server.ts index ed82fd1..c41042d 100644 --- a/src/commands/restart-server.ts +++ b/src/clientCommands/restart-server.ts @@ -1,12 +1,12 @@ import { LanguageClient } from "vscode-languageclient/node"; -import Commands from "."; +import ClientCommands from "."; import Logger from "../logger"; interface Context { client: LanguageClient; } -const restartServer: Commands.T = { +const restartServer: ClientCommands.T = { id: "lexical.server.restart", createHandler: ({ client }) => { function handle() { diff --git a/src/extension.ts b/src/extension.ts index bd28466..743710e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,10 +10,11 @@ import { } from "vscode-languageclient/node"; import { join } from "path"; import * as fs from "fs"; -import Commands from "./commands"; -import restartServer from "./commands/restart-server"; import { URI } from "vscode-uri"; import Logger from "./logger"; +import Commands from "./clientCommands"; +import restartServer from "./clientCommands/restart-server"; +import reindexProject from "./clientCommands/reindex-project"; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -28,9 +29,8 @@ export async function activate(context: ExtensionContext): Promise { context.subscriptions.push(commands.registerCommand(id, handler)); }); - registerCommand(restartServer, { - client, - }); + registerCommand(restartServer, { client }); + registerCommand(reindexProject, { client }); } } diff --git a/src/serverCommands/server-commands.ts b/src/serverCommands/server-commands.ts new file mode 100644 index 0000000..84696ad --- /dev/null +++ b/src/serverCommands/server-commands.ts @@ -0,0 +1,17 @@ +import { + ExecuteCommandRequest, + LanguageClient, +} from "vscode-languageclient/node"; + +const REINDEX_COMMAND_NAME = "Reindex"; + +namespace ServerCommands { + export async function reindex(client: LanguageClient): Promise { + await client.sendRequest(ExecuteCommandRequest.type, { + command: REINDEX_COMMAND_NAME, + arguments: [], + }); + } +} + +export default ServerCommands; diff --git a/src/test/auto-installer.test.ts b/src/test/auto-installer.test.ts index 795bc4f..c404f97 100644 --- a/src/test/auto-installer.test.ts +++ b/src/test/auto-installer.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, jest } from "@jest/globals"; +import { describe, test, expect, jest, beforeEach } from "@jest/globals"; import ReleaseVersion from "../release/version"; import ReleaseFixture from "./fixtures/release-fixture"; import AutoInstaller from "../auto-installer"; @@ -11,17 +11,6 @@ import * as os from "os"; import Zip from "../zip"; import Release from "../release"; -jest.mock("../installation-manifest", () => { - const original = jest.requireActual( - "../installation-manifest", - ); - - return { - ...original, - fetch: jest.fn(), - }; -}); -jest.mock("../github"); jest.mock("fs", () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { mockKeys } = require("./utils/strict-mocks"); @@ -33,10 +22,12 @@ jest.mock("fs", () => { realpathSync: original.realpathSync, }; }); -jest.mock("../zip"); -jest.mock("os"); describe("AutoInstaller", () => { + beforeEach(() => { + mockResolvedValue(Zip, "extract"); + }); + describe("isInstalledReleaseLatest", () => { test("should return false given manifest version is lower than remote version", () => { givenAnInstallationManifestWithVersion("0.3.0"); diff --git a/src/test/commands/index.test.ts b/src/test/clientCommands/index.test.ts similarity index 72% rename from src/test/commands/index.test.ts rename to src/test/clientCommands/index.test.ts index 8ac3b58..5488cca 100644 --- a/src/test/commands/index.test.ts +++ b/src/test/clientCommands/index.test.ts @@ -1,11 +1,11 @@ import { describe, expect, jest, test } from "@jest/globals"; -import Commands from "../../commands"; +import ClientCommands from "../../clientCommands"; describe("Commands", () => { describe("getRegisterFunction", () => { test("returns a function that registers the command with the client when called", () => { const clientRegister = jest.fn(); - const register = Commands.getRegisterFunction(clientRegister); + const register = ClientCommands.getRegisterFunction(clientRegister); register(commandStub, undefined); @@ -16,7 +16,7 @@ describe("Commands", () => { const handler = jest.fn(); -const commandStub: Commands.T = { +const commandStub: ClientCommands.T = { id: "stub", createHandler: () => handler, }; diff --git a/src/test/clientCommands/reindex-project.test.ts b/src/test/clientCommands/reindex-project.test.ts new file mode 100644 index 0000000..6d8fa69 --- /dev/null +++ b/src/test/clientCommands/reindex-project.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "@jest/globals"; +import reindexProject from "../../clientCommands/reindex-project"; +import { mockResolvedValue } from "../utils/strict-mocks"; +import ServerCommands from "../../serverCommands/server-commands"; +import clientStub from "../fixtures/client-stub"; + +describe("reindexProject", () => { + test("should call the 'Reindex' server command", () => { + const handler = reindexProject.createHandler({ + client: clientStub({ isRunning: true }), + }); + mockResolvedValue(ServerCommands, "reindex"); + + handler(); + + expect(ServerCommands.reindex).toHaveBeenCalled(); + }); + + test("given client is not running, it should do nothing", () => { + const handler = reindexProject.createHandler({ + client: clientStub({ isRunning: false }), + }); + mockResolvedValue(ServerCommands, "reindex"); + + handler(); + + expect(ServerCommands.reindex).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/commands/restart-server.test.ts b/src/test/clientCommands/restart-server.test.ts similarity index 53% rename from src/test/commands/restart-server.test.ts rename to src/test/clientCommands/restart-server.test.ts index cc1b7a1..5575238 100644 --- a/src/test/commands/restart-server.test.ts +++ b/src/test/clientCommands/restart-server.test.ts @@ -1,12 +1,13 @@ import { describe, expect, jest, test } from "@jest/globals"; -import restartServer from "../../commands/restart-server"; import { LanguageClient } from "vscode-languageclient/node"; +import restartServer from "../../clientCommands/restart-server"; +import clientStub from "../fixtures/client-stub"; describe("restartServer", () => { test("given it is running, restarts it", () => { const restart = jest.fn(); const handler = restartServer.createHandler({ - client: getClientStub({ isRunning: true, restart }), + client: clientStub({ isRunning: true, restart }), }); handler(); @@ -17,7 +18,7 @@ describe("restartServer", () => { test("given the client is not running, starts it", () => { const start = jest.fn(); const handler = restartServer.createHandler({ - client: getClientStub({ isRunning: false, start }), + client: clientStub({ isRunning: false, start }), }); handler(); @@ -25,21 +26,3 @@ describe("restartServer", () => { expect(start).toHaveBeenCalled(); }); }); - -function getClientStub({ - isRunning, - restart = jest.fn(), - start = jest.fn(), -}: { - isRunning: boolean; - restart?: LanguageClient["restart"]; - start?: LanguageClient["start"]; -}): LanguageClient { - return { - isRunning() { - return isRunning; - }, - restart, - start, - } as unknown as LanguageClient; -} diff --git a/src/test/fixtures/client-stub.ts b/src/test/fixtures/client-stub.ts new file mode 100644 index 0000000..72439cb --- /dev/null +++ b/src/test/fixtures/client-stub.ts @@ -0,0 +1,25 @@ +import { jest } from "@jest/globals"; +import { LanguageClient } from "vscode-languageclient/node"; + +interface Opts { + sendRequest?: jest.Mock; + isRunning?: boolean; + restart?: jest.Mock; + start?: jest.Mock; +} + +export default function clientStub({ + sendRequest = jest.fn(), + isRunning = true, + restart = jest.fn(), + start = jest.fn(), +}: Opts): LanguageClient { + return { + isRunning() { + return isRunning; + }, + sendRequest, + start, + restart, + } as unknown as LanguageClient; +} diff --git a/src/test/serverCommands/server-commands.test.ts b/src/test/serverCommands/server-commands.test.ts new file mode 100644 index 0000000..c277389 --- /dev/null +++ b/src/test/serverCommands/server-commands.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, jest, test } from "@jest/globals"; +import { ExecuteCommandRequest } from "vscode-languageclient/node"; +import ServerCommands from "../../serverCommands/server-commands"; +import clientStub from "../fixtures/client-stub"; + +describe("reindex", () => { + test("it should send a request to the server with the 'Reindex' command", () => { + const sendRequest = jest.fn(); + const client = clientStub({ sendRequest }); + + ServerCommands.reindex(client); + + expect(sendRequest).toHaveBeenCalledWith(ExecuteCommandRequest.type, { + command: "Reindex", + arguments: [], + }); + }); +}); diff --git a/src/test/utils/strict-mocks.ts b/src/test/utils/strict-mocks.ts index c70b10c..3f24f04 100644 --- a/src/test/utils/strict-mocks.ts +++ b/src/test/utils/strict-mocks.ts @@ -1,26 +1,43 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { jest } from "@jest/globals"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any type AsyncFunction = (...args: any) => Promise; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any type Fun = (...args: any) => unknown; -// keyof returns the keys of the given object type, except for keys of type never +// `keyof` returns the keys of the given object type, except for keys of type `never` type KeyOfType = keyof { - // All keys P that do not match the type V will be replaced by never + // All keys P that do not match the type V will be replaced by `never` [P in keyof T as T[P] extends V ? P : never]: unknown; }; -type AwaitedReturnOfFunctionOfType = Awaited< - ReturnOfFunctionOfType ->; +type AwaitedReturnOfFunctionOfType< + T, + F extends keyof T, +> = T[F] extends AsyncFunction ? Awaited> : never; type ReturnOfFunctionOfType = T[F] extends Fun ? ReturnType : never; -function mockModuleFunction( +/* + We wrap the value param type in a 1-tuple to get control over when the param is required or not. + In essence, we're looking for the same behavior as optional params (`param?: any`), but rather than being generally + optional we want the parameter to be required or not based on the function's other parameters. +*/ +type AsyncReturnTypeTuple> = + AwaitedReturnOfFunctionOfType extends void + ? [] + : [AwaitedReturnOfFunctionOfType]; +type SyncReturnTypeTuple> = + ReturnOfFunctionOfType extends void + ? [] + : [ReturnOfFunctionOfType]; + +function mockModuleFunction>( module: M, fun: F, -): jest.Mock<(...args: any) => any> { +): jest.Mock { if (!jest.isMockFunction(module[fun])) { module[fun] = jest.fn() as M[F]; } @@ -28,6 +45,17 @@ function mockModuleFunction( return module[fun] as jest.Mock; } +function mockAsyncModuleFunction>( + module: M, + fun: F, +): jest.Mock { + if (!jest.isMockFunction(module[fun])) { + module[fun] = jest.fn() as M[F]; + } + + return module[fun] as jest.Mock; +} + /* The V type is necessary because while KeyOfType asserts that F is a key of M, typescript isn't able to infer that it also matches AsyncFunction. @@ -39,32 +67,80 @@ function mockModuleFunction( export function mockResolvedValue>( module: M, fun: F, - value: AwaitedReturnOfFunctionOfType, + ...params: AsyncReturnTypeTuple ): void { - const mockedFunction = mockModuleFunction(module, fun); - mockedFunction.mockResolvedValue(value); + const [value] = params; + const mockedFunction = mockAsyncModuleFunction(module, fun); + mockedFunction.mockResolvedValue(value) as M[F]; } -export function mockRejectedValue( +export function mockResolvedValueOnce>( module: M, - fun: KeyOfType, - value: unknown, + fun: F, + ...params: AsyncReturnTypeTuple ): void { - const mockedFunction = mockModuleFunction(module, fun); - mockedFunction.mockRejectedValue(value); + const [value] = params; + const mockedFunction = mockAsyncModuleFunction(module, fun); + mockedFunction.mockResolvedValueOnce(value) as M[F]; +} + +export function mockBlockedPromise>( + module: M, + fun: F, +): void { + const mockedFunction = mockAsyncModuleFunction(module, fun); + mockedFunction.mockImplementation(() => new Promise(() => undefined)) as M[F]; +} + +export function mockBlockedPromiseOnce< + M, + F extends KeyOfType, +>(module: M, fun: F): void { + const mockedFunction = mockAsyncModuleFunction(module, fun); + mockedFunction.mockImplementationOnce( + () => new Promise(() => undefined), + ) as M[F]; +} + +export function mockRejectedValue>( + module: M, + fun: F, + value?: unknown, +): void { + const mockedFunction = mockAsyncModuleFunction(module, fun); + mockedFunction.mockRejectedValue(value ?? new Error()); +} + +export function mockRejectedValueOnce>( + module: M, + fun: F, + value?: unknown, +): void { + const mockedFunction = mockAsyncModuleFunction(module, fun); + mockedFunction.mockRejectedValueOnce(value ?? new Error()); } export function mockReturnValue>( module: M, fun: F, - value: ReturnOfFunctionOfType, + ...params: SyncReturnTypeTuple +): void { + const [value] = params; + const mockedFunction = mockModuleFunction(module, fun); + mockedFunction.mockReturnValue(value) as M[F]; +} + +export function clearMock>( + module: M, + fun: F, ): void { const mockedFunction = mockModuleFunction(module, fun); - mockedFunction.mockReturnValue(value); + mockedFunction.mockClear(); } export function mockKeys(obj: T): T { return Object.keys(obj).reduce((acc, cur) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any acc[cur as keyof T] = jest.fn() as any; return acc; }, {} as T);