diff --git a/package.json b/package.json index 7550698781..d27f540952 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "contribShareMenu", "diffCommand", "fileComments", + "findFiles2New", + "findTextInFilesNew", "quickDiffProvider", "shareProvider", "tokenInformation", diff --git a/src/@types/vscode.proposed.findFiles2New.d.ts b/src/@types/vscode.proposed.findFiles2New.d.ts new file mode 100644 index 0000000000..7898d98d21 --- /dev/null +++ b/src/@types/vscode.proposed.findFiles2New.d.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface FindFiles2OptionsNew { + /** + * A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. + */ + exclude?: GlobPattern[]; + + /** + * Which settings to follow when searching for files. Defaults to {@link ExcludeSettingOptions.searchAndFilesExclude}. + */ + useExcludeSettings?: ExcludeSettingOptions; + + /** + * The maximum number of results to search for + */ + maxResults?: number; + + /** + * Which file locations we should look for ignore (.gitignore or .ignore) files to respect. + * + * When any of these fields are `undefined`, we will: + * - assume the value if possible (e.g. if only one is valid) + * or + * - follow settings using the value for the corresponding `search.use*IgnoreFiles` settting. + * + * Will log an error if an invalid combination is set. + */ + useIgnoreFiles?: { + /** + * Use ignore files at the current workspace root. + * May default to `search.useIgnoreFiles` setting if not set. + */ + local?: boolean; + /** + * Use ignore files at the parent directory. When set to `true`, {@link FindFiles2OptionsNew.useIgnoreFiles.local} must also be `true`. + * May default to `search.useParentIgnoreFiles` setting if not set. + */ + parent?: boolean; + /** + * Use global ignore files. When set to `true`, {@link FindFiles2OptionsNew.useIgnoreFiles.local} must also be `true`. + * May default to `search.useGlobalIgnoreFiles` setting if not set. + */ + global?: boolean; + }; + + /** + * Whether symlinks should be followed while searching. + * Defaults to the value for `search.followSymlinks` in settings. + * For more info, see the setting description for `search.followSymlinks`. + */ + followSymlinks?: boolean; + } + + export namespace workspace { + /** + * WARNING: VERY EXPERIMENTAL. + * + * Find files across all {@link workspace.workspaceFolders workspace folders} in the workspace. + * + * @example + * + * @param filePattern A {@link GlobPattern glob pattern} that defines the files to search for. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. Use a {@link RelativePattern relative pattern} + * to restrict the search results to a {@link WorkspaceFolder workspace folder}. + * @param options A set of {@link FindFiles2Options FindFiles2Options} that defines where and how to search (e.g. exclude settings). + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @returns A thenable that resolves to an array of resource identifiers. Will return no results if no + * {@link workspace.workspaceFolders workspace folders} are opened. + */ + export function findFiles2New(filePattern: GlobPattern[], options?: FindFiles2OptionsNew, token?: CancellationToken): Thenable; + } +} diff --git a/src/@types/vscode.proposed.findTextInFilesNew.d.ts b/src/@types/vscode.proposed.findTextInFilesNew.d.ts new file mode 100644 index 0000000000..928291faaa --- /dev/null +++ b/src/@types/vscode.proposed.findTextInFilesNew.d.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/59924 + + export interface FindTextInFilesOptionsNew { + /** + * A {@link GlobPattern glob pattern} that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a {@link RelativePattern relative pattern} + * to restrict the search results to a {@link WorkspaceFolder workspace folder}. + */ + include?: GlobPattern[]; + + /** + * A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. + */ + exclude?: GlobPattern[]; + + /** + * Which settings to follow when searching for files. Defaults to {@link ExcludeSettingOptions.searchAndFilesExclude}. + */ + useExcludeSettings?: ExcludeSettingOptions; + + /** + * The maximum number of results to search for + */ + maxResults?: number; + + + /** + * Which file locations we should look for ignore (.gitignore or .ignore) files to respect. + * + * When any of these fields are `undefined`, we will: + * - assume the value if possible (e.g. if only one is valid) + * or + * - follow settings using the value for the corresponding `search.use*IgnoreFiles` settting. + * + * Will log an error if an invalid combination is set. + */ + useIgnoreFiles?: { + /** + * Use ignore files at the current workspace root. + * May default to `search.useIgnoreFiles` setting if not set. + */ + local?: boolean; + /** + * Use ignore files at the parent directory. When set to `true`, {@link FindTextInFilesOptionsNew.useIgnoreFiles.local} must be `true`. + * May default to `search.useParentIgnoreFiles` setting if not set. + */ + parent?: boolean; + /** + * Use global ignore files. When set to `true`, {@link FindTextInFilesOptionsNew.useIgnoreFiles.local} must also be `true`. + * May default to `search.useGlobalIgnoreFiles` setting if not set. + */ + global?: boolean; + }; + + /** + * Whether symlinks should be followed while searching. + * Defaults to the value for `search.followSymlinks` in settings. + * For more info, see the setting description for `search.followSymlinks`. + */ + followSymlinks?: boolean; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ + encoding?: string; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: { + /** + * The maximum number of lines in the preview. + * Only search providers that support multiline search will ever return more than one line in the match. + */ + matchLines?: number; + + /** + * The maximum number of characters included per line. + */ + charsPerLine?: number; + }; + + /** + * Number of lines of context to include before and after each match. + */ + surroundingContext?: number; + } + + export interface FindTextInFilesResponse { + /** + * The results of the text search, in batches. To get completion information, wait on the `complete` property. + */ + results: AsyncIterable; + /** + * The text search completion information. This resolves on completion. + */ + complete: Thenable; + } + + /* + * Options for following search.exclude and files.exclude settings. + */ + export enum ExcludeSettingOptions { + /* + * Don't use any exclude settings. + */ + none = 1, + /* + * Use: + * - files.exclude setting + */ + filesExclude = 2, + /* + * Use: + * - files.exclude setting + * - search.exclude setting + */ + searchAndFilesExclude = 3 + } + + export namespace workspace { + /** + * WARNING: VERY EXPERIMENTAL. + * + * Search text in files across all {@link workspace.workspaceFolders workspace folders} in the workspace. + * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. + * @param options An optional set of query options. Include and exclude patterns, maxResults, etc. + * @param callback A callback, called for each result + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves when the search is complete. + */ + export function findTextInFilesNew(query: TextSearchQueryNew, options?: FindTextInFilesOptionsNew, token?: CancellationToken): FindTextInFilesResponse; + } +} diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 1a57ac86d6..b50fa34501 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -167,7 +167,7 @@ enum PagedDataType { IssueSearch, } -const CACHED_TEMPLATE_BODY = 'templateBody'; +const CACHED_TEMPLATE_URI = 'templateUri'; export class FolderRepositoryManager implements vscode.Disposable { static ID = 'FolderRepositoryManager'; @@ -1211,50 +1211,53 @@ export class FolderRepositoryManager implements vscode.Disposable { async getIssueTemplates(): Promise { const pattern = '{docs,.github}/ISSUE_TEMPLATE/*.md'; - return vscode.workspace.findFiles( - new vscode.RelativePattern(this._repository.rootUri, pattern), null + return vscode.workspace.findFiles2New( + [new vscode.RelativePattern(this._repository.rootUri, pattern)], { useExcludeSettings: vscode.ExcludeSettingOptions.filesExclude } ); } async getPullRequestTemplateBody(owner: string): Promise { - try { - const template = await this.getPullRequestTemplateWithCache(owner); - if (template) { - return template; - } + // First try for a local template + const templateUri = await this.getPullRequestTemplatesWithCache(); - // If there's no local template, look for a owner-wide template - return this.getOwnerPullRequestTemplate(owner); - } catch (e) { - Logger.error(`Error fetching pull request template for ${owner}: ${e instanceof Error ? e.message : e}`, this.id); + if (templateUri) { + try { + const templateContent = await vscode.workspace.fs.readFile(templateUri); + return new TextDecoder('utf-8').decode(templateContent); + } catch (e) { + Logger.warn(`Reading pull request template failed: ${e}`); + } } + + // If there's no local template, look for a owner-wide template + return this.getOwnerPullRequestTemplates(owner); } - private async getPullRequestTemplateWithCache(owner: string): Promise { - const cacheLocation = `${CACHED_TEMPLATE_BODY}+${this.repository.rootUri.toString()}`; + async getPullRequestTemplatesWithCache(): Promise { + const cacheLocation = `${CACHED_TEMPLATE_URI}+${this.repository.rootUri.toString()}`; - const findTemplate = this.getPullRequestTemplate(owner).then((template) => { + const findTemplate = this.getFirstLocalPullRequestTemplate().then((template) => { //update cache if (template) { - this.context.workspaceState.update(cacheLocation, template); + this.context.workspaceState.update(cacheLocation, template.toString()); } else { this.context.workspaceState.update(cacheLocation, null); } return template; }); const hasCachedTemplate = this.context.workspaceState.keys().includes(cacheLocation); - const cachedTemplate = this.context.workspaceState.get(cacheLocation); + const cachedTemplateLocation = this.context.workspaceState.get(cacheLocation); if (hasCachedTemplate) { - if (cachedTemplate === null) { - return undefined; - } else if (cachedTemplate) { - return cachedTemplate; + if (cachedTemplateLocation === null) { + return; + } else if (cachedTemplateLocation) { + return vscode.Uri.parse(cachedTemplateLocation); } } return findTemplate; } - private async getOwnerPullRequestTemplate(owner: string): Promise { + private async getOwnerPullRequestTemplates(owner: string): Promise { const githubRepository = await this.createGitHubRepositoryFromOwnerName(owner, '.github'); if (!githubRepository) { return undefined; @@ -1265,13 +1268,25 @@ export class FolderRepositoryManager implements vscode.Disposable { } } - private async getPullRequestTemplate(owner: string): Promise { - const repository = this.gitHubRepositories.find(repo => repo.remote.owner === owner); - if (!repository) { - return; + private async getFirstLocalPullRequestTemplate(): Promise { + function patternToGlob(pattern: string) { + return new vscode.RelativePattern(this._repository.rootUri, pattern); } - const templates = await repository.getPullRequestTemplates(); - return templates ? templates[0] : undefined; + + /** + * Places a PR template can be: + * - At the root, the docs folder, or the.github folder, named pull_request_template.md or PULL_REQUEST_TEMPLATE.md + * - At the same folder locations under a PULL_REQUEST_TEMPLATE folder with any name + */ + const pattern1 = patternToGlob('{pull_request_template,PULL_REQUEST_TEMPLATE}.{md,txt}'); + const pattern2 = patternToGlob('{docs,.github}/{pull_request_template,PULL_REQUEST_TEMPLATE}.{md,txt}'); + const pattern3 = patternToGlob('{pull_request_template,PULL_REQUEST_TEMPLATE}'); + const pattern4 = patternToGlob('{docs,.github}/{pull_request_template,PULL_REQUEST_TEMPLATE}'); + const pattern5 = patternToGlob('PULL_REQUEST_TEMPLATE/*.md'); + const pattern6 = patternToGlob('{docs,.github}/PULL_REQUEST_TEMPLATE/*.md'); + + const result = await vscode.workspace.findFiles2New([pattern1, pattern2, pattern3, pattern4, pattern5, pattern6], { maxResults: 1, useExcludeSettings: vscode.ExcludeSettingOptions.filesExclude }); + return result.length > 0 ? result[0] : undefined; } async getPullRequestDefaults(branch?: Branch): Promise {