-
Notifications
You must be signed in to change notification settings - Fork 39.5k
Add basic github lexical search #312228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add basic github lexical search #312228
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -15,7 +15,8 @@ import './findTextInFilesTool'; | |||||
| import './getErrorsTool'; | ||||||
| import './getNotebookCellOutputTool'; | ||||||
| import './getSearchViewResultsTool'; | ||||||
| import './githubRepoTool'; | ||||||
| import './githubRepoSemanticSearchTool.tsx'; | ||||||
|
||||||
| import './githubRepoSemanticSearchTool.tsx'; | |
| import './githubRepoSemanticSearchTool'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,183 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /*--------------------------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Licensed under the MIT License. See License.txt in the project root for license information. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *--------------------------------------------------------------------------------------------*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as l10n from '@vscode/l10n'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BasePromptElementProps, PromptElement, PromptPiece, PromptReference, PromptSizing, TextChunk } from '@vscode/prompt-tsx'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type * as vscode from 'vscode'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { FileChunkAndScore } from '../../../platform/chunking/common/chunk'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { GithubRepoId } from '../../../platform/git/common/gitService'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { GithubCodeSearchScope, IGithubCodeSearchService } from '../../../platform/remoteCodeSearch/common/githubCodeSearchService'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createFencedCodeBlock, getLanguageId } from '../../../util/common/markdown'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { TelemetryCorrelationId } from '../../../util/common/telemetryCorrelationId'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isLocation, isUri } from '../../../util/common/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CancellationToken } from '../../../util/vs/base/common/cancellation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { URI } from '../../../util/vs/base/common/uri'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getUniqueReferences } from '../../prompt/common/conversation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ToolName } from '../common/toolNames'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface GithubTextSearchToolParams { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly query: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Either 'owner/repo' for a single repo, or an org name (no slash) */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly scope: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly maxResults?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class GithubTextSearchTool implements ICopilotTool<GithubTextSearchToolParams> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static readonly toolName = ToolName.GithubTextSearch; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @IInstantiationService private readonly _instantiationService: IInstantiationService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @IGithubCodeSearchService private readonly _githubCodeSearch: IGithubCodeSearchService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async invoke(options: vscode.LanguageModelToolInvocationOptions<GithubTextSearchToolParams>, token: CancellationToken): Promise<vscode.LanguageModelToolResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scope = parseScope(options.input.scope); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!scope) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(l10n.t`Invalid input. Could not parse 'scope' argument`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const maxResults = options.input.maxResults ?? 100; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const searchResults = await this._githubCodeSearch.lexicalSearch( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { silent: true }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scope, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.input.query, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxResults, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new TelemetryCorrelationId('github-text-search-tool'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const chunks = searchResults.chunks.map((entry): FileChunkAndScore => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let file = entry.file; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (file.scheme === 'githubRepoResult') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Path format: /owner/repo/relative/file/path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parts = file.path.split('/').filter(Boolean); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parts.length >= 3) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nwo = `${parts[0]}/${parts[1]}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const relativePath = parts.slice(2).join('/'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| file = URI.joinPath(URI.parse('https://github.com'), nwo, 'tree', 'main', '/' + relativePath).with({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| file = URI.joinPath(URI.parse('https://github.com'), nwo, 'tree', 'main', '/' + relativePath).with({ | |
| file = URI.joinPath(URI.parse('https://github.com'), nwo, 'tree', 'main', relativePath).with({ |
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tool hard-codes the branch name to main when building GitHub URLs. That will generate broken links for repos whose default branch is not main (common for master, release branches, or protected default branches). Consider using the html_url returned by GitHub's search API (or fetching the repo default branch once) instead of hardcoding main.
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lineInfo is always rendered as starting at line ${chunk.chunk.range.startLineNumber}. For lexical search results the range is currently new Range(0, 0, 0, 0), which will display “starting at line 0” (and also prevents fragments from being added). Consider omitting the line info (and fragment) when the range is unknown/0 so the output is not misleading.
| const lineInfo = ` starting at line ${chunk.chunk.range.startLineNumber}`; | |
| const headerText = nwoLabel | |
| ? `Text match excerpt from \`${nwoLabel}\` in \`${displayPath}\`${lineInfo}:` | |
| : `Text match excerpt in \`${displayPath}\`${lineInfo}:`; | |
| return <TextChunk> | |
| {headerText}<br /> | |
| {createFencedCodeBlock(getLanguageId(chunk.chunk.file), chunk.chunk.text)}<br /><br /> | |
| const hasKnownRange = chunk.chunk.range.startLineNumber > 0; | |
| const lineInfo = hasKnownRange ? ` starting at line ${chunk.chunk.range.startLineNumber}` : ''; | |
| const headerText = nwoLabel | |
| ? hasKnownRange | |
| ? `Text match excerpt from \`${nwoLabel}\` in \`${displayPath}\`${lineInfo}:` | |
| : `Text match from \`${nwoLabel}\` in \`${displayPath}\`:` | |
| : hasKnownRange | |
| ? `Text match excerpt in \`${displayPath}\`${lineInfo}:` | |
| : `Text match in \`${displayPath}\`:`; | |
| return <TextChunk> | |
| {headerText}<br /> | |
| {hasKnownRange ? <>{createFencedCodeBlock(getLanguageId(chunk.chunk.file), chunk.chunk.text)}<br /></> : undefined} | |
| <br /> |
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
references (and therefore toolResultMessage) is derived only from chunks with non-empty chunk.text because of .filter(x => x.chunk.text). But parseLexicalSearchResponse can produce isFullFile: true entries with text: '' when the API omits text_matches, which would make the tool report “no results” even though files matched. Consider rendering (or at least referencing) those file-only matches so the tool result accurately reflects hits.
| const renderedChunks = this.props.chunks | |
| .filter(x => x.chunk.text) | |
| .map(chunk => { | |
| const fileKey = chunk.chunk.file.toString(); | |
| if (!seenFiles.has(fileKey)) { | |
| seenFiles.add(fileKey); | |
| references.push(new PromptReference(chunk.chunk.file)); | |
| } | |
| const githubInfo = parseGithubFileUrl(chunk.chunk.file); | |
| const displayPath = githubInfo?.path ?? chunk.chunk.file.toString(); | |
| const nwoLabel = githubInfo?.nwo; | |
| const lineInfo = ` starting at line ${chunk.chunk.range.startLineNumber}`; | |
| const headerText = nwoLabel | |
| ? `Text match excerpt from \`${nwoLabel}\` in \`${displayPath}\`${lineInfo}:` | |
| : `Text match excerpt in \`${displayPath}\`${lineInfo}:`; | |
| return <TextChunk> | |
| {headerText}<br /> | |
| {createFencedCodeBlock(getLanguageId(chunk.chunk.file), chunk.chunk.text)}<br /><br /> | |
| </TextChunk>; | |
| }); | |
| const renderedChunks = this.props.chunks.map(chunk => { | |
| const fileKey = chunk.chunk.file.toString(); | |
| if (!seenFiles.has(fileKey)) { | |
| seenFiles.add(fileKey); | |
| references.push(new PromptReference(chunk.chunk.file)); | |
| } | |
| const githubInfo = parseGithubFileUrl(chunk.chunk.file); | |
| const displayPath = githubInfo?.path ?? chunk.chunk.file.toString(); | |
| const nwoLabel = githubInfo?.nwo; | |
| if (!chunk.chunk.text) { | |
| if (!chunk.chunk.isFullFile) { | |
| return undefined; | |
| } | |
| const headerText = nwoLabel | |
| ? l10n.t("Match found in `{0}` in `{1}`. GitHub did not provide a text excerpt.", nwoLabel, displayPath) | |
| : l10n.t("Match found in `{0}`. GitHub did not provide a text excerpt.", displayPath); | |
| return <TextChunk> | |
| {headerText}<br /><br /> | |
| </TextChunk>; | |
| } | |
| const lineInfo = ` starting at line ${chunk.chunk.range.startLineNumber}`; | |
| const headerText = nwoLabel | |
| ? `Text match excerpt from \`${nwoLabel}\` in \`${displayPath}\`${lineInfo}:` | |
| : `Text match excerpt in \`${displayPath}\`${lineInfo}:`; | |
| return <TextChunk> | |
| {headerText}<br /> | |
| {createFencedCodeBlock(getLanguageId(chunk.chunk.file), chunk.chunk.text)}<br /><br /> | |
| </TextChunk>; | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User-facing text: “Semantic Search a GitHub repository …” is grammatically incorrect. Consider changing it to “Semantically search a GitHub repository …” (same applies to the userDescription).