Make list of VSCodeRepos into a class

Summary: In the next diff, we need to be able to lookup the repository for a given path in the vscode extension. Let's make this function that was managing VSCodeRepo creation into a class that keeps a list that we can query from.

Reviewed By: quark-zju

Differential Revision: D47385149

fbshipit-source-id: c90934162d552b14f716782da62168f7c258dcc8
This commit is contained in:
Evan Krause 2023-07-12 10:27:26 -07:00 committed by Facebook GitHub Bot
parent 124062ab92
commit 737705df1b
3 changed files with 86 additions and 38 deletions

View File

@ -19,55 +19,79 @@ import {repositoryCache} from 'isl-server/src/RepositoryCache';
import {ComparisonType} from 'shared/Comparison';
import * as vscode from 'vscode';
/**
* Construct Repositories and VSCodeRepos for every workspace folder.
* Treats repositoryCache as the source of truth for re-using repositories.
*/
export function watchAndCreateRepositoriesForWorkspaceFolders(logger: Logger): vscode.Disposable {
const knownRepos = new Map<string, RepositoryReference>();
const vscodeRepos = new Map<string, VSCodeRepo>();
function updateRepos(
export class VSCodeReposList {
private knownRepos = new Map</* attached folder root */ string, RepositoryReference>();
private vscodeRepos = new Map</* repo root path */ string, VSCodeRepo>();
private disposables: Array<vscode.Disposable> = [];
private reposByPath = new Map</* arbitrary subpath of repo */ string, VSCodeRepo>();
constructor(private logger: Logger) {
if (vscode.workspace.workspaceFolders) {
this.updateRepos(vscode.workspace.workspaceFolders, []);
}
this.disposables.push(
vscode.workspace.onDidChangeWorkspaceFolders(e => {
this.updateRepos(e.added, e.removed);
}),
);
// TODO: consider also listening for vscode.workspace.onDidOpenTextDocument to support repos
// for ad-hoc non-workspace-folder files
}
private updateRepos(
added: ReadonlyArray<vscode.WorkspaceFolder>,
removed: ReadonlyArray<vscode.WorkspaceFolder>,
) {
for (const add of added) {
const {fsPath} = add.uri;
if (knownRepos.has(fsPath)) {
if (this.knownRepos.has(fsPath)) {
throw new Error(`Attempted to add workspace folder path twice: ${fsPath}`);
}
const repoReference = repositoryCache.getOrCreate(getCLICommand(), logger, fsPath);
knownRepos.set(fsPath, repoReference);
const repoReference = repositoryCache.getOrCreate(getCLICommand(), this.logger, fsPath);
this.knownRepos.set(fsPath, repoReference);
repoReference.promise.then(repo => {
if (repo instanceof Repository) {
const root = repo?.info.repoRoot;
const existing = vscodeRepos.get(root);
const existing = this.vscodeRepos.get(root);
if (existing) {
return;
}
const vscodeRepo = new VSCodeRepo(repo, logger);
vscodeRepos.set(root, vscodeRepo);
const vscodeRepo = new VSCodeRepo(repo, this.logger);
this.vscodeRepos.set(root, vscodeRepo);
repo.onDidDispose(() => {
vscodeRepo.dispose();
vscodeRepos.delete(root);
this.vscodeRepos.delete(root);
});
}
});
}
for (const remove of removed) {
const {fsPath} = remove.uri;
const repo = knownRepos.get(fsPath);
const repo = this.knownRepos.get(fsPath);
repo?.unref();
knownRepos.delete(fsPath);
this.knownRepos.delete(fsPath);
}
}
if (vscode.workspace.workspaceFolders) {
updateRepos(vscode.workspace.workspaceFolders, []);
/** return the VSCodeRepo that contains the given path */
public repoForPath(path: string): VSCodeRepo | undefined {
if (this.reposByPath.has(path)) {
return this.reposByPath.get(path);
}
for (const value of this.vscodeRepos.values()) {
if (path.startsWith(value.rootPath)) {
return value;
}
}
return undefined;
}
public dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
}
return vscode.workspace.onDidChangeWorkspaceFolders(e => {
updateRepos(e.added, e.removed);
});
// TODO: consider also listening for vscode.workspace.onDidOpenTextDocument to support repos
// for ad-hoc non-workspace-folder files
}
/**
@ -81,11 +105,13 @@ export class VSCodeRepo implements vscode.QuickDiffProvider {
'changes' | 'untracked' | 'unresolved' | 'resolved',
vscode.SourceControlResourceGroup
>;
private rootUri: vscode.Uri;
public rootUri: vscode.Uri;
public rootPath: string;
constructor(public repo: Repository, private logger: Logger) {
repo.onDidDispose(() => this.dispose());
this.rootUri = vscode.Uri.file(repo.info.repoRoot);
this.rootPath = repo.info.repoRoot;
this.sourceControl = vscode.scm.createSourceControl(
'sapling',
@ -226,5 +252,3 @@ const themeColors = {
untracked: new vscode.ThemeColor('gitDecoration.untrackedResourceForeground'),
conflicting: new vscode.ThemeColor('gitDecoration.conflictingResourceForeground'),
};
export const __TEST__ = {watchAndCreateRepositoriesForWorkspaceFolders};

View File

@ -9,14 +9,12 @@ import type {Repository} from 'isl-server/src/Repository';
import type {Logger} from 'isl-server/src/logger';
import type {RepoInfo, ValidatedRepoInfo} from 'isl/src/types';
import {__TEST__} from '../VSCodeRepo';
import {VSCodeReposList} from '../VSCodeRepo';
import {repositoryCache} from 'isl-server/src/RepositoryCache';
import {TypedEventEmitter} from 'shared/TypedEventEmitter';
import {nextTick} from 'shared/testUtils';
import * as vscode from 'vscode';
const {watchAndCreateRepositoriesForWorkspaceFolders} = __TEST__;
const mockLogger: Logger = {info: jest.fn(), warn: jest.fn(), log: jest.fn(), error: jest.fn()};
jest.mock('isl-server/src/Repository', () => {
@ -75,7 +73,7 @@ describe('adding and removing repositories', () => {
});
it('creates repositories for workspace folders', async () => {
const dispose = watchAndCreateRepositoriesForWorkspaceFolders(mockLogger);
const repos = new VSCodeReposList(mockLogger);
foldersEmitter.emit('value', {
added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1')}],
removed: [],
@ -83,11 +81,11 @@ describe('adding and removing repositories', () => {
await nextTick();
expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(1);
dispose.dispose();
repos.dispose();
});
it('deduplicates among shared repos', async () => {
const dispose = watchAndCreateRepositoriesForWorkspaceFolders(mockLogger);
const repos = new VSCodeReposList(mockLogger);
foldersEmitter.emit('value', {
added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1/foo')}],
removed: [],
@ -108,11 +106,11 @@ describe('adding and removing repositories', () => {
await nextTick();
expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(2);
dispose.dispose();
repos.dispose();
});
it('deletes repositories for workspace folders', async () => {
const dispose = watchAndCreateRepositoriesForWorkspaceFolders(mockLogger);
const repos = new VSCodeReposList(mockLogger);
// add repo twice, only creates 1 repo
foldersEmitter.emit('value', {
@ -147,6 +145,31 @@ describe('adding and removing repositories', () => {
await nextTick();
expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(2);
dispose.dispose();
repos.dispose();
});
it('looks up repos by prefix', async () => {
const repos = new VSCodeReposList(mockLogger);
foldersEmitter.emit('value', {
added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1/foo')}],
removed: [],
});
await nextTick();
expect(repos.repoForPath('/path/to/repo1/foo')).not.toBeUndefined();
expect(repos.repoForPath('/path/to/repo1/foo/myFile.txt')).not.toBeUndefined();
expect(repos.repoForPath('/path/to/repo2/foo')).toBeUndefined();
foldersEmitter.emit('value', {
added: [],
removed: [{name: 'my folder', index: 1, uri: vscode.Uri.file('/path/to/repo1/foo')}],
});
await nextTick();
expect(repos.repoForPath('/path/to/repo1/foo')).toBeUndefined();
expect(repos.repoForPath('/path/to/repo1/foo/myFile.txt')).toBeUndefined();
repos.dispose();
});
});

View File

@ -9,7 +9,7 @@ import type {Logger} from 'isl-server/src/logger';
import packageJson from '../package.json';
import {registerSaplingDiffContentProvider} from './DiffContentProvider';
import {watchAndCreateRepositoriesForWorkspaceFolders} from './VSCodeRepo';
import {VSCodeReposList} from './VSCodeRepo';
import {registerCommands} from './commands';
import {ensureTranslationsLoaded} from './i18n';
import {registerISLCommands} from './islWebviewPanel';
@ -26,7 +26,8 @@ export async function activate(context: vscode.ExtensionContext) {
await ensureTranslationsLoaded(context);
context.subscriptions.push(registerISLCommands(context, logger));
context.subscriptions.push(outputChannel);
context.subscriptions.push(watchAndCreateRepositoriesForWorkspaceFolders(logger));
const reposList = new VSCodeReposList(logger);
context.subscriptions.push(reposList);
context.subscriptions.push(registerSaplingDiffContentProvider(logger));
context.subscriptions.push(...registerCommands(extensionTracker));
extensionTracker.track('VSCodeExtensionActivated', {duration: Date.now() - start});