From 079f7d38857d16e880f1fe38f83a148f0e6f1470 Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Sun, 26 Jan 2025 16:32:29 +0800 Subject: [PATCH 1/5] fix(eclipse): skip version check for non-semantic version string. (#3762) --- clients/eclipse/feature/feature.xml | 4 +- clients/eclipse/plugin/META-INF/MANIFEST.MF | 2 +- .../com/tabbyml/tabby4eclipse/Version.java | 4 ++ .../tabby4eclipse/chat/ChatViewUtils.java | 38 ++++++++++--------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/clients/eclipse/feature/feature.xml b/clients/eclipse/feature/feature.xml index 6555cd4f7543..658aaa6be516 100644 --- a/clients/eclipse/feature/feature.xml +++ b/clients/eclipse/feature/feature.xml @@ -2,7 +2,7 @@ @@ -19,6 +19,6 @@ + version="0.0.2.32"/> diff --git a/clients/eclipse/plugin/META-INF/MANIFEST.MF b/clients/eclipse/plugin/META-INF/MANIFEST.MF index 9c9e3de5e32c..ba56a202848b 100644 --- a/clients/eclipse/plugin/META-INF/MANIFEST.MF +++ b/clients/eclipse/plugin/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Tabby Plugin for Eclipse Bundle-SymbolicName: com.tabbyml.tabby4eclipse;singleton:=true -Bundle-Version: 0.0.2.31 +Bundle-Version: 0.0.2.32 Bundle-Activator: com.tabbyml.tabby4eclipse.Activator Bundle-Vendor: com.tabbyml Require-Bundle: org.eclipse.ui, diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Version.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Version.java index 58cba2420a50..c567a1dd9860 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Version.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/Version.java @@ -67,6 +67,10 @@ public boolean isEqual(Version other, boolean ignorePatch) { return this.patch == other.patch; } + public boolean isZero() { + return this.major == 0 && this.minor == 0 && this.patch == 0; + } + private int parseInt(String str) { try { return Integer.parseInt(str); diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java index f3992776b9ec..bbe763fc2b34 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java @@ -88,7 +88,7 @@ public static String checkServerHealth(Map serverHealth) { if (version != null) { Version parsedVersion = new Version(version); Version requiredVersion = new Version(MIN_SERVER_VERSION); - if (!parsedVersion.isGreaterOrEqualThan(requiredVersion)) { + if (!parsedVersion.isZero() && !parsedVersion.isGreaterOrEqualThan(requiredVersion)) { return String.format( "Tabby Chat requires Tabby server version %s or later. Your server is running version %s.", MIN_SERVER_VERSION, version); @@ -212,24 +212,25 @@ public static boolean openInEditor(FileLocation fileLocation) { public static void openExternal(String url) { Program.launch(url); } - + public static List readGitRepositoriesInWorkspace() { List repositories = new ArrayList<>(); - IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - IProject[] projects = workspaceRoot.getProjects(); - - for (IProject project : projects) { - try { - URI projectRootUri = project.getLocation().toFile().toURI(); - com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository repo = GitProvider.getInstance().getRepository(new GitRepositoryParams(projectRootUri.toString())); - if (repo != null) { - repositories.add(new GitRepository(repo.getRemoteUrl())); - } - } catch (Exception e) { - logger.warn("Error when read git repository.", e); - } - } - return repositories; + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IProject[] projects = workspaceRoot.getProjects(); + + for (IProject project : projects) { + try { + URI projectRootUri = project.getLocation().toFile().toURI(); + com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository repo = GitProvider.getInstance() + .getRepository(new GitRepositoryParams(projectRootUri.toString())); + if (repo != null) { + repositories.add(new GitRepository(repo.getRemoteUrl())); + } + } catch (Exception e) { + logger.warn("Error when read git repository.", e); + } + } + return repositories; } public static void setClipboardContent(String content) { @@ -261,7 +262,8 @@ public static void applyContentInEditor(String content) { public static Filepath fileUriToChatPanelFilepath(URI fileUri) { String fileUriString = fileUri.toString(); - com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository gitRepo = GitProvider.getInstance().getRepository(new GitRepositoryParams(fileUriString)); + com.tabbyml.tabby4eclipse.lsp.protocol.GitRepository gitRepo = GitProvider.getInstance() + .getRepository(new GitRepositoryParams(fileUriString)); String gitUrl = (gitRepo != null) ? gitRepo.getRemoteUrl() : null; if (gitUrl != null) { gitRemoteUrlToLocalRoot.put(gitUrl, gitRepo.getRoot()); From ae5aae3aa211a15b070b3ae93eac3e9ffc54df4a Mon Sep 17 00:00:00 2001 From: Wei Zhang Date: Sun, 26 Jan 2025 17:07:21 +0800 Subject: [PATCH 2/5] =?UTF-8?q?chore(build):=20temporarily=20disable=20the?= =?UTF-8?q?=20tags=20pattern=20match=20to=20resolve=20a=E2=80=A6=20(#3761)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(build): temporarily disable the tags pattern match to resolve a version issue on Windows Signed-off-by: Wei Zhang * chore: update comment to point to upstream issue Signed-off-by: Wei Zhang --------- Signed-off-by: Wei Zhang --- crates/tabby/build.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/tabby/build.rs b/crates/tabby/build.rs index c65d7bf1fa24..34c22d7bced2 100644 --- a/crates/tabby/build.rs +++ b/crates/tabby/build.rs @@ -7,7 +7,10 @@ fn main() -> Result<(), Box> { EmitBuilder::builder() .all_build() .all_git() - .git_describe(false, true, Some("v*")) + // TODO(kweizh): we encounter a issue with match_pattern in vergen on Windows + // will add the match_pattern back when the issue is resolved + // ref: https://github.com/rustyhorde/vergen/issues/402 + .git_describe(false, true, None) .emit()?; Ok(()) } From de45082d019ab06bf333d4fbdc3446ea0f992b63 Mon Sep 17 00:00:00 2001 From: aliang Date: Sun, 26 Jan 2025 17:51:18 +0800 Subject: [PATCH 3/5] chore(ui): optimize styling of chat mention tags (#3758) * chore(ui): optimize styling of chat mention tags * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- ee/tabby-ui/components/chat/chat-panel.tsx | 85 +++--- .../components/chat/form-editor/mention.tsx | 30 ++- .../components/chat/form-editor/types.ts | 1 + .../components/chat/form-editor/utils.tsx | 15 +- ee/tabby-ui/components/chat/prompt-form.tsx | 4 +- ee/tabby-ui/components/chat/repo-select.tsx | 242 +++++++++--------- 6 files changed, 195 insertions(+), 182 deletions(-) diff --git a/ee/tabby-ui/components/chat/chat-panel.tsx b/ee/tabby-ui/components/chat/chat-panel.tsx index 0583524b0966..e0a009e0c55e 100644 --- a/ee/tabby-ui/components/chat/chat-panel.tsx +++ b/ee/tabby-ui/components/chat/chat-panel.tsx @@ -287,55 +287,46 @@ function ChatPanelRenderer( className="border-t bg-background px-4 py-2 shadow-lg sm:space-y-4 sm:rounded-t-xl sm:border md:py-4" >
- - - {activeSelection ? ( - + {activeSelection ? ( + + + + + Current file + + - - - ) : null} + {enableActiveSelection ? : } + + + ) : null} + {relevantContext.map((item, idx) => { // `git_url + filepath + range` as unique key const key = `${item.git_url}_${item.filepath}_${item.range?.start}_${item.range?.end}` diff --git a/ee/tabby-ui/components/chat/form-editor/mention.tsx b/ee/tabby-ui/components/chat/form-editor/mention.tsx index 84e3e533b27c..5a958f23632c 100644 --- a/ee/tabby-ui/components/chat/form-editor/mention.tsx +++ b/ee/tabby-ui/components/chat/form-editor/mention.tsx @@ -4,12 +4,14 @@ import { useEffect, useImperativeHandle, useLayoutEffect, + useMemo, useRef, useState } from 'react' import Mention from '@tiptap/extension-mention' import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion' +import { uniqBy } from 'lodash-es' import { Filepath, ListFileItem, @@ -21,7 +23,7 @@ import { IconFile } from '@/components/ui/icons' import { emitter } from '../event-emitter' import type { SourceItem } from './types' -import { fileItemToSourceItem, shortenLabel } from './utils' +import { fileItemToSourceItem } from './utils' /** * A React component to render a mention node in the editor. @@ -41,17 +43,15 @@ export const MentionComponent = ({ node }: { node: any }) => { }, []) return ( - + - - + + {resolveFileNameForDisplay(filepathString)} @@ -161,7 +161,7 @@ export const MentionList = forwardRef( if (!listFileInWorkspace) return [] const files = await listFileInWorkspace({ query }) const result = files?.map(fileItemToSourceItem) || [] - setItems(result) + setItems(uniqBy(result, 'id')) } fetchOptions() }, [query]) @@ -205,7 +205,7 @@ export const MentionList = forwardRef( key={`${JSON.stringify(filepath)}`} onClick={() => handleSelectItem(index)} onMouseEnter={() => setSelectedIndex(index)} - title={item.name} + title={item.filepath} data={item} isSelected={index === selectedIndex} /> @@ -226,6 +226,10 @@ interface OptionItemView extends HTMLAttributes { } function OptionItemView({ isSelected, data, ...rest }: OptionItemView) { const ref = useRef(null) + const filepathWithoutFilename = useMemo(() => { + return data.filepath.split('/').slice(0, -1).join('/') + }, [data.filepath]) + useLayoutEffect(() => { if (isSelected && ref.current) { ref.current?.scrollIntoView({ @@ -238,7 +242,7 @@ function OptionItemView({ isSelected, data, ...rest }: OptionItemView) { return (
- {shortenLabel(data.name)} - - {shortenLabel(data.filepath, 20)} + {data.name} + + {filepathWithoutFilename}
) diff --git a/ee/tabby-ui/components/chat/form-editor/types.ts b/ee/tabby-ui/components/chat/form-editor/types.ts index d6c4be4fb619..b847e4bf725e 100644 --- a/ee/tabby-ui/components/chat/form-editor/types.ts +++ b/ee/tabby-ui/components/chat/form-editor/types.ts @@ -45,6 +45,7 @@ export type FileItem = ListFileItem * Represents a file source item for the mention suggestion list. */ export interface SourceItem { + id: string name: string filepath: string category: 'file' diff --git a/ee/tabby-ui/components/chat/form-editor/utils.tsx b/ee/tabby-ui/components/chat/form-editor/utils.tsx index 6b477eacaac3..7c41911fbcb6 100644 --- a/ee/tabby-ui/components/chat/form-editor/utils.tsx +++ b/ee/tabby-ui/components/chat/form-editor/utils.tsx @@ -4,7 +4,7 @@ import { Filepath } from 'tabby-chat-panel/index' import { PLACEHOLDER_FILE_REGEX } from '@/lib/constants/regex' import { FileContext } from '@/lib/types' -import { convertFilepath, resolveFileNameForDisplay } from '@/lib/utils' +import { convertFilepath, nanoid, resolveFileNameForDisplay } from '@/lib/utils' import { FileItem, SourceItem } from './types' @@ -13,12 +13,23 @@ import { FileItem, SourceItem } from './types' */ export function fileItemToSourceItem(info: FileItem): SourceItem { const filepathString = convertFilepath(info.filepath).filepath - return { + const source: Omit = { fileItem: info, name: resolveFileNameForDisplay(filepathString), // Extract the last segment of the path as the name filepath: filepathString, category: 'file' } + try { + return { + id: JSON.stringify(info.filepath), + ...source + } + } catch (e) { + return { + id: nanoid(), + ...source + } + } } /** diff --git a/ee/tabby-ui/components/chat/prompt-form.tsx b/ee/tabby-ui/components/chat/prompt-form.tsx index 8d592af6c14e..75c3ac4bad04 100644 --- a/ee/tabby-ui/components/chat/prompt-form.tsx +++ b/ee/tabby-ui/components/chat/prompt-form.tsx @@ -21,7 +21,7 @@ import { import './prompt-form.css' import { EditorState } from '@tiptap/pm/state' -import { isEqual } from 'lodash-es' +import { isEqual, uniqBy } from 'lodash-es' import { EditorFileContext } from 'tabby-chat-panel/index' import tippy, { GetReferenceClientRect, Instance } from 'tippy.js' @@ -112,7 +112,7 @@ function PromptFormRenderer( items: async ({ query }) => { if (!listFileInWorkspace) return [] const files = await listFileInWorkspace({ query }) - return files?.map(fileItemToSourceItem) || [] + return uniqBy(files?.map(fileItemToSourceItem) || [], 'id') }, render: () => { let component: ReactRenderer diff --git a/ee/tabby-ui/components/chat/repo-select.tsx b/ee/tabby-ui/components/chat/repo-select.tsx index 1ed9035876a9..61474cc7eaf5 100644 --- a/ee/tabby-ui/components/chat/repo-select.tsx +++ b/ee/tabby-ui/components/chat/repo-select.tsx @@ -28,7 +28,8 @@ import LoadingWrapper from '@/components/loading-wrapper' import { SourceIcon } from '../source-icon' -interface RepoSelectProps { +interface RepoSelectProps + extends Omit, 'onChange'> { repos: RepositorySourceListQuery['repositoryList'] | undefined value: string | undefined onChange: (v: string | undefined) => void @@ -39,7 +40,8 @@ export function RepoSelect({ repos, value, onChange, - isInitializing + isInitializing, + ...rest }: RepoSelectProps) { const [open, setOpen] = useState(false) const commandListRef = useRef(null) @@ -69,125 +71,129 @@ export function RepoSelect({ if (!isInitializing && !repos?.length) return null return ( - - -
- } - > - - - -
- {selectedRepo ? ( - - ) : ( - - )} -
- - {selectedRepoName || 'Workspace'} - -
- {!value && ( -
- +
+ + +
+ } + > + + + +
+ {selectedRepo ? ( + + ) : ( + + )} +
+ + {selectedRepoName || 'Workspace'} +
- )} -
-
- {!!value && ( - - )} -
- - - - - No context found - - {repos?.map(repo => { - const isSelected = repo.sourceId === value + {!value && ( +
+ +
+ )} +
+ + {!!value && ( + + )} + + + + + + No context found + + {repos?.map(repo => { + const isSelected = repo.sourceId === value - return ( - { - onSelectRepo(repo.sourceId) - setOpen(false) - }} - title={repo.sourceName} - className="cursor-pointer" - > - -
- { + onSelectRepo(repo.sourceId) + setOpen(false) + }} + title={repo.sourceName} + className="cursor-pointer" + > + -
- {repo.sourceName} +
+ +
+ {repo.sourceName} +
-
- - ) - })} - - - - - - + + ) + })} + + + + + + +
) } From 78fd2e57be4d88077900157a07eed7ed96f8967c Mon Sep 17 00:00:00 2001 From: aliang Date: Sun, 26 Jan 2025 18:11:21 +0800 Subject: [PATCH 4/5] fix(ui): correct the placeholder of chat panel mention function (#3763) --- ee/tabby-ui/components/chat/form-editor/mention.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/tabby-ui/components/chat/form-editor/mention.tsx b/ee/tabby-ui/components/chat/form-editor/mention.tsx index 5a958f23632c..5ff1bbb7da44 100644 --- a/ee/tabby-ui/components/chat/form-editor/mention.tsx +++ b/ee/tabby-ui/components/chat/form-editor/mention.tsx @@ -194,7 +194,7 @@ export const MentionList = forwardRef( {!items.length ? (
{/* If no items are found, show a message. */} - {query ? 'No results found' : 'Typing to search...'} + {query ? 'No results found' : 'Type to search...'}
) : (
From 889750601da93e229ccdd23ddf6fc5f754cd677d Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Mon, 27 Jan 2025 16:38:32 +0800 Subject: [PATCH 5/5] fix(vscode): exclude ignored files for chat panel at file list. (#3765) * fix(vscode): exclude ignored files for chat panel @file list. * fix: handle emplty gitignore line. * fix: typo in comments. --- clients/vscode/src/chat/webview.ts | 5 +- clients/vscode/src/findFiles.ts | 183 +++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 clients/vscode/src/findFiles.ts diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts index 46f6ef5569fd..3cb76cc908bd 100644 --- a/clients/vscode/src/chat/webview.ts +++ b/clients/vscode/src/chat/webview.ts @@ -51,6 +51,7 @@ import { localUriToListFileItem, escapeGlobPattern, } from "./utils"; +import { findFiles } from "../findFiles"; import mainHtml from "./html/main.html"; import errorHtml from "./html/error.html"; @@ -529,10 +530,8 @@ export class ChatWebview { .join(""); const globPattern = `**/${caseInsensitivePattern}*`; - this.logger.info(`Searching files with pattern: ${globPattern}, limit: ${maxResults}`); - - const files = await workspace.findFiles(globPattern, undefined, maxResults); + const files = await findFiles(globPattern, { maxResults }); this.logger.info(`Found ${files.length} files.`); return files.map((uri) => localUriToListFileItem(uri, this.gitProvider)); } catch (error) { diff --git a/clients/vscode/src/findFiles.ts b/clients/vscode/src/findFiles.ts new file mode 100644 index 000000000000..b1f5b9c44a5b --- /dev/null +++ b/clients/vscode/src/findFiles.ts @@ -0,0 +1,183 @@ +// We want to use the findFiles2 API, but it is still in the proposed state. +// Therefore, we implement a findFiles function here that can use the following rules in the exclude pattern: +// 1. .gitignore files +// 2. Settings from `files.exclude` and `search.exclude` +// +// See https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts + +import { GlobPattern, RelativePattern, Uri, WorkspaceFolder, CancellationToken, workspace } from "vscode"; +import path from "path"; +import { getLogger } from "./logger"; + +const logger = getLogger("FindFiles"); + +// Map from workspace folder to gitignore patterns +const gitIgnorePatternsMap = new Map>(); + +function gitIgnoreItemToExcludePatterns(item: string, prefix?: string | undefined): string[] { + let pattern = item.trim(); + if (pattern.length === 0) { + return []; + } + if (pattern.indexOf("/") === -1 || pattern.indexOf("/") === pattern.length - 1) { + if (!pattern.startsWith("**/")) { + pattern = `**/${pattern}`; + } + } else if (pattern.startsWith("/")) { + pattern = pattern.slice(1); + } + return [path.join(prefix ?? "", pattern), path.join(prefix ?? "", pattern, "/**")]; +} + +async function buildGitIgnorePatterns(workspaceFolder: WorkspaceFolder) { + const patterns = new Set(); + logger.debug(`Building gitignore patterns for workspace folder: ${workspaceFolder.uri.toString()}`); + + // Read parent gitignore files + let current = workspaceFolder.uri; + let parent = current.with({ path: path.dirname(current.path) }); + while (parent.path !== current.path) { + const gitignore = parent.with({ path: path.join(parent.path, ".gitignore") }); + try { + const content = (await workspace.fs.readFile(gitignore)).toString(); + content.split(/\r?\n/).forEach((line) => { + if (!line.trim().startsWith("#")) { + gitIgnoreItemToExcludePatterns(line).forEach((pattern) => patterns.add(pattern)); + } + }); + } catch (error) { + // ignore + } + + // next + current = parent; + parent = current.with({ path: path.dirname(current.path) }); + } + + // Read subdirectories gitignore files + const ignoreFiles = await workspace.findFiles(new RelativePattern(workspaceFolder, "**/.gitignore")); + await Promise.all( + ignoreFiles.map(async (ignoreFile) => { + const prefix = path.relative(workspaceFolder.uri.path, path.dirname(ignoreFile.path)); + try { + const content = (await workspace.fs.readFile(ignoreFile)).toString(); + content.split(/\r?\n/).forEach((line) => { + if (!line.trim().startsWith("#")) { + gitIgnoreItemToExcludePatterns(line, prefix).forEach((pattern) => patterns.add(pattern)); + } + }); + } catch (error) { + // ignore + } + }), + ); + // Update map + logger.debug( + `Completed building git ignore patterns for workspace folder: ${workspaceFolder.uri.toString()}, git ignore patterns: ${JSON.stringify([...patterns])}`, + ); + gitIgnorePatternsMap.set(workspaceFolder.uri.toString(), patterns); +} + +workspace.onDidChangeTextDocument(async (event) => { + const uri = event.document.uri; + if (path.basename(uri.fsPath) === ".gitignore") { + const workspaceFolder = workspace.getWorkspaceFolder(uri); + if (workspaceFolder) { + await buildGitIgnorePatterns(workspaceFolder); + } + } +}); + +export async function findFiles( + pattern: GlobPattern, + options?: { + excludes?: string[]; + noUserSettings?: boolean; // User settings is used by default, set to true to skip user settings + noIgnoreFiles?: boolean; // .gitignore files are used by default, set to true to skip .gitignore files + maxResults?: number; + token?: CancellationToken; + }, +): Promise { + const combinedExcludes = new Set(); + if (options?.excludes) { + for (const exclude of options.excludes) { + combinedExcludes.add(exclude); + } + } + if (!options?.noUserSettings) { + const searchExclude: Record = + (await workspace.getConfiguration("search", null).get("exclude")) ?? {}; + const filesExclude: Record = + (await workspace.getConfiguration("files", null).get("exclude")) ?? {}; + for (const pattern in { ...searchExclude, ...filesExclude }) { + if (filesExclude[pattern]) { + combinedExcludes.add(pattern); + } + } + } + if (options?.noIgnoreFiles) { + const excludesPattern = `{${[...combinedExcludes].join(",")}}`; + logger.debug( + `Executing search: ${JSON.stringify({ includePattern: pattern, excludesPattern, maxResults: options?.maxResults, token: options?.token })}`, + ); + return await workspace.findFiles(pattern, excludesPattern, options.maxResults, options.token); + } else { + await Promise.all( + workspace.workspaceFolders?.map(async (workspaceFolder) => { + if (!gitIgnorePatternsMap.has(workspaceFolder.uri.toString())) { + await buildGitIgnorePatterns(workspaceFolder); + } + }) ?? [], + ); + + return new Promise((resolve, reject) => { + if (options?.token) { + options?.token.onCancellationRequested((reason) => { + reject(reason); + }); + } + + const allSearches = + workspace.workspaceFolders?.map(async (workspaceFolder) => { + let includePattern: RelativePattern; + if (typeof pattern === "string") { + includePattern = new RelativePattern(workspaceFolder, pattern); + } else { + if (pattern.baseUri.toString().startsWith(workspaceFolder.uri.toString())) { + includePattern = pattern; + } else { + return []; + } + } + const allExcludes = new Set([ + ...combinedExcludes, + ...(gitIgnorePatternsMap.get(workspaceFolder.uri.toString()) ?? []), + ]); + const excludesPattern = `{${[...allExcludes].slice(0, 1000).join(",")}}`; // Limit to 1000 patterns + logger.debug( + `Executing search: ${JSON.stringify({ includePattern, excludesPattern, maxResults: options?.maxResults, token: options?.token })}`, + ); + return await workspace.findFiles(includePattern, excludesPattern, options?.maxResults, options?.token); + }) ?? []; + + const results: Uri[] = []; + Promise.all( + allSearches.map(async (search) => { + try { + const result = await search; + if (result.length > 0) { + results.push(...result); + if (options?.maxResults && results.length >= options.maxResults) { + resolve(results.slice(0, options.maxResults)); + } + } + } catch (error) { + // ignore + } + }), + ).then(() => { + resolve(results); + }); + }); + } +}