Skip to content

Commit 196177b

Browse files
committed
add support for docs via config
1 parent 7f0cf7d commit 196177b

File tree

12 files changed

+144
-32
lines changed

12 files changed

+144
-32
lines changed

core/context/providers/DocsContextProvider.ts

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ContextProviderExtras,
66
ContextSubmenuItem,
77
LoadSubmenuItemsArgs,
8+
SiteIndexingConfig,
89
} from "../../index.js";
910
import { DocsService } from "../../indexing/docs/DocsService.js";
1011
import configs from "../../indexing/docs/preIndexedDocs.js";
@@ -108,40 +109,44 @@ class DocsContextProvider extends BaseContextProvider {
108109
];
109110
}
110111

111-
async loadSubmenuItems(
112-
args: LoadSubmenuItemsArgs,
113-
): Promise<ContextSubmenuItem[]> {
114-
const docs = await this.docsService.list();
115-
const submenuItems: ContextSubmenuItem[] = docs.map((doc) => ({
112+
// Get combined site configs from preIndexedDocs and options.sites.
113+
private _getDocsSitesConfig(): SiteIndexingConfig[] {
114+
return [...configs, ...(this.options?.sites || [])];
115+
}
116+
117+
// Get indexed docs as ContextSubmenuItems from database.
118+
private async _getIndexedDocsContextSubmenuItems(): Promise<ContextSubmenuItem[]> {
119+
return (await this.docsService.list()).map((doc) => ({
116120
title: doc.title,
117121
description: new URL(doc.baseUrl).hostname,
118122
id: doc.baseUrl,
119-
metadata: {
120-
preIndexed: !!configs.find((config) => config.title === doc.title),
121-
},
122123
}));
124+
}
123125

124-
submenuItems.push(
125-
...configs
126-
// After it's actually downloaded, we don't want to show twice
127-
.filter(
128-
(config) => !submenuItems.some((item) => item.id === config.startUrl),
129-
)
130-
.map((config) => ({
131-
title: config.title,
132-
description: new URL(config.startUrl).hostname,
133-
id: config.startUrl,
134-
metadata: {
135-
preIndexed: true,
136-
},
137-
// iconUrl: config.faviconUrl,
138-
})),
139-
);
126+
async loadSubmenuItems(
127+
args: LoadSubmenuItemsArgs,
128+
): Promise<ContextSubmenuItem[]> {
129+
const submenuItemsMap = new Map<string, ContextSubmenuItem>();
130+
131+
for (const item of await this._getIndexedDocsContextSubmenuItems()) {
132+
submenuItemsMap.set(item.id, item);
133+
}
134+
135+
for (const config of this._getDocsSitesConfig()) {
136+
submenuItemsMap.set(config.startUrl, {
137+
id: config.startUrl,
138+
title: config.title,
139+
description: new URL(config.startUrl).hostname,
140+
metadata: { preIndexed: !!configs.find((cnf) => cnf.title === config.title), },
141+
});
142+
}
143+
144+
const submenuItems = Array.from(submenuItemsMap.values());
140145

141146
// Sort submenuItems such that the objects with titles which don't occur in configs occur first, and alphabetized
142147
submenuItems.sort((a, b) => {
143-
const aTitleInConfigs = a.metadata?.preIndexed;
144-
const bTitleInConfigs = b.metadata?.preIndexed;
148+
const aTitleInConfigs = a.metadata?.preIndexed ?? false;
149+
const bTitleInConfigs = b.metadata?.preIndexed ?? false;
145150

146151
// Primary criterion: Items not in configs come first
147152
if (!aTitleInConfigs && bTitleInConfigs) {

core/core.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,19 @@ export class Core {
242242
await this.docsService.delete(baseUrl);
243243
this.messenger.send("refreshSubmenuItems", undefined);
244244
});
245+
on("context/indexDocs", async (msg) => {
246+
const config = await this.config();
247+
const provider: any = config.contextProviders?.find(
248+
(provider) => provider.description.title === "docs",
249+
);
250+
251+
const siteIndexingOptions: SiteIndexingConfig[] = provider ?
252+
(mProvider => mProvider?.options?.sites || [])({ ...provider }) :
253+
[];
254+
255+
await this.indexDocs(siteIndexingOptions, msg.data.reIndex);
256+
this.ide.infoPopup("Docs indexing completed");
257+
});
245258
on("context/loadSubmenuItems", async (msg) => {
246259
const config = await this.config();
247260
const items = config.contextProviders
@@ -623,4 +636,16 @@ export class Core {
623636
this.indexingState = update;
624637
}
625638
}
639+
640+
private async indexDocs(sites: SiteIndexingConfig[], reIndex: boolean): Promise<void> {
641+
for (const site of sites) {
642+
for await (const update of this.docsService.indexAndAdd(site, new TransformersJsEmbeddingsProvider(), reIndex)) {
643+
// Temporary disabled posting progress updates to the UI due to
644+
// possible collision with code indexing progress updates.
645+
646+
// this.messenger.request("indexProgress", update);
647+
// this.indexingState = update;
648+
}
649+
}
650+
}
626651
}

core/indexing/docs/DocsService.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class DocsService {
3030
private static instance: DocsService;
3131
private static DOCS_TABLE_NAME = "docs";
3232
private _sqliteTable: Database | undefined;
33+
private docsIndexingQueue: Set<string> = new Set();
3334

3435
public static getInstance(): DocsService {
3536
if (!DocsService.instance) {
@@ -204,10 +205,16 @@ export class DocsService {
204205
async *indexAndAdd(
205206
siteIndexingConfig: SiteIndexingConfig,
206207
embeddingsProvider: EmbeddingsProvider,
208+
reIndex: boolean = false,
207209
): AsyncGenerator<IndexingProgressUpdate> {
208-
const startUrl = new URL(siteIndexingConfig.startUrl);
210+
const startUrl = new URL(siteIndexingConfig.startUrl.toString());
209211

210-
if (await this.has(siteIndexingConfig.startUrl.toString())) {
212+
if (this.docsIndexingQueue.has(startUrl.toString())) {
213+
console.log("Already in queue");
214+
return;
215+
}
216+
217+
if (!reIndex && await this.has(startUrl.toString())) {
211218
yield {
212219
progress: 1,
213220
desc: "Already indexed",
@@ -216,38 +223,53 @@ export class DocsService {
216223
return;
217224
}
218225

226+
// Mark the site as currently being indexed
227+
this.docsIndexingQueue.add(startUrl.toString());
228+
219229
yield {
220230
progress: 0,
221231
desc: "Finding subpages",
222232
status: "indexing",
223233
};
224234

225235
const articles: Article[] = [];
236+
let processedPages = 0;
237+
let maxKnownPages = 1;
226238

227239
// Crawl pages and retrieve info as articles
228240
for await (const page of crawlPage(startUrl, siteIndexingConfig.maxDepth)) {
241+
processedPages++;
229242
const article = pageToArticle(page);
230243
if (!article) {
231244
continue;
232245
}
233246
articles.push(article);
234247

248+
// Use a heuristic approach for progress calculation
249+
const progress = Math.min(processedPages / maxKnownPages, 1);
250+
235251
yield {
236-
progress: 0,
252+
progress, // Yield the heuristic progress
237253
desc: `Finding subpages (${page.path})`,
238254
status: "indexing",
239255
};
256+
257+
// Increase maxKnownPages to delay progress reaching 100% too soon
258+
if (processedPages === maxKnownPages) {
259+
maxKnownPages *= 2;
260+
}
240261
}
241262

242263
const chunks: Chunk[] = [];
243264
const embeddings: number[][] = [];
244265

245266
// Create embeddings of retrieved articles
246267
console.log("Creating Embeddings for ", articles.length, " articles");
247-
for (const article of articles) {
268+
for (let i = 0; i < articles.length; i++) {
269+
const article = articles[i];
248270
yield {
249-
progress: Math.max(1, Math.floor(100 / (articles.length + 1))),
250-
desc: `${article.subpath}`,
271+
progress: i / articles.length,
272+
desc: `Creating Embeddings: ${article.subpath}`,
251273
status: "indexing",
252274
};
253275

@@ -268,7 +290,14 @@ export class DocsService {
268290

269291
// Add docs to databases
270292
console.log("Adding ", embeddings.length, " embeddings to db");
293+
yield {
294+
progress: 0.5,
295+
desc: `Adding ${embeddings.length} embeddings to db`,
296+
status: "indexing",
297+
};
298+
271299
await this.add(siteIndexingConfig.title, startUrl, chunks, embeddings);
300+
this.docsIndexingQueue.delete(startUrl.toString());
272301

273302
yield {
274303
progress: 1,

core/protocol/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export type ToCoreFromIdeOrWebviewProtocol = {
6868
"autocomplete/complete": [AutocompleteInput, string[]];
6969
"context/addDocs": [SiteIndexingConfig, void];
7070
"context/removeDocs": [{ baseUrl: string }, void];
71+
"context/indexDocs": [{ reIndex: boolean }, void];
7172
"autocomplete/cancel": [undefined, void];
7273
"autocomplete/accept": [{ completionId: string }, void];
7374
"command/run": [

core/protocol/passThrough.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
2525
"context/loadSubmenuItems",
2626
"context/addDocs",
2727
"context/removeDocs",
28+
"context/indexDocs",
2829
"autocomplete/complete",
2930
"autocomplete/cancel",
3031
"autocomplete/accept",

docs/docs/customization/context-providers.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,28 @@ Type `@docs` to index and retrieve snippets from any documentation site. You can
5050

5151
Continue also pre-indexes a number of common sites, listed [here](https://github.com/continuedev/continue/blob/main/core/indexing/docs/preIndexedDocs.ts). The embeddings for these sites are hosted by us, but downloaded for local use after the first time. All other indexing occurs entirely locally.
5252

53+
#### Adding a Documentation Site via Configuration
54+
55+
To add a documentation site via configuration, update the `config.json` file as follows:
56+
57+
```json
58+
{
59+
"name": "docs",
60+
"params": {
61+
"sites": [
62+
{
63+
"title": "ExampleDocs",
64+
"startUrl": "https://exampledocs.com/docs",
65+
"rootUrl": "https://exampledocs.com",
66+
"maxDepth": 3 // Default
67+
}
68+
]
69+
}
70+
}
71+
```
72+
73+
Then run the docs indexing command: VS Code: `cmd/ctrl + shift + p`, then select `Continue: Index Docs`
74+
5375
### Open Files
5476

5577
Type '@open' to reference the contents of all of your open files. Set `onlyPinned` to `true` to only reference pinned files.

extensions/vscode/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@
229229
"category": "Continue",
230230
"title": "Fix Grammar / Spelling",
231231
"group": "Continue"
232+
},
233+
{
234+
"command": "continue.docsIndex",
235+
"category": "Continue",
236+
"title": "Index Docs",
237+
"group": "Continue"
232238
}
233239
],
234240
"keybindings": [

extensions/vscode/src/commands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { VerticalPerLineDiffManager } from "./diff/verticalPerLine/manager";
2525
import { QuickEdit } from "./quickPicks/QuickEdit";
2626
import { Battery } from "./util/battery";
2727
import type { VsCodeWebviewProtocol } from "./webviewProtocol";
28+
import { Core } from "core/core";
2829

2930
let fullScreenPanel: vscode.WebviewPanel | undefined;
3031

@@ -169,6 +170,7 @@ const commandsMap: (
169170
verticalDiffManager: VerticalPerLineDiffManager,
170171
continueServerClientPromise: Promise<ContinueServerClient>,
171172
battery: Battery,
173+
core: Core,
172174
) => { [command: string]: (...args: any) => any } = (
173175
ide,
174176
extensionContext,
@@ -178,6 +180,7 @@ const commandsMap: (
178180
verticalDiffManager,
179181
continueServerClientPromise,
180182
battery,
183+
core
181184
) => {
182185
/**
183186
* Streams an inline edit to the vertical diff manager.
@@ -307,6 +310,9 @@ const commandsMap: (
307310
"continue.toggleAuxiliaryBar": () => {
308311
vscode.commands.executeCommand("workbench.action.toggleAuxiliaryBar");
309312
},
313+
"continue.docsIndex": async () => {
314+
core.invoke("context/indexDocs", {reIndex: false});
315+
},
310316
"continue.focusContinueInput": async () => {
311317
const fullScreenTab = getFullScreenTab();
312318
if (!fullScreenTab) {
@@ -690,6 +696,7 @@ export function registerAllCommands(
690696
verticalDiffManager: VerticalPerLineDiffManager,
691697
continueServerClientPromise: Promise<ContinueServerClient>,
692698
battery: Battery,
699+
core: Core,
693700
) {
694701
for (const [command, callback] of Object.entries(
695702
commandsMap(
@@ -701,6 +708,7 @@ export function registerAllCommands(
701708
verticalDiffManager,
702709
continueServerClientPromise,
703710
battery,
711+
core
704712
),
705713
)) {
706714
context.subscriptions.push(

extensions/vscode/src/extension/VsCodeExtension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class VsCodeExtension {
190190
this.verticalDiffManager,
191191
this.core.continueServerClientPromise,
192192
this.battery,
193+
this.core
193194
);
194195

195196
registerDebugTracker(this.sidebar.webviewProtocol, this.ide);

gui/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"devDependencies": {
6767
"@swc/cli": "^0.3.12",
6868
"@swc/core": "^1.5.25",
69+
"@types/lodash": "^4.17.6",
6970
"@types/node": "^20.5.6",
7071
"@types/node-fetch": "^2.6.4",
7172
"@types/react": "^18.0.27",

gui/src/hooks/useSetup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { isJetBrains } from "../util";
1616
import { setLocalStorage } from "../util/localStorage";
1717
import useChatHandler from "./useChatHandler";
1818
import { useWebviewListener } from "./useWebviewListener";
19+
import { debounce } from "lodash";
1920

2021
function useSetup(dispatch: Dispatch<any>) {
2122
const [configLoaded, setConfigLoaded] = useState<boolean>(false);
@@ -86,8 +87,13 @@ function useSetup(dispatch: Dispatch<any>) {
8687
});
8788
});
8889

90+
const debouncedIndexDocs = debounce(() => {
91+
ideMessenger.request("context/indexDocs", {reIndex: false});
92+
}, 1000);
93+
8994
useWebviewListener("configUpdate", async () => {
9095
loadConfig();
96+
debouncedIndexDocs();
9197
});
9298

9399
useWebviewListener("submitMessage", async (data) => {

0 commit comments

Comments
 (0)