浏览代码

Refactors vsls support into its own provider

Refactors Git static class into an instance (so it could be re-used for vsls)
Reworks vsls guest for better stability
main
Eric Amodio 2 年前
父节点
当前提交
e1303d4b46
共有 25 个文件被更改,包括 915 次插入747 次删除
  1. +1
    -0
      src/codelens/codeLensProvider.ts
  2. +1
    -0
      src/constants.ts
  3. +1
    -1
      src/container.ts
  4. +0
    -0
      src/env/browser/providers.ts
  5. +0
    -12
      src/env/node/git.ts
  6. +263
    -292
      src/env/node/git/git.ts
  7. +180
    -158
      src/env/node/git/localGitProvider.ts
  8. +95
    -0
      src/env/node/git/vslsGitProvider.ts
  9. +34
    -0
      src/env/node/providers.ts
  10. +3
    -2
      src/git/gitProvider.ts
  11. +17
    -24
      src/git/gitProviderService.ts
  12. +0
    -9
      src/git/gitUri.ts
  13. +1
    -2
      src/git/models/repository.ts
  14. +7
    -18
      src/premium/github/githubGitProvider.ts
  15. +14
    -1
      src/repositories.ts
  16. +21
    -0
      src/system/event.ts
  17. +0
    -50
      src/system/function.ts
  18. +82
    -16
      src/system/path.ts
  19. +52
    -0
      src/system/promise.ts
  20. +2
    -4
      src/trackers/documentTracker.ts
  21. +6
    -23
      src/vsls/guest.ts
  22. +45
    -43
      src/vsls/host.ts
  23. +7
    -6
      src/vsls/protocol.ts
  24. +66
    -69
      src/vsls/vsls.ts

+ 1
- 0
src/codelens/codeLensProvider.ts 查看文件

@ -91,6 +91,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
{ scheme: DocumentSchemes.GitLens },
{ scheme: DocumentSchemes.PRs },
{ scheme: DocumentSchemes.Vsls },
{ scheme: DocumentSchemes.VslsScc },
{ scheme: DocumentSchemes.Virtual },
{ scheme: DocumentSchemes.GitHub },
];

+ 1
- 0
src/constants.ts 查看文件

@ -97,6 +97,7 @@ export const enum DocumentSchemes {
Output = 'output',
PRs = 'pr',
Vsls = 'vsls',
VslsScc = 'vsls-scc',
Virtual = 'vscode-vfs',
}

+ 1
- 1
src/container.ts 查看文件

@ -1,5 +1,5 @@
import { commands, ConfigurationChangeEvent, ConfigurationScope, Event, EventEmitter, ExtensionContext } from 'vscode';
import { getSupportedGitProviders } from '@env/git';
import { getSupportedGitProviders } from '@env/providers';
import { Autolinks } from './annotations/autolinks';
import { FileAnnotationController } from './annotations/fileAnnotationController';
import { LineAnnotationController } from './annotations/lineAnnotationController';

src/env/browser/git.ts → src/env/browser/providers.ts 查看文件


+ 0
- 12
src/env/node/git.ts 查看文件

@ -1,12 +0,0 @@
import { Container } from '../../container';
import { GitProvider } from '../../git/gitProvider';
import { GitHubGitProvider } from '../../premium/github/githubGitProvider';
import { LocalGitProvider } from './git/localGitProvider';
export { git } from './git/git';
export function getSupportedGitProviders(container: Container): GitProvider[] {
return container.config.experimental.virtualRepositories.enabled
? [new LocalGitProvider(container), new GitHubGitProvider(container)]
: [new LocalGitProvider(container)];
}

+ 263
- 292
src/env/node/git/git.ts
文件差异内容过多而无法显示
查看文件


+ 180
- 158
src/env/node/git/localGitProvider.ts
文件差异内容过多而无法显示
查看文件


+ 95
- 0
src/env/node/git/vslsGitProvider.ts 查看文件

@ -0,0 +1,95 @@
import { FileType, Uri, workspace } from 'vscode';
import { DocumentSchemes } from '../../../constants';
import { Container } from '../../../container';
import { GitCommandOptions } from '../../../git/commandOptions';
import { GitProviderDescriptor, GitProviderId } from '../../../git/gitProvider';
import { Repository } from '../../../git/models/repository';
import { Logger } from '../../../logger';
import { addVslsPrefixIfNeeded, dirname } from '../../../system/path';
import { Git } from './git';
import { LocalGitProvider } from './localGitProvider';
export class VslsGit extends Git {
constructor(private readonly localGit: Git) {
super();
}
override async git<TOut extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<TOut> {
if (options.local) {
// Since we will have a live share path here, just blank it out
options.cwd = '';
return this.localGit.git<TOut>(options, ...args);
}
const guest = await Container.instance.vsls.guest();
if (guest == null) {
debugger;
throw new Error('No guest');
}
return guest.git<TOut>(options, ...args);
}
}
export class VslsGitProvider extends LocalGitProvider {
override readonly descriptor: GitProviderDescriptor = { id: GitProviderId.Vsls, name: 'Live Share' };
override readonly supportedSchemes: string[] = [DocumentSchemes.Vsls, DocumentSchemes.VslsScc];
override async discoverRepositories(uri: Uri): Promise<Repository[]> {
if (!this.supportedSchemes.includes(uri.scheme)) return [];
const cc = Logger.getCorrelationContext();
try {
const guest = await this.container.vsls.guest();
const repositories = await guest?.getRepositoriesForUri(uri);
if (repositories == null || repositories.length === 0) return [];
return repositories.map(r =>
this.openRepository(undefined, Uri.parse(r.folderUri, true), r.root, undefined, r.closed),
);
} catch (ex) {
Logger.error(ex, cc);
debugger;
return [];
}
}
override getAbsoluteUri(pathOrUri: string | Uri, base: string | Uri): Uri {
pathOrUri = addVslsPrefixIfNeeded(pathOrUri);
const scheme =
(typeof base !== 'string' ? base.scheme : undefined) ??
(typeof pathOrUri !== 'string' ? pathOrUri.scheme : undefined) ??
DocumentSchemes.Vsls;
return super.getAbsoluteUri(pathOrUri, base).with({ scheme: scheme });
}
override async findRepositoryUri(uri: Uri, isDirectory?: boolean): Promise<Uri | undefined> {
const cc = Logger.getCorrelationContext();
let repoPath: string | undefined;
try {
if (!isDirectory) {
try {
const stats = await workspace.fs.stat(uri);
uri = stats?.type === FileType.Directory ? uri : uri.with({ path: dirname(uri.fsPath) });
} catch {}
}
repoPath = await this.git.rev_parse__show_toplevel(uri.fsPath);
if (!repoPath) return undefined;
return repoPath ? Uri.parse(repoPath, true) : undefined;
} catch (ex) {
Logger.error(ex, cc);
return undefined;
}
}
override getLastFetchedTimestamp(_repoPath: string): Promise<number | undefined> {
return Promise.resolve(undefined);
}
}

+ 34
- 0
src/env/node/providers.ts 查看文件

@ -0,0 +1,34 @@
import { Container } from '../../container';
import { GitCommandOptions } from '../../git/commandOptions';
import { GitProvider } from '../../git/gitProvider';
import { GitHubGitProvider } from '../../premium/github/githubGitProvider';
import { Git } from './git/git';
import { LocalGitProvider } from './git/localGitProvider';
import { VslsGit, VslsGitProvider } from './git/vslsGitProvider';
let gitInstance: Git | undefined;
function ensureGit() {
if (gitInstance == null) {
gitInstance = new Git();
}
return gitInstance;
}
export function git(_options: GitCommandOptions, ..._args: any[]): Promise<string | Buffer> {
return ensureGit().git(_options, ..._args);
}
export function getSupportedGitProviders(container: Container): GitProvider[] {
const git = ensureGit();
const providers: GitProvider[] = [
new LocalGitProvider(container, git),
new VslsGitProvider(container, new VslsGit(git)),
];
if (container.config.experimental.virtualRepositories.enabled) {
providers.push(new GitHubGitProvider(container));
}
return providers;
}

+ 3
- 2
src/git/gitProvider.ts 查看文件

@ -37,6 +37,7 @@ import { SearchPattern } from './search';
export const enum GitProviderId {
Git = 'git',
GitHub = 'github',
Vsls = 'vsls',
}
export interface GitProviderDescriptor {
@ -93,8 +94,8 @@ export interface GitProvider extends Disposable {
getOpenScmRepositories(): Promise<ScmRepository[]>;
getOrOpenScmRepository(repoPath: string): Promise<ScmRepository | undefined>;
canHandlePathOrUri(pathOrUri: string | Uri): string | undefined;
getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri;
canHandlePathOrUri(scheme: string, pathOrUri: string | Uri): string | undefined;
getAbsoluteUri(pathOrUri: string | Uri, base: string | Uri): Uri;
getBestRevisionUri(repoPath: string, path: string, ref: string | undefined): Promise<Uri | undefined>;
getRelativePath(pathOrUri: string | Uri, base: string | Uri): string;
getRevisionUri(repoPath: string, path: string, ref: string): Uri;

+ 17
- 24
src/git/gitProviderService.ts 查看文件

@ -32,11 +32,9 @@ import { groupByFilterMap, groupByMap } from '../system/array';
import { gate } from '../system/decorators/gate';
import { debug, log } from '../system/decorators/log';
import { count, filter, first, flatMap, map } from '../system/iterable';
import { dirname, getBestPath, isAbsolute, normalizePath } from '../system/path';
import { dirname, getBestPath, getScheme, isAbsolute, maybeUri } from '../system/path';
import { cancellable, isPromise, PromiseCancelledError } from '../system/promise';
import { CharCode } from '../system/string';
import { VisitedPathsTrie } from '../system/trie';
import { vslsUriPrefixRegex } from '../vsls/vsls';
import { GitProvider, GitProviderDescriptor, GitProviderId, PagedResult, ScmRepository } from './gitProvider';
import { GitUri } from './gitUri';
import {
@ -81,8 +79,6 @@ import { RemoteProviders } from './remotes/factory';
import { Authentication, RemoteProvider, RichRemoteProvider } from './remotes/provider';
import { SearchPattern } from './search';
export const isUriRegex = /^(\w[\w\d+.-]{1,}?):\/\//;
const maxDefaultBranchWeight = 100;
const weightedDefaultBranches = new Map<string, number>([
['master', maxDefaultBranchWeight],
@ -133,11 +129,13 @@ export class GitProviderService implements Disposable {
return this._onDidChangeRepository.event;
}
readonly supportedSchemes = new Set<string>();
private readonly _disposable: Disposable;
private readonly _pendingRepositories = new Map<RepoComparisionKey, Promise<Repository | undefined>>();
private readonly _providers = new Map<GitProviderId, GitProvider>();
private readonly _repositories = new Repositories();
private readonly _richRemotesCache = new Map<string, GitRemote<RichRemoteProvider> | null>();
private readonly _supportedSchemes = new Set<string>();
private readonly _visitedPaths = new VisitedPathsTrie();
constructor(private readonly container: Container) {
@ -290,7 +288,7 @@ export class GitProviderService implements Disposable {
this._providers.set(id, provider);
for (const scheme of provider.supportedSchemes) {
this._supportedSchemes.add(scheme);
this.supportedSchemes.add(scheme);
}
const disposables = [];
@ -569,17 +567,24 @@ export class GitProviderService implements Disposable {
// private _pathToProvider = new Map<string, GitProviderResult>();
private getProvider(repoPath: string | Uri): GitProviderResult {
if (repoPath == null || (typeof repoPath !== 'string' && !this._supportedSchemes.has(repoPath.scheme))) {
if (repoPath == null || (typeof repoPath !== 'string' && !this.supportedSchemes.has(repoPath.scheme))) {
debugger;
throw new ProviderNotFoundError(repoPath);
}
let scheme;
if (typeof repoPath === 'string') {
scheme = getScheme(repoPath) ?? DocumentSchemes.File;
} else {
({ scheme } = repoPath);
}
// const key = repoPath.toString();
// let providerResult = this._pathToProvider.get(key);
// if (providerResult != null) return providerResult;
for (const provider of this._providers.values()) {
const path = provider.canHandlePathOrUri(repoPath);
const path = provider.canHandlePathOrUri(scheme, repoPath);
if (path == null) continue;
const providerResult: GitProviderResult = { provider: provider, path: path };
@ -621,7 +626,7 @@ export class GitProviderService implements Disposable {
getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri {
if (base == null) {
if (typeof pathOrUri === 'string') {
if (isUriRegex.test(pathOrUri)) return Uri.parse(pathOrUri, true);
if (maybeUri(pathOrUri)) return Uri.parse(pathOrUri, true);
// I think it is safe to assume this should be file://
return Uri.file(pathOrUri);
@ -1611,8 +1616,6 @@ export class GitProviderService implements Disposable {
return (editor != null ? this.getRepository(editor.document.uri) : undefined) ?? this.highlander;
}
private _pendingRepositories = new Map<RepoComparisionKey, Promise<Repository | undefined>>();
@log<GitProviderService['getOrOpenRepository']>({ exit: r => `returned ${r?.path}` })
async getOrOpenRepository(uri: Uri, detectNested?: boolean): Promise<Repository | undefined> {
const cc = Logger.getCorrelationContext();
@ -1642,17 +1645,7 @@ export class GitProviderService implements Disposable {
if (repository != null) return repository;
// If this new repo is inside one of our known roots and we we don't already know about, add it
let root = this._repositories.getClosest(uri);
// If we can't find the repo and we are a guest, check if we are a "root" workspace
if (root == null && (uri.scheme === DocumentSchemes.Vsls || this.container.vsls.isMaybeGuest)) {
// TODO@eamodio verify this works for live share
let path = uri.fsPath;
if (!vslsUriPrefixRegex.test(path)) {
path = normalizePath(path);
const vslsPath = `/~0${path.charCodeAt(0) === CharCode.Slash ? path : `/${path}`}`;
root = this._repositories.getClosest(Uri.file(vslsPath).with({ scheme: DocumentSchemes.Vsls }));
}
}
const root = this._repositories.getClosest(provider.getAbsoluteUri(uri, repoUri));
Logger.log(cc, `Repository found in '${repoUri.toString(false)}'`);
repository = provider.openRepository(root?.folder, repoUri, false);
@ -1830,7 +1823,7 @@ export class GitProviderService implements Disposable {
}
isTrackable(uri: Uri): boolean {
if (!this._supportedSchemes.has(uri.scheme)) return false;
if (!this.supportedSchemes.has(uri.scheme)) return false;
const { provider } = this.getProvider(uri);
return provider.isTrackable(uri);

+ 0
- 9
src/git/gitUri.ts 查看文件

@ -230,15 +230,6 @@ export class GitUri extends (Uri as any as UriEx) {
return Container.instance.git.getAbsoluteUri(this.fsPath, this.repoPath);
}
static file(path: string, useVslsScheme?: boolean) {
const uri = Uri.file(path);
if (Container.instance.vsls.isMaybeGuest && useVslsScheme !== false) {
return uri.with({ scheme: DocumentSchemes.Vsls });
}
return uri;
}
static fromCommit(commit: GitCommit, previous: boolean = false) {
if (!previous) return new GitUri(commit.uri, commit);

+ 1
- 2
src/git/models/repository.ts 查看文件

@ -529,8 +529,7 @@ export class Repository implements Disposable {
@gate()
async getLastFetched(): Promise<number> {
if (this._lastFetched == null) {
const hasRemotes = await this.hasRemotes();
if (!hasRemotes || this.container.vsls.isMaybeGuest) return 0;
if (!(await this.hasRemotes())) return 0;
}
try {

+ 7
- 18
src/premium/github/githubGitProvider.ts 查看文件

@ -30,7 +30,6 @@ import {
RepositoryOpenEvent,
ScmRepository,
} from '../../git/gitProvider';
import { isUriRegex } from '../../git/gitProviderService';
import { GitUri } from '../../git/gitUri';
import {
BranchSortOptions,
@ -74,7 +73,7 @@ import { RemoteProvider, RichRemoteProvider } from '../../git/remotes/provider';
import { SearchPattern } from '../../git/search';
import { LogCorrelationContext, Logger } from '../../logger';
import { debug, gate, Iterables, log } from '../../system';
import { isAbsolute, isFolderGlob, normalizePath, relative } from '../../system/path';
import { isAbsolute, isFolderGlob, maybeUri, normalizePath, relative } from '../../system/path';
import { CharCode } from '../../system/string';
import { CachedBlame, CachedLog, GitDocumentState } from '../../trackers/gitDocumentTracker';
import { TrackedDocument } from '../../trackers/trackedDocument';
@ -135,7 +134,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
}
async discoverRepositories(uri: Uri): Promise<Repository[]> {
if (uri.scheme !== DocumentSchemes.Virtual) return [];
if (!this.supportedSchemes.includes(uri.scheme)) return [];
try {
void (await this.ensureRepositoryContext(uri.toString()));
@ -172,17 +171,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
return undefined;
}
canHandlePathOrUri(pathOrUri: string | Uri): string | undefined {
let scheme;
if (typeof pathOrUri === 'string') {
const match = isUriRegex.exec(pathOrUri);
if (match == null) return undefined;
[, scheme] = match;
} else {
({ scheme } = pathOrUri);
}
canHandlePathOrUri(scheme: string, pathOrUri: string | Uri): string | undefined {
if (!this.supportedSchemes.includes(scheme)) return undefined;
return typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.toString();
}
@ -191,7 +180,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
// Convert the base to a Uri if it isn't one
if (typeof base === 'string') {
// If it looks like a Uri parse it, otherwise throw
if (isUriRegex.test(base)) {
if (maybeUri(base)) {
base = Uri.parse(base, true);
} else {
debugger;
@ -199,7 +188,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
}
}
if (typeof pathOrUri === 'string' && !isUriRegex.test(pathOrUri) && !isAbsolute(pathOrUri)) {
if (typeof pathOrUri === 'string' && !maybeUri(pathOrUri) && !isAbsolute(pathOrUri)) {
return Uri.joinPath(base, pathOrUri);
}
@ -216,7 +205,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
// Convert the base to a Uri if it isn't one
if (typeof base === 'string') {
// If it looks like a Uri parse it, otherwise throw
if (isUriRegex.test(base)) {
if (maybeUri(base)) {
base = Uri.parse(base, true);
} else {
debugger;
@ -228,7 +217,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
// Convert the path to a Uri if it isn't one
if (typeof pathOrUri === 'string') {
if (isUriRegex.test(pathOrUri)) {
if (maybeUri(pathOrUri)) {
pathOrUri = Uri.parse(pathOrUri, true);
} else {
pathOrUri = normalizePath(pathOrUri);

+ 14
- 1
src/repositories.ts 查看文件

@ -2,7 +2,7 @@ import { Uri } from 'vscode';
import { DocumentSchemes } from './constants';
import { isLinux } from './env/node/platform';
import { Repository } from './git/models/repository';
import { normalizePath } from './system/path';
import { addVslsPrefixIfNeeded, normalizePath } from './system/path';
import { UriTrie } from './system/trie';
// TODO@eamodio don't import from string here since it will break the tests because of ESM dependencies
// import { CharCode } from './string';
@ -44,6 +44,19 @@ export function normalizeRepoUri(uri: Uri): { path: string; ignoreCase: boolean
const authority = uri.authority?.split('+', 1)[0];
return { path: authority ? `${authority}${path}` : path.slice(1), ignoreCase: false };
}
case DocumentSchemes.Vsls:
case DocumentSchemes.VslsScc:
// Check if this is a root live share folder, if so add the required prefix (required to match repos correctly)
path = addVslsPrefixIfNeeded(uri.path);
if (path.charCodeAt(path.length - 1) === slash) {
path = path.slice(1, -1);
} else {
path = path.slice(1);
}
return { path: path, ignoreCase: false };
default:
path = uri.path;
if (path.charCodeAt(path.length - 1) === slash) {

+ 21
- 0
src/system/event.ts 查看文件

@ -14,3 +14,24 @@ export function once(event: Event): Event {
return result;
};
}
export function promisify<T>(event: Event<T>): Promise<T> {
return new Promise<T>(resolve => once(event)(resolve));
}
export function until<T>(event: Event<T>, predicate: (e: T) => boolean): Event<T> {
return (listener: (e: T) => unknown, thisArgs?: unknown, disposables?: Disposable[]) => {
const result = event(
e => {
if (predicate(e)) {
result.dispose();
}
return listener.call(thisArgs, e);
},
null,
disposables,
);
return result;
};
}

+ 0
- 50
src/system/function.ts 查看文件

@ -13,19 +13,6 @@ interface PropOfValue {
value: string | undefined;
}
export function cachedOnce<T>(fn: (...args: any[]) => Promise<T>, seed: T): (...args: any[]) => Promise<T> {
let cached: T | undefined = seed;
return (...args: any[]) => {
if (cached !== undefined) {
const promise = Promise.resolve(cached);
cached = undefined;
return promise;
}
return fn(...args);
};
}
export interface DebounceOptions {
leading?: boolean;
maxWait?: number;
@ -164,40 +151,3 @@ export function disposableInterval(fn: (...args: any[]) => void, ms: number): Di
return disposable;
}
export function progress<T>(promise: Promise<T>, intervalMs: number, onProgress: () => boolean): Promise<T> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setInterval> | undefined;
timer = setInterval(() => {
if (onProgress()) {
if (timer != null) {
clearInterval(timer);
timer = undefined;
}
}
}, intervalMs);
promise.then(
() => {
if (timer != null) {
clearInterval(timer);
timer = undefined;
}
resolve(promise);
},
ex => {
if (timer != null) {
clearInterval(timer);
timer = undefined;
}
reject(ex);
},
);
});
}
export async function wait(ms: number) {
await new Promise(resolve => setTimeout(resolve, ms));
}

+ 82
- 16
src/system/path.ts 查看文件

@ -1,16 +1,51 @@
import { basename, dirname } from 'path';
import { isAbsolute as _isAbsolute, basename, dirname } from 'path';
import { Uri } from 'vscode';
import { isLinux, isWindows } from '@env/platform';
import { DocumentSchemes } from '../constants';
// TODO@eamodio don't import from string here since it will break the tests because of ESM dependencies
// import { CharCode } from './string';
export { basename, dirname, extname, isAbsolute, join as joinPaths } from 'path';
export { basename, dirname, extname, join as joinPaths } from 'path';
const slash = 47; //slash;
const driveLetterNormalizeRegex = /(?<=^\/?)([A-Z])(?=:\/)/;
const hasSchemeRegex = /^([a-zA-Z][\w+.-]+):/;
const pathNormalizeRegex = /\\/g;
const slash = 47; //slash;
const uriSchemeRegex = /^(\w[\w\d+.-]{1,}?):\/\//;
const vslsHasPrefixRegex = /^[/|\\]~(?:\d+?|external)(?:[/|\\]|$)/;
const vslsRootUriRegex = /^[/|\\]~(?:\d+?|external)(?:[/|\\]|$)/;
export function addVslsPrefixIfNeeded(path: string): string;
export function addVslsPrefixIfNeeded(uri: Uri): Uri;
export function addVslsPrefixIfNeeded(pathOrUri: string | Uri): string | Uri;
export function addVslsPrefixIfNeeded(pathOrUri: string | Uri): string | Uri {
if (typeof pathOrUri === 'string') {
if (maybeUri(pathOrUri)) {
pathOrUri = Uri.parse(pathOrUri);
}
}
if (typeof pathOrUri === 'string') {
if (hasVslsPrefix(pathOrUri)) return pathOrUri;
pathOrUri = normalizePath(pathOrUri);
return `/~0${pathOrUri.charCodeAt(0) === slash ? pathOrUri : `/${pathOrUri}`}`;
}
let path = pathOrUri.fsPath;
if (hasVslsPrefix(path)) return pathOrUri;
path = normalizePath(path);
return pathOrUri.with({ path: `/~0${path.charCodeAt(0) === slash ? path : `/${path}`}` });
}
export function hasVslsPrefix(path: string): boolean {
return vslsHasPrefixRegex.test(path);
}
export function isVslsRoot(path: string): boolean {
return vslsRootUriRegex.test(path);
}
export function commonBase(s1: string, s2: string, delimiter: string, ignoreCase?: boolean): string | undefined {
const index = commonBaseIndex(s1, s2, delimiter, ignoreCase);
@ -40,7 +75,11 @@ export function commonBaseIndex(s1: string, s2: string, delimiter: string, ignor
}
export function getBestPath(uri: Uri): string {
return uri.scheme === DocumentSchemes.File ? uri.fsPath : uri.path;
return normalizePath(uri.scheme === DocumentSchemes.File ? uri.fsPath : uri.path);
}
export function getScheme(path: string): string | undefined {
return hasSchemeRegex.exec(path)?.[1];
}
export function isChild(path: string, base: string | Uri): boolean;
@ -54,7 +93,7 @@ export function isChild(pathOrUri: string | Uri, base: string | Uri): boolean {
return (
isDescendent(pathOrUri, base) &&
(typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.path)
.substr(base.length + (base.endsWith('/') ? 0 : 1))
.substr(base.length + (base.charCodeAt(base.length - 1) === slash ? 0 : 1))
.split('/').length === 1
);
}
@ -62,7 +101,7 @@ export function isChild(pathOrUri: string | Uri, base: string | Uri): boolean {
return (
isDescendent(pathOrUri, base) &&
(typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.path)
.substr(base.path.length + (base.path.endsWith('/') ? 0 : 1))
.substr(base.path.length + (base.path.charCodeAt(base.path.length - 1) === slash ? 0 : 1))
.split('/').length === 1
);
}
@ -89,26 +128,40 @@ export function isDescendent(pathOrUri: string | Uri, base: string | Uri): boole
return (
base.length === 1 ||
(typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.path).startsWith(
base.endsWith('/') ? base : `${base}/`,
base.charCodeAt(base.length - 1) === slash ? base : `${base}/`,
)
);
}
if (typeof pathOrUri === 'string') {
return base.path.length === 1 || pathOrUri.startsWith(base.path.endsWith('/') ? base.path : `${base.path}/`);
return (
base.path.length === 1 ||
pathOrUri.startsWith(base.path.charCodeAt(base.path.length - 1) === slash ? base.path : `${base.path}/`)
);
}
return (
base.scheme === pathOrUri.scheme &&
base.authority === pathOrUri.authority &&
(base.path.length === 1 || pathOrUri.path.startsWith(base.path.endsWith('/') ? base.path : `${base.path}/`))
(base.path.length === 1 ||
pathOrUri.path.startsWith(
base.path.charCodeAt(base.path.length - 1) === slash ? base.path : `${base.path}/`,
))
);
}
export function isAbsolute(path: string): boolean {
return !maybeUri(path) && _isAbsolute(path);
}
export function isFolderGlob(path: string): boolean {
return basename(path) === '*';
}
export function maybeUri(path: string): boolean {
return hasSchemeRegex.test(path);
}
export function normalizePath(path: string): string {
if (!path) return path;
@ -119,15 +172,15 @@ export function normalizePath(path: string): string {
if (isWindows) {
// Ensure that drive casing is normalized (lower case)
path = path.replace(driveLetterNormalizeRegex, drive => drive.toLowerCase());
path = path.replace(driveLetterNormalizeRegex, d => d.toLowerCase());
}
return path;
}
export function relative(from: string, to: string, ignoreCase?: boolean): string {
from = uriSchemeRegex.test(from) ? Uri.parse(from, true).path : normalizePath(from);
to = uriSchemeRegex.test(to) ? Uri.parse(to, true).path : normalizePath(to);
from = hasSchemeRegex.test(from) ? Uri.parse(from, true).path : normalizePath(from);
to = hasSchemeRegex.test(to) ? Uri.parse(to, true).path : normalizePath(to);
const index = commonBaseIndex(`${to}/`, `${from}/`, '/', ignoreCase);
return index > 0 ? to.substring(index + 1) : to;
@ -140,16 +193,29 @@ export function splitPath(
ignoreCase?: boolean,
): [string, string] {
if (repoPath) {
path = normalizePath(path);
repoPath = normalizePath(repoPath);
path = hasSchemeRegex.test(path) ? Uri.parse(path, true).path : normalizePath(path);
let repoUri;
if (hasSchemeRegex.test(repoPath)) {
repoUri = Uri.parse(repoPath, true);
repoPath = getBestPath(repoUri);
} else {
repoPath = normalizePath(repoPath);
}
const index = commonBaseIndex(`${repoPath}/`, `${path}/`, '/', ignoreCase);
if (index > 0) {
repoPath = path.substring(0, index);
path = path.substring(index + 1);
} else if (path.charCodeAt(0) === slash) {
path = path.slice(1);
}
if (repoUri != null) {
repoPath = repoUri.with({ path: repoPath }).toString();
}
} else {
repoPath = normalizePath(splitOnBaseIfMissing ? dirname(path) : repoPath ?? '');
repoPath = normalizePath(splitOnBaseIfMissing ? dirname(path) : '');
path = normalizePath(splitOnBaseIfMissing ? basename(path) : path);
}

+ 52
- 0
src/system/promise.ts 查看文件

@ -101,10 +101,58 @@ export function cancellable(
});
}
export interface Deferred<T> {
promise: Promise<T>;
fulfill: (value: T) => void;
cancel(): void;
}
export function defer<T>(): Deferred<T> {
const deferred: Deferred<T> = { promise: undefined!, fulfill: undefined!, cancel: undefined! };
deferred.promise = new Promise((resolve, reject) => {
deferred.fulfill = resolve;
deferred.cancel = reject;
});
return deferred;
}
export function isPromise<T>(obj: PromiseLike<T> | T): obj is Promise<T> {
return obj instanceof Promise || typeof (obj as PromiseLike<T>)?.then === 'function';
}
export function progress<T>(promise: Promise<T>, intervalMs: number, onProgress: () => boolean): Promise<T> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setInterval> | undefined;
timer = setInterval(() => {
if (onProgress()) {
if (timer != null) {
clearInterval(timer);
timer = undefined;
}
}
}, intervalMs);
promise.then(
() => {
if (timer != null) {
clearInterval(timer);
timer = undefined;
}
resolve(promise);
},
ex => {
if (timer != null) {
clearInterval(timer);
timer = undefined;
}
reject(ex);
},
);
});
}
export function raceAll<TPromise>(
promises: Promise<TPromise>[],
timeout?: number,
@ -168,6 +216,10 @@ export async function raceAll(
);
}
export async function wait(ms: number): Promise<void> {
await new Promise(resolve => setTimeout(resolve, ms));
}
export class AggregateError extends Error {
constructor(readonly errors: Error[]) {
super(`AggregateError(${errors.length})\n${errors.map(e => `\t${String(e)}`).join('\n')}`);

+ 2
- 4
src/trackers/documentTracker.ts 查看文件

@ -16,7 +16,7 @@ import {
workspace,
} from 'vscode';
import { configuration } from '../configuration';
import { ContextKeys, DocumentSchemes, isActiveDocument, isTextEditor, setContext } from '../constants';
import { ContextKeys, isActiveDocument, isTextEditor, setContext } from '../constants';
import { Container } from '../container';
import { RepositoriesChangeEvent } from '../git/gitProviderService';
import { GitUri } from '../git/gitUri';
@ -170,9 +170,7 @@ export class DocumentTracker implements Disposable {
private async onTextDocumentChanged(e: TextDocumentChangeEvent) {
const { scheme } = e.document.uri;
if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Git && scheme !== DocumentSchemes.Vsls) {
return;
}
if (!this.container.git.supportedSchemes.has(scheme)) return;
const doc = await (this._documentMap.get(e.document) ?? this.addCore(e.document));
doc.reset('document');

+ 6
- 23
src/vsls/guest.ts 查看文件

@ -1,12 +1,11 @@
import { CancellationToken, Disposable, Uri, window, WorkspaceFolder } from 'vscode';
import { CancellationToken, Disposable, Uri, window } from 'vscode';
import type { LiveShare, SharedServiceProxy } from '../@types/vsls';
import { Container } from '../container';
import { GitCommandOptions } from '../git/commandOptions';
import { Repository, RepositoryChangeEvent } from '../git/models';
import { Logger } from '../logger';
import { debug, log } from '../system';
import { VslsHostService } from './host';
import { GitCommandRequestType, RepositoriesInFolderRequestType, RepositoryProxy, RequestType } from './protocol';
import { GetRepositoriesForUriRequestType, GitCommandRequestType, RepositoryProxy, RequestType } from './protocol';
export class VslsGuestService implements Disposable {
@log()
@ -64,28 +63,12 @@ export class VslsGuestService implements Disposable {
}
@log()
async getRepositoriesInFolder(
folder: WorkspaceFolder,
onAnyRepositoryChanged: (repo: Repository, e: RepositoryChangeEvent) => void,
): Promise<Repository[]> {
const response = await this.sendRequest(RepositoriesInFolderRequestType, {
folderUri: folder.uri.toString(true),
async getRepositoriesForUri(uri: Uri): Promise<RepositoryProxy[]> {
const response = await this.sendRequest(GetRepositoriesForUriRequestType, {
folderUri: uri.toString(),
});
return response.repositories.map(
(r: RepositoryProxy) =>
new Repository(
this.container,
onAnyRepositoryChanged,
// TODO@eamodio add live share provider
undefined!,
folder,
Uri.parse(r.uri),
r.root,
!window.state.focused,
r.closed,
),
);
return response.repositories;
}
@debug()

+ 45
- 43
src/vsls/host.ts 查看文件

@ -1,21 +1,21 @@
import { CancellationToken, Disposable, Uri, workspace, WorkspaceFoldersChangeEvent } from 'vscode';
import { git } from '@env/git';
import { git } from '@env/providers';
import type { LiveShare, SharedService } from '../@types/vsls';
import { Container } from '../container';
import { Logger } from '../logger';
import { debug, log } from '../system/decorators/log';
import { filterMap, join } from '../system/iterable';
import { normalizePath } from '../system/path';
import { join } from '../system/iterable';
import { isVslsRoot, normalizePath } from '../system/path';
import {
GetRepositoriesForUriRequest,
GetRepositoriesForUriRequestType,
GetRepositoriesForUriResponse,
GitCommandRequest,
GitCommandRequestType,
GitCommandResponse,
RepositoriesInFolderRequest,
RepositoriesInFolderRequestType,
RepositoriesInFolderResponse,
RepositoryProxy,
RequestType,
} from './protocol';
import { vslsUriRootRegex } from './vsls';
const defaultWhitelistFn = () => true;
const gitWhitelist = new Map<string, (args: any[]) => boolean>([
@ -45,6 +45,7 @@ const gitWhitelist = new Map boolean>([
]);
const leadingSlashRegex = /^[/|\\]/;
const slash = 47; //CharCode.Slash;
export class VslsHostService implements Disposable {
static ServiceId = 'proxy';
@ -75,7 +76,7 @@ export class VslsHostService implements Disposable {
this._disposable = Disposable.from(workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this));
this.onRequest(GitCommandRequestType, this.onGitCommandRequest.bind(this));
this.onRequest(RepositoriesInFolderRequestType, this.onRepositoriesInFolderRequest.bind(this));
this.onRequest(GetRepositoriesForUriRequestType, this.onGetRepositoriesForUriRequest.bind(this));
void this.onWorkspaceFoldersChanged();
}
@ -101,7 +102,7 @@ export class VslsHostService implements Disposable {
@debug()
private onWorkspaceFoldersChanged(_e?: WorkspaceFoldersChangeEvent) {
if (workspace.workspaceFolders === undefined || workspace.workspaceFolders.length === 0) return;
if (workspace.workspaceFolders == null || workspace.workspaceFolders.length === 0) return;
const cc = Logger.getCorrelationContext();
@ -112,7 +113,7 @@ export class VslsHostService implements Disposable {
let sharedPath;
for (const f of workspace.workspaceFolders) {
localPath = normalizePath(f.uri.fsPath);
sharedPath = normalizePath(this.convertLocalUriToShared(f.uri).fsPath);
sharedPath = normalizePath(this.convertLocalUriToShared(f.uri).toString());
Logger.debug(cc, `shared='${sharedPath}' \u2194 local='${localPath}'`);
this._localToSharedPaths.set(localPath, sharedPath);
@ -136,10 +137,10 @@ export class VslsHostService implements Disposable {
const { options, args } = request;
const fn = gitWhitelist.get(request.args[0]);
if (fn === undefined || !fn(request.args)) throw new Error(`Git ${request.args[0]} command is not allowed`);
if (fn == null || !fn(request.args)) throw new Error(`Git ${request.args[0]} command is not allowed`);
let isRootWorkspace = false;
if (options.cwd !== undefined && options.cwd.length > 0 && this._sharedToLocalPaths !== undefined) {
if (options.cwd != null && options.cwd.length > 0 && this._sharedToLocalPaths != null) {
// This is all so ugly, but basically we are converting shared paths to local paths
if (this._sharedPathsRegex?.test(options.cwd)) {
options.cwd = normalizePath(options.cwd).replace(this._sharedPathsRegex, (match, shared) => {
@ -151,8 +152,8 @@ export class VslsHostService implements Disposable {
return local != null ? local : shared;
});
} else if (leadingSlashRegex.test(options.cwd)) {
const localCwd = this._sharedToLocalPaths.get('/~0');
if (localCwd !== undefined) {
const localCwd = this._sharedToLocalPaths.get('vsls:/~0');
if (localCwd != null) {
isRootWorkspace = true;
options.cwd = normalizePath(this.container.git.getAbsoluteUri(options.cwd, localCwd).fsPath);
}
@ -192,9 +193,9 @@ export class VslsHostService implements Disposable {
let data = await git(options, ...args);
if (typeof data === 'string') {
// And then we convert local paths to shared paths
if (this._localPathsRegex !== undefined && data.length > 0) {
if (this._localPathsRegex != null && data.length > 0) {
data = data.replace(this._localPathsRegex, (match, local) => {
const shared = this._localToSharedPaths.get(local);
const shared = this._localToSharedPaths.get(normalizePath(local));
return shared != null ? shared : local;
});
}
@ -207,30 +208,26 @@ export class VslsHostService implements Disposable {
// eslint-disable-next-line @typescript-eslint/require-await
@log()
private async onRepositoriesInFolderRequest(
request: RepositoriesInFolderRequest,
private async onGetRepositoriesForUriRequest(
request: GetRepositoriesForUriRequest,
_cancellation: CancellationToken,
): Promise<RepositoriesInFolderResponse> {
const uri = this.convertSharedUriToLocal(Uri.parse(request.folderUri));
const normalized = normalizePath(uri.fsPath).toLowerCase();
const repos = [
...filterMap(this.container.git.repositories, r => {
if (!r.id.startsWith(normalized)) return undefined;
const vslsUri = this.convertLocalUriToShared(r.folder?.uri ?? r.uri);
return {
folderUri: vslsUri.toString(true),
uri: vslsUri.toString(),
root: r.root,
closed: r.closed,
};
}),
];
return {
repositories: repos,
};
): Promise<GetRepositoriesForUriResponse> {
const repositories: RepositoryProxy[] = [];
const uri = this.convertSharedUriToLocal(Uri.parse(request.folderUri, true));
const repository = this.container.git.getRepository(uri);
if (repository != null) {
const vslsUri = this.convertLocalUriToShared(repository.uri);
repositories.push({
folderUri: vslsUri.toString(),
// uri: vslsUri.toString(),
root: repository.root,
closed: repository.closed,
});
}
return { repositories: repositories };
}
@debug({
@ -267,17 +264,22 @@ export class VslsHostService implements Disposable {
}
private convertSharedUriToLocal(sharedUri: Uri) {
if (vslsUriRootRegex.test(sharedUri.path)) {
if (isVslsRoot(sharedUri.path)) {
sharedUri = sharedUri.with({ path: `${sharedUri.path}/` });
}
const localUri = this._api.convertSharedUriToLocal(sharedUri);
const localPath = localUri.path;
let localPath = localUri.path;
const sharedPath = sharedUri.path;
if (localPath.endsWith(sharedPath)) {
return localUri.with({ path: localPath.substr(0, localPath.length - sharedPath.length) });
localPath = localPath.substr(0, localPath.length - sharedPath.length);
}
return localUri;
if (localPath.charCodeAt(localPath.length - 1) === slash) {
localPath = localPath.slice(0, -1);
}
return localUri.with({ path: localPath });
}
}

+ 7
- 6
src/vsls/protocol.ts 查看文件

@ -19,20 +19,21 @@ export const GitCommandRequestType = new RequestType
export interface RepositoryProxy {
folderUri: string;
uri: string;
/** @deprecated */
path?: string;
root: boolean;
closed: boolean;
}
export interface RepositoriesInFolderRequest {
export interface GetRepositoriesForUriRequest {
folderUri: string;
}
export interface RepositoriesInFolderResponse {
export interface GetRepositoriesForUriResponse {
repositories: RepositoryProxy[];
}
export const RepositoriesInFolderRequestType = new RequestType<
RepositoriesInFolderRequest,
RepositoriesInFolderResponse
export const GetRepositoriesForUriRequestType = new RequestType<
GetRepositoriesForUriRequest,
GetRepositoriesForUriResponse
>('repositories/inFolder');

+ 66
- 69
src/vsls/vsls.ts 查看文件

@ -3,13 +3,13 @@ import type { LiveShare, LiveShareExtension, SessionChangeEvent } from '../@type
import { ContextKeys, DocumentSchemes, setContext } from '../constants';
import { Container } from '../container';
import { Logger } from '../logger';
import { debug, timeout } from '../system';
import { debug } from '../system/decorators/log';
import { timeout } from '../system/decorators/timeout';
import { once } from '../system/event';
import { defer, Deferred } from '../system/promise';
import { VslsGuestService } from './guest';
import { VslsHostService } from './host';
export const vslsUriPrefixRegex = /^[/|\\]~(?:\d+?|external)(?:[/|\\]|$)/;
export const vslsUriRootRegex = /^[/|\\]~(?:\d+?|external)$/;
export interface ContactPresence {
status: ContactPresenceStatus;
statusText: string;
@ -32,20 +32,20 @@ function contactStatusToPresence(status: string | undefined): ContactPresence {
}
export class VslsController implements Disposable {
private _api: Promise<LiveShare | undefined> | undefined;
private _disposable: Disposable;
private _guest: VslsGuestService | undefined;
private _host: VslsHostService | undefined;
private _onReady: (() => void) | undefined;
private _waitForReady: Promise<void> | undefined;
private _api: Promise<LiveShare | undefined> | undefined;
private _ready: Deferred<void>;
constructor(private readonly container: Container) {
this._disposable = Disposable.from(container.onReady(this.onReady, this));
this._ready = defer<void>();
this._disposable = Disposable.from(once(container.onReady)(this.onReady, this));
}
dispose() {
this._ready.fulfill();
this._disposable.dispose();
this._host?.dispose();
this._guest?.dispose();
@ -56,22 +56,20 @@ export class VslsController implements Disposable {
}
private async initialize() {
try {
// If we have a vsls: workspace open, we might be a guest, so wait until live share transitions into a mode
if (workspace.workspaceFolders?.some(f => f.uri.scheme === DocumentSchemes.Vsls)) {
this.setReadonly(true);
this._waitForReady = new Promise(resolve => (this._onReady = resolve));
}
// If we have a vsls: workspace open, we might be a guest, so wait until live share transitions into a mode
if (workspace.workspaceFolders?.some(f => f.uri.scheme === DocumentSchemes.Vsls)) {
this.setReadonly(true);
}
try {
this._api = this.getLiveShareApi();
const api = await this._api;
if (api == null) {
debugger;
void setContext(ContextKeys.Vsls, false);
// Tear it down if we can't talk to live share
if (this._onReady !== undefined) {
this._onReady();
this._waitForReady = undefined;
}
this._ready.fulfill();
return;
}
@ -82,8 +80,47 @@ export class VslsController implements Disposable {
this._disposable,
api.onDidChangeSession(e => this.onLiveShareSessionChanged(api, e), this),
);
void this.onLiveShareSessionChanged(api, { session: api.session });
} catch (ex) {
Logger.error(ex);
debugger;
}
}
private async onLiveShareSessionChanged(api: LiveShare, e: SessionChangeEvent) {
this._host?.dispose();
this._host = undefined;
this._guest?.dispose();
this._guest = undefined;
switch (e.session.role) {
case 1 /*Role.Host*/:
this.setReadonly(false);
void setContext(ContextKeys.Vsls, 'host');
if (this.container.config.liveshare.allowGuestAccess) {
this._host = await VslsHostService.share(api, this.container);
}
this._ready.fulfill();
break;
case 2 /*Role.Guest*/:
this.setReadonly(true);
void setContext(ContextKeys.Vsls, 'guest');
this._guest = await VslsGuestService.connect(api, this.container);
this._ready.fulfill();
break;
default:
this.setReadonly(false);
void setContext(ContextKeys.Vsls, true);
this._ready = defer<void>();
break;
}
}
@ -92,17 +129,13 @@ export class VslsController implements Disposable {
const extension = extensions.getExtension<LiveShareExtension>('ms-vsliveshare.vsliveshare');
if (extension != null) {
const liveshareExtension = extension.isActive ? extension.exports : await extension.activate();
return (await liveshareExtension.getApi('1.0.3015')) ?? undefined;
return (await liveshareExtension.getApi('1.0.4753')) ?? undefined;
}
} catch {}
return undefined;
}
get isMaybeGuest() {
return this._guest !== undefined || this._waitForReady !== undefined;
}
private _readonly: boolean = false;
get readonly() {
return this._readonly;
@ -112,9 +145,16 @@ export class VslsController implements Disposable {
void setContext(ContextKeys.Readonly, value ? true : undefined);
}
async guest() {
if (this._guest != null) return this._guest;
await this._ready.promise;
return this._guest;
}
@debug()
async getContact(email: string | undefined) {
if (email === undefined) return undefined;
if (email == null) return undefined;
const api = await this._api;
if (api == null) return undefined;
@ -171,47 +211,4 @@ export class VslsController implements Disposable {
return api.share();
}
async guest() {
if (this._waitForReady !== undefined) {
await this._waitForReady;
this._waitForReady = undefined;
}
return this._guest;
}
host() {
return this._host;
}
private async onLiveShareSessionChanged(api: LiveShare, e: SessionChangeEvent) {
this._host?.dispose();
this._guest?.dispose();
switch (e.session.role) {
case 1 /*Role.Host*/:
this.setReadonly(false);
void setContext(ContextKeys.Vsls, 'host');
if (this.container.config.liveshare.allowGuestAccess) {
this._host = await VslsHostService.share(api, this.container);
}
break;
case 2 /*Role.Guest*/:
this.setReadonly(true);
void setContext(ContextKeys.Vsls, 'guest');
this._guest = await VslsGuestService.connect(api, this.container);
break;
default:
this.setReadonly(false);
void setContext(ContextKeys.Vsls, true);
break;
}
if (this._onReady !== undefined) {
this._onReady();
this._onReady = undefined;
}
}
}

正在加载...
取消
保存