Skip to content

Commit 0623705

Browse files
committed
fix: refresh codebase index on config change
1 parent 030e534 commit 0623705

File tree

2 files changed

+334
-1
lines changed

2 files changed

+334
-1
lines changed

core/indexing/CodebaseIndexer.test.ts

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable max-lines-per-function */
2+
/* lint is not useful for test classes */
13
import { jest } from "@jest/globals";
24
import { execSync } from "node:child_process";
35
import fs from "node:fs";
@@ -14,6 +16,9 @@ import {
1416
} from "../test/testDir.js";
1517
import { getIndexSqlitePath } from "../util/paths.js";
1618

19+
import { ConfigResult } from "@continuedev/config-yaml";
20+
import CodebaseContextProvider from "../context/providers/CodebaseContextProvider.js";
21+
import { ContinueConfig } from "../index.js";
1722
import { localPathToUri } from "../util/pathToUri.js";
1823
import { CodebaseIndexer, PauseToken } from "./CodebaseIndexer.js";
1924
import { getComputeDeleteAddRemove } from "./refreshIndex.js";
@@ -57,6 +62,17 @@ class TestCodebaseIndexer extends CodebaseIndexer {
5762
protected async getIndexesToBuild(): Promise<CodebaseIndex[]> {
5863
return [new TestCodebaseIndex()];
5964
}
65+
66+
// Add public methods to test private methods
67+
public testHasCodebaseContextProvider() {
68+
return (this as any).hasCodebaseContextProvider();
69+
}
70+
71+
public async testHandleConfigUpdate(
72+
configResult: ConfigResult<ContinueConfig>,
73+
) {
74+
return (this as any).handleConfigUpdate({ config: configResult.config });
75+
}
6076
}
6177

6278
// Create a mock messenger type that doesn't require actual protocol imports
@@ -411,4 +427,270 @@ describe("CodebaseIndexer", () => {
411427
expect(codebaseIndexer.currentIndexingState).toEqual(testState);
412428
});
413429
});
430+
431+
// New describe block for testing handleConfigUpdate functionality
432+
describe("handleConfigUpdate functionality", () => {
433+
let testIndexer: TestCodebaseIndexer;
434+
let mockRefreshCodebaseIndex: jest.MockedFunction<any>;
435+
let mockGetWorkspaceDirs: jest.MockedFunction<any>;
436+
437+
beforeEach(() => {
438+
testIndexer = new TestCodebaseIndexer(
439+
testConfigHandler,
440+
testIde,
441+
mockMessenger as any,
442+
false,
443+
);
444+
445+
// Mock the refreshCodebaseIndex method to avoid actual indexing
446+
mockRefreshCodebaseIndex = jest
447+
.spyOn(testIndexer, "refreshCodebaseIndex")
448+
.mockImplementation(async () => {});
449+
450+
// Mock getWorkspaceDirs to return test directories
451+
mockGetWorkspaceDirs = jest
452+
.spyOn(testIde, "getWorkspaceDirs")
453+
.mockResolvedValue(["/test/workspace"]);
454+
});
455+
456+
afterEach(() => {
457+
jest.clearAllMocks();
458+
});
459+
460+
describe("hasCodebaseContextProvider", () => {
461+
test("should return true when codebase context provider is present", () => {
462+
// Set up config with codebase context provider
463+
(testIndexer as any).config = {
464+
contextProviders: [
465+
{
466+
description: {
467+
title: CodebaseContextProvider.description.title,
468+
},
469+
},
470+
],
471+
};
472+
473+
const result = testIndexer.testHasCodebaseContextProvider();
474+
expect(result).toBe(true);
475+
});
476+
477+
test("should return false when no context providers are configured", () => {
478+
(testIndexer as any).config = {
479+
contextProviders: undefined,
480+
};
481+
482+
const result = testIndexer.testHasCodebaseContextProvider();
483+
expect(result).toBe(false);
484+
});
485+
486+
test("should return false when context providers exist but no codebase provider", () => {
487+
(testIndexer as any).config = {
488+
contextProviders: [
489+
{
490+
description: {
491+
title: "SomeOtherProvider",
492+
},
493+
},
494+
],
495+
};
496+
497+
const result = testIndexer.testHasCodebaseContextProvider();
498+
expect(result).toBe(false);
499+
});
500+
501+
test("should return false when context providers is empty array", () => {
502+
(testIndexer as any).config = {
503+
contextProviders: [],
504+
};
505+
506+
const result = testIndexer.testHasCodebaseContextProvider();
507+
expect(result).toBe(false);
508+
});
509+
});
510+
511+
describe("handleConfigUpdate", () => {
512+
test("should return early when newConfig is null", async () => {
513+
const configResult: ConfigResult<ContinueConfig> = {
514+
config: null as any,
515+
errors: [],
516+
configLoadInterrupted: false,
517+
};
518+
519+
await testIndexer.testHandleConfigUpdate(configResult);
520+
521+
// These get called once on init, so we want them to not get called again
522+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
523+
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
524+
});
525+
526+
test("should return early when newConfig is undefined", async () => {
527+
const configResult: ConfigResult<ContinueConfig> = {
528+
config: undefined as any,
529+
errors: [],
530+
configLoadInterrupted: false,
531+
};
532+
533+
await testIndexer.testHandleConfigUpdate(configResult);
534+
535+
// These get called once on init, so we want them to not get called again
536+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
537+
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
538+
});
539+
540+
test("should return early when no codebase context provider is present", async () => {
541+
const configResult: ConfigResult<ContinueConfig> = {
542+
config: {
543+
contextProviders: [
544+
{
545+
description: {
546+
title: "SomeOtherProvider",
547+
},
548+
},
549+
],
550+
selectedModelByRole: {
551+
embed: {
552+
model: "test-model",
553+
provider: "test-provider",
554+
},
555+
},
556+
} as unknown as ContinueConfig,
557+
errors: [],
558+
configLoadInterrupted: false,
559+
};
560+
561+
await testIndexer.testHandleConfigUpdate(configResult);
562+
563+
// These get called once on init, so we want them to not get called again
564+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
565+
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
566+
});
567+
568+
test("should return early when no embed model is configured", async () => {
569+
const configResult: ConfigResult<ContinueConfig> = {
570+
config: {
571+
contextProviders: [
572+
{
573+
description: {
574+
title: CodebaseContextProvider.description.title,
575+
},
576+
},
577+
],
578+
selectedModelByRole: {
579+
embed: undefined,
580+
},
581+
} as unknown as ContinueConfig,
582+
errors: [],
583+
configLoadInterrupted: false,
584+
};
585+
586+
await testIndexer.testHandleConfigUpdate(configResult);
587+
588+
// These get called once on init, so we want them to not get called again
589+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
590+
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
591+
});
592+
593+
test("should call refreshCodebaseIndex when all conditions are met", async () => {
594+
const configResult: ConfigResult<ContinueConfig> = {
595+
config: {
596+
contextProviders: [
597+
{
598+
description: {
599+
title: CodebaseContextProvider.description.title,
600+
},
601+
},
602+
],
603+
selectedModelByRole: {
604+
embed: {
605+
model: "test-model",
606+
provider: "test-provider",
607+
},
608+
},
609+
} as unknown as ContinueConfig,
610+
errors: [],
611+
configLoadInterrupted: false,
612+
};
613+
614+
await testIndexer.testHandleConfigUpdate(configResult);
615+
616+
// These get called once on init, and we want them to get called again
617+
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(2);
618+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(2);
619+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledWith([
620+
"/test/workspace",
621+
]);
622+
});
623+
624+
test("should set config property before checking conditions", async () => {
625+
const testConfig = {
626+
contextProviders: [
627+
{
628+
description: {
629+
title: CodebaseContextProvider.description.title,
630+
},
631+
},
632+
],
633+
selectedModelByRole: {
634+
embed: {
635+
model: "test-model",
636+
provider: "test-provider",
637+
},
638+
},
639+
} as unknown as ContinueConfig;
640+
641+
const configResult: ConfigResult<ContinueConfig> = {
642+
config: testConfig,
643+
errors: [],
644+
configLoadInterrupted: false,
645+
};
646+
647+
await testIndexer.testHandleConfigUpdate(configResult);
648+
649+
// Verify that the config was set
650+
expect((testIndexer as any).config).toBe(testConfig);
651+
// These get called once on init, and we want them to get called again
652+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(2);
653+
});
654+
655+
test("should handle multiple context providers correctly", async () => {
656+
const configResult: ConfigResult<ContinueConfig> = {
657+
config: {
658+
contextProviders: [
659+
{
660+
description: {
661+
title: "SomeOtherProvider",
662+
},
663+
},
664+
{
665+
description: {
666+
title: CodebaseContextProvider.description.title,
667+
},
668+
},
669+
{
670+
description: {
671+
title: "AnotherProvider",
672+
},
673+
},
674+
],
675+
selectedModelByRole: {
676+
embed: {
677+
model: "test-model",
678+
provider: "test-provider",
679+
},
680+
},
681+
} as unknown as ContinueConfig,
682+
errors: [],
683+
configLoadInterrupted: false,
684+
};
685+
686+
await testIndexer.testHandleConfigUpdate(configResult);
687+
688+
// These get called once on init, and we want them to get called again
689+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(2);
690+
expect(mockRefreshCodebaseIndex).toHaveBeenCalledWith([
691+
"/test/workspace",
692+
]);
693+
});
694+
});
695+
});
414696
});

core/indexing/CodebaseIndexer.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import * as fs from "fs/promises";
22

33
import { ConfigHandler } from "../config/ConfigHandler.js";
4-
import { IDE, IndexingProgressUpdate, IndexTag } from "../index.js";
4+
import {
5+
ContinueConfig,
6+
IDE,
7+
IndexingProgressUpdate,
8+
IndexTag,
9+
} from "../index.js";
510
import type { FromCoreProtocol, ToCoreProtocol } from "../protocol";
611
import type { IMessenger } from "../protocol/messenger";
712
import { extractMinimalStackTraceInfo } from "../util/extractMinimalStackTraceInfo.js";
813
import { getIndexSqlitePath, getLanceDbPath } from "../util/paths.js";
914
import { Telemetry } from "../util/posthog.js";
1015
import { findUriInDirs, getUriPathBasename } from "../util/uri.js";
1116

17+
import { ConfigResult } from "@continuedev/config-yaml";
18+
import CodebaseContextProvider from "../context/providers/CodebaseContextProvider.js";
1219
import { ContinueServerClient } from "../continueServer/stubs/client";
1320
import { LLMError } from "../llm/index.js";
1421
import { getRootCause } from "../util/errors.js";
@@ -44,6 +51,8 @@ export class CodebaseIndexer {
4451
* - To make as few requests as possible to the embeddings providers
4552
*/
4653
filesPerBatch = 500;
54+
public isInitialized: Promise<void>;
55+
private config!: ContinueConfig;
4756
private indexingCancellationController: AbortController | undefined;
4857
private codebaseIndexingState: IndexingProgressUpdate;
4958
private readonly pauseToken: PauseToken;
@@ -73,6 +82,17 @@ export class CodebaseIndexer {
7382

7483
// Initialize pause token
7584
this.pauseToken = new PauseToken(initialPaused);
85+
86+
this.isInitialized = this.init(configHandler);
87+
}
88+
89+
// Initialization - load config and attach config listener
90+
private async init(configHandler: ConfigHandler) {
91+
const result = await configHandler.loadConfig();
92+
await this.handleConfigUpdate(result);
93+
configHandler.onConfigUpdate(
94+
this.handleConfigUpdate.bind(this) as (arg: any) => void,
95+
);
7696
}
7797

7898
/**
@@ -652,4 +672,35 @@ export class CodebaseIndexer {
652672
public get currentIndexingState(): IndexingProgressUpdate {
653673
return this.codebaseIndexingState;
654674
}
675+
676+
private hasCodebaseContextProvider() {
677+
return !!this.config.contextProviders?.some(
678+
(provider) =>
679+
provider.description.title ===
680+
CodebaseContextProvider.description.title,
681+
);
682+
}
683+
684+
private async handleConfigUpdate({
685+
config: newConfig,
686+
}: ConfigResult<ContinueConfig>) {
687+
if (newConfig) {
688+
this.config = newConfig; // IMPORTANT - need to set up top, other methods below use this without passing it in
689+
690+
// No point in indexing if no codebase context provider
691+
const hasCodebaseContextProvider = this.hasCodebaseContextProvider();
692+
if (!hasCodebaseContextProvider) {
693+
return;
694+
}
695+
696+
// Skip codebase indexing if not supported
697+
// No warning message here because would show on ANY config update
698+
if (!this.config.selectedModelByRole.embed) {
699+
return;
700+
}
701+
702+
const dirs = await this.ide.getWorkspaceDirs();
703+
await this.refreshCodebaseIndex(dirs);
704+
}
705+
}
655706
}

0 commit comments

Comments
 (0)