Browse Source

Fixes #198 - submodules & nested repos not working

main
Eric Amodio 7 years ago
parent
commit
6461b2fe4f
12 changed files with 657 additions and 73 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +2
    -1
      README.md
  3. +6
    -0
      package.json
  4. +2
    -0
      src/configuration.ts
  5. +16
    -4
      src/git/models/repository.ts
  6. +211
    -58
      src/gitService.ts
  7. +4
    -1
      src/system.ts
  8. +13
    -0
      src/system/asyncIterable.ts
  9. +4
    -4
      src/system/function.ts
  10. +12
    -3
      src/system/iterable.ts
  11. +384
    -0
      src/system/searchTree.ts
  12. +2
    -2
      src/views/gitExplorer.ts

+ 1
- 0
CHANGELOG.md View File

@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Changes to use `diff.guitool` first if available, before falling back to `diff.tool` -- closes [#195](https://github.com/eamodio/vscode-gitlens/issues/195)
### Fixed
- Fixes [#198](https://github.com/eamodio/vscode-gitlens/issues/198) - Files in submodules or nested repositories no longer work properly
- Fixes issue where failed git commands would get stuck in the pending queue causing future similar commands to also fail
- Fixes issue where changes to git remotes would refresh the entire `GitLens` view

+ 2
- 1
README.md View File

@ -460,10 +460,11 @@ GitLens is highly customizable and provides many configuration settings to allow
|Name | Description
|-----|------------
|`gitlens.advanced.telemetry.enabled`|Specifies whether or not to enable GitLens telemetry (even if enabled still abides by the overall `telemetry.enableTelemetry` setting
|`gitlens.advanced.git`|Specifies the git path to use
|`gitlens.advanced.repositorySearchDepth`|Specifies how many folders deep to search for repositories
|`gitlens.advanced.menus`|Specifies which commands will be added to which menus
|`gitlens.advanced.caching.enabled`|Specifies whether git output will be cached
|`gitlens.advanced.caching.maxLines`|Specifies the threshold for caching larger documents
|`gitlens.advanced.git`|Specifies the git path to use
|`gitlens.advanced.maxQuickHistory`|Specifies the maximum number of QuickPick history entries to show
|`gitlens.advanced.quickPick.closeOnFocusOut`|Specifies whether or not to close the QuickPick menu when focus is lost

+ 6
- 0
package.json View File

@ -980,6 +980,12 @@
"description": "Specifies whether or not to close the QuickPick menu when focus is lost",
"scope": "window"
},
"gitlens.advanced.repositorySearchDepth": {
"type": "number",
"default": 1,
"description": "Specifies how many folders deep to search for repositories",
"scope": "resource"
},
"gitlens.advanced.telemetry.enabled": {
"type": "boolean",
"default": true,

+ 2
- 0
src/configuration.ts View File

@ -93,6 +93,7 @@ export interface IAdvancedConfig {
quickPick: {
closeOnFocusOut: boolean;
};
repositorySearchDepth: number;
telemetry: {
enabled: boolean;
};
@ -518,6 +519,7 @@ const emptyConfig: IConfig = {
quickPick: {
closeOnFocusOut: false
},
repositorySearchDepth: 0,
telemetry: {
enabled: false
}

+ 16
- 4
src/git/models/repository.ts View File

@ -5,6 +5,7 @@ import { configuration, IRemotesConfig } from '../../configuration';
import { GitBranch, GitDiffShortStat, GitRemote, GitStash, GitStatus } from '../git';
import { GitService, GitUri } from '../../gitService';
import { RemoteProviderFactory, RemoteProviderMap } from '../remotes/factory';
import * as _path from 'path';
export enum RepositoryChange {
Config = 'config',
@ -70,13 +71,14 @@ export class Repository extends Disposable {
private _fsWatchCounter = 0;
private _fsWatcherDisposable: Disposable | undefined;
private _pendingChanges: { repo?: RepositoryChangeEvent, fs?: RepositoryFileSystemChangeEvent } = { };
private _providerMap: RemoteProviderMap;
private _providerMap: RemoteProviderMap | undefined;
private _remotes: GitRemote[] | undefined;
private _suspended: boolean;
constructor(
private readonly folder: WorkspaceFolder,
public readonly folder: WorkspaceFolder,
public readonly path: string,
public readonly root: boolean,
private readonly git: GitService,
private readonly onAnyRepositoryChanged: () => void,
suspended: boolean
@ -84,7 +86,10 @@ export class Repository extends Disposable {
super(() => this.dispose());
this.index = folder.index;
this.name = folder.name;
this.name = root
? folder.name
: `${folder.name} (${_path.relative(folder.uri.fsPath, path)})`;
this.normalizedPath = (this.path.endsWith('/') ? this.path : `${this.path}/`).toLowerCase();
this._suspended = suspended;
@ -118,7 +123,9 @@ export class Repository extends Disposable {
const section = configuration.name('remotes').value;
if (initializing || configuration.changed(e, section, this.folder.uri)) {
this._providerMap = RemoteProviderFactory.createMap(configuration.get<IRemotesConfig[] | null | undefined>(section, this.folder.uri));
// Can't reset the provider map here because of https://github.com/Microsoft/vscode/issues/38229
// this._providerMap = RemoteProviderFactory.createMap(configuration.get<IRemotesConfig[] | null | undefined>(section, this.folder.uri));
this._providerMap = undefined;
if (!initializing) {
this._remotes = undefined;
@ -234,6 +241,11 @@ export class Repository extends Disposable {
async getRemotes(): Promise<GitRemote[]> {
if (this._remotes === undefined) {
if (this._providerMap === undefined) {
const remotesCfg = configuration.get<IRemotesConfig[] | null | undefined>(configuration.name('remotes').value, this.folder.uri);
this._providerMap = RemoteProviderFactory.createMap(remotesCfg);
}
this._remotes = await this.git.getRemotesCore(this.path, this._providerMap);
}

+ 211
- 58
src/gitService.ts View File

@ -1,6 +1,6 @@
'use strict';
import { Functions, Iterables } from './system';
import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFoldersChangeEvent } from 'vscode';
import { Functions, Iterables, Objects, TernarySearchTree } from './system';
import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
import { configuration, IConfig, IRemotesConfig } from './configuration';
import { CommandContext, DocumentSchemes, setCommandContext } from './constants';
import { RemoteProviderFactory, RemoteProviderMap } from './git/remotes/factory';
@ -100,8 +100,8 @@ export class GitService extends Disposable {
private _disposable: Disposable | undefined;
private _documentKeyMap: Map<TextDocument, string>;
private _gitCache: Map<string, GitCacheEntry>;
private _repositories: Map<string, Repository | undefined>;
private _repositoriesPromise: Promise<void> | undefined;
private _repositoryTree: TernarySearchTree<Repository>;
private _repositoriesLoadingPromise: Promise<void> | undefined;
private _suspended: boolean = false;
private _trackedCache: Map<string, boolean | Promise<boolean>>;
private _versionedUriCache: Map<string, UriCacheEntry>;
@ -111,7 +111,7 @@ export class GitService extends Disposable {
this._documentKeyMap = new Map();
this._gitCache = new Map();
this._repositories = new Map();
this._repositoryTree = TernarySearchTree.forPaths();
this._trackedCache = new Map();
this._versionedUriCache = new Map();
@ -121,11 +121,11 @@ export class GitService extends Disposable {
configuration.onDidChange(this.onConfigurationChanged, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
this._repositoriesPromise = this.onWorkspaceFoldersChanged();
this._repositoriesLoadingPromise = this.onWorkspaceFoldersChanged();
}
dispose() {
this._repositories.forEach(r =>; r &&; r.dispose());
this._repositoryTree.forEach(r => r.dispose());
this._disposable && this._disposable.dispose();
@ -139,10 +139,11 @@ export class GitService extends Disposable {
}
get repoPath(): string | undefined {
if (this._repositories.size !== 1) return undefined;
const entry = this._repositoryTree.highlander();
if (entry === undefined) return undefined;
const repo = Iterables.first(this._repositories.values());
return repo === undefined ? undefined : repo.path;
const [repo] = entry;
return repo.path;
}
get UseCaching() {
@ -213,10 +214,10 @@ export class GitService extends Disposable {
private onWindowStateChanged(e: WindowState) {
if (e.focused) {
this._repositories.forEach(r =>; r &&; r.resume());
this._repositoryTree.forEach(r => r.resume());
}
else {
this._repositories.forEach(r =>; r &&; r.suspend());
this._repositoryTree.forEach(r => r.suspend());
}
this._suspended = !e.focused;
@ -235,45 +236,163 @@ export class GitService extends Disposable {
for (const f of e.added) {
if (f.uri.scheme !== DocumentSchemes.File) continue;
const fsPath = f.uri.fsPath;
const rp = await this.getRepoPathCore(fsPath, true);
if (rp === undefined) {
Logger.log(`onWorkspaceFoldersChanged(${fsPath})`, 'No repository found');
this._repositories.set(fsPath, undefined);
}
else {
this._repositories.set(fsPath, new Repository(f, rp, this, this.onAnyRepositoryChanged.bind(this), this._suspended));
// Search for and add all repositories (nested and/or submodules)
const repositories = await this.repositorySearch(f);
for (const r of repositories) {
this._repositoryTree.set(r.path, r);
}
// const repoPaths = await this.searchForRepositories(f);
// if (repoPaths.length !== 0) {
// debugger;
// }
}
for (const f of e.removed) {
if (f.uri.scheme !== DocumentSchemes.File) continue;
const repo = this._repositories.get(f.uri.fsPath);
const fsPath = f.uri.fsPath;
const filteredTree = this._repositoryTree.findSuperstr(fsPath);
const reposToDelete = filteredTree !== undefined
// Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path
? [...Iterables.map<[Repository, string], [Repository, string]>(filteredTree.entries(), ([r, k]) => [r, path.join(fsPath, k)])]
: [];
const repo = this._repositoryTree.get(fsPath);
if (repo !== undefined) {
repo.dispose();
reposToDelete.push([repo, fsPath]);
}
this._repositories.delete(f.uri.fsPath);
for (const [r, k] of reposToDelete) {
this._repositoryTree.delete(k);
r.dispose();
}
}
const hasRepository = Iterables.some(this._repositories.values(), rp => rp !== undefined);
await setCommandContext(CommandContext.HasRepository, hasRepository);
await setCommandContext(CommandContext.HasRepository, this._repositoryTree.any());
if (!initializing) {
this.fireChange(GitChangeReason.Repositories);
// Defer the event trigger enough to let everything unwind
setTimeout(() => this.fireChange(GitChangeReason.Repositories), 1);
}
}
// private async searchForRepositories(folder: WorkspaceFolder): Promise<string[]> {
// const uris = await workspace.findFiles(new RelativePattern(folder, '**/.git/HEAD'));
// return Arrays.filterMapAsync(uris, uri => this.getRepoPathCore(path.dirname(uri.fsPath), true));
// }
private async repositorySearch(folder: WorkspaceFolder): Promise<Repository[]> {
const folderUri = folder.uri;
const repositories: Repository[] = [];
const anyRepoChangedFn = this.onAnyRepositoryChanged.bind(this);
const rootPath = await this.getRepoPathCore(folderUri.fsPath, true);
await this.getRepoPathCore(folderUri.fsPath, true);
if (rootPath !== undefined) {
repositories.push(new Repository(folder, rootPath, true, this, anyRepoChangedFn, this._suspended));
}
// Can remove this try/catch once https://github.com/Microsoft/vscode/issues/38229 is fixed
let depth = 1;
try {
depth = configuration.get<number>(configuration.name('advanced')('repositorySearchDepth').value, folderUri);
}
catch (ex) {
Logger.error(ex);
depth = configuration.get<number>(configuration.name('advanced')('repositorySearchDepth').value, null);
}
if (depth <= 0) return repositories;
// Can remove this try/catch once https://github.com/Microsoft/vscode/issues/38229 is fixed
let excludes = {};
try {
// Get any specified excludes -- this is a total hack, but works for some simple cases and something is better than nothing :)
excludes = {
...workspace.getConfiguration('files', folderUri).get<{ [key: string]: boolean }>('exclude', {}),
...workspace.getConfiguration('search', folderUri).get<{ [key: string]: boolean }>('exclude', {})
};
}
catch (ex) {
Logger.error(ex);
excludes = {
...workspace.getConfiguration('files', null!).get<{ [key: string]: boolean }>('exclude', {}),
...workspace.getConfiguration('search', null!).get<{ [key: string]: boolean }>('exclude', {})
};
}
const excludedPaths = [...Iterables.filterMap(Objects.entries(excludes), ([key, value]) => {
if (!value) return undefined;
if (key.startsWith('**/')) return key.substring(3);
return key;
})];
excludes = excludedPaths.reduce((accumulator, current) => {
accumulator[current] = true;
return accumulator;
}, Object.create(null) as any);
const start = process.hrtime();
const paths = await this.repositorySearchCore(folderUri.fsPath, depth, excludes);
const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to search (depth=${depth}) for repositories in ${folderUri.fsPath}`);
for (const p of paths) {
const rp = await this.getRepoPathCore(path.dirname(p), true);
if (rp !== undefined && rp !== rootPath) {
repositories.push(new Repository(folder, rp, false, this, anyRepoChangedFn, this._suspended));
}
}
// const uris = await workspace.findFiles(new RelativePattern(folder, '**/.git/HEAD'));
// for (const uri of uris) {
// const rp = await this.getRepoPathCore(path.resolve(path.dirname(uri.fsPath), '../'), true);
// if (rp !== undefined && rp !== rootPath) {
// repositories.push(new Repository(folder, rp, false, this, anyRepoChangedFn, this._suspended));
// }
// }
return repositories;
}
private async repositorySearchCore(root: string, depth: number, excludes: { [key: string]: boolean }, repositories: string[] = []): Promise<string[]> {
return new Promise<string[]>((resolve, reject) => {
fs.readdir(root, async (err, files) => {
if (err != null) {
reject(err);
return;
}
if (files.length === 0) {
resolve(repositories);
return;
}
const folders: string[] = [];
const promises = files.map(file => {
const fullPath = path.resolve(root, file);
return new Promise<void>((res, rej) => {
fs.stat(fullPath, (err, stat) => {
if (file === '.git') {
repositories.push(fullPath);
}
else if (err == null && excludes[file] !== true && stat != null && stat.isDirectory()) {
folders.push(fullPath);
}
res();
});
});
});
await Promise.all(promises);
if (depth-- > 0) {
for (const folder of folders) {
await this.repositorySearchCore(folder, depth, excludes, repositories);
}
}
resolve(repositories);
});
});
}
private fireChange(reason: GitChangeReason) {
this._onDidChange.fire({ reason: reason });
@ -910,51 +1029,85 @@ export class GitService extends Disposable {
if (filePathOrUri === undefined) return this.repoPath;
if (filePathOrUri instanceof GitUri) return filePathOrUri.repoPath;
if (typeof filePathOrUri === 'string') return this.getRepoPathCore(filePathOrUri, false);
const repo = await this.getRepository(filePathOrUri);
if (repo !== undefined) return repo.path;
return this.getRepoPathCore(filePathOrUri.fsPath, false);
const rp = await this.getRepoPathCore(typeof filePathOrUri === 'string' ? filePathOrUri : filePathOrUri.fsPath, false);
if (rp === undefined) return undefined;
// Recheck this._repositoryTree.get(rp) to make sure we haven't already tried adding this due to awaits
if (this._repositoryTree.get(rp) !== undefined) return rp;
// If this new repo is inside one of our known roots and we we don't already know about, add it
const root = this._repositoryTree.findSubstr(rp);
const folder = root === undefined
? workspace.getWorkspaceFolder(Uri.file(rp))
: root.folder;
if (folder !== undefined) {
const repo = new Repository(folder, rp, false, this, this.onAnyRepositoryChanged.bind(this), this._suspended);
this._repositoryTree.set(rp, repo);
// Send a notification that the repositories changed
setTimeout(async () => {
await setCommandContext(CommandContext.HasRepository, this._repositoryTree.any());
this.fireChange(GitChangeReason.Repositories);
}, 0);
}
return rp;
}
private getRepoPathCore(filePath: string, isDirectory: boolean): Promise<string | undefined> {
return Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath));
}
async getRepositories(): Promise<Repository[]> {
const repositories = await this.getRepositoriesCore();
return [...Iterables.filter(repositories.values(), r => r !== undefined) as Iterable<Repository>];
async getRepositories(): Promise<Iterable<Repository>> {
const repositoryTree = await this.getRepositoryTree();
return repositoryTree.values();
}
private async getRepositoriesCore(): Promise<Map<string, Repository | undefined>> {
if (this._repositoriesPromise !== undefined) {
await this._repositoriesPromise;
this._repositoriesPromise = undefined;
private async getRepositoryTree(): Promise<TernarySearchTree<Repository>> {
if (this._repositoriesLoadingPromise !== undefined) {
await this._repositoriesLoadingPromise;
this._repositoriesLoadingPromise = undefined;
}
return this._repositories;
return this._repositoryTree;
}
async getRepository(repoPath: string): Promise<Repository | undefined>;
async getRepository(uri: Uri): Promise<Repository | undefined>;
async getRepository(repoPathOrUri: string | Uri): Promise<Repository | undefined>;
async getRepository(repoPathOrUri: string | Uri): Promise<Repository | undefined> {
if (repoPathOrUri instanceof GitUri) {
repoPathOrUri = repoPathOrUri.repoPath !== undefined
? Uri.file(repoPathOrUri.repoPath)
: repoPathOrUri.fileUri();
}
const repositoryTree = await this.getRepositoryTree();
let path: string;
if (typeof repoPathOrUri === 'string') {
const repositories = await this.getRepositoriesCore();
return Iterables.find(repositories.values(), r => r !== undefined && r.path === repoPathOrUri) || undefined;
const repo = repositoryTree.get(repoPathOrUri);
if (repo !== undefined) return repo;
path = repoPathOrUri;
}
else {
if (repoPathOrUri instanceof GitUri) {
const repo = repositoryTree.get(repoPathOrUri.repoPath!);
if (repo !== undefined) return repo;
path = repoPathOrUri.fsPath;
}
else {
path = repoPathOrUri.fsPath;
}
}
const folder = workspace.getWorkspaceFolder(repoPathOrUri);
if (folder === undefined) return undefined;
const repo = repositoryTree.findSubstr(path);
if (repo === undefined) return undefined;
const repositories = await this.getRepositoriesCore();
return repositories.get(folder.uri.fsPath);
// Make sure the file is tracked in that repo, before returning
if (!await this.isTrackedCore(repo.path, path)) return undefined;
return repo;
}
async getStashList(repoPath: string | undefined): Promise<GitStash | undefined> {
@ -1113,7 +1266,7 @@ export class GitService extends Disposable {
}
stopWatchingFileSystem() {
this._repositories.forEach(r =>; r &&; r.stopWatchingFileSystem());
this._repositoryTree.forEach(r => r.stopWatchingFileSystem());
}
stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) {

+ 4
- 1
src/system.ts View File

@ -1,5 +1,7 @@
'use strict';
export * from './system/array';
// export * from './system/asyncIterable';
export * from './system/date';
// export * from './system/disposable';
// export * from './system/element';
@ -10,4 +12,5 @@ export * from './system/function';
export * from './system/iterable';
// export * from './system/map';
export * from './system/object';
export * from './system/string';
export * from './system/searchTree';
export * from './system/string';

+ 13
- 0
src/system/asyncIterable.ts View File

@ -0,0 +1,13 @@
// 'use strict';
// // Polyfill for asyncIterator
// (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol.for('Symbol.asyncIterator');
// export namespace AsyncIterables {
// export async function* filterMap<T, TMapped>(source: Iterable<T>, predicateMapper: (item: T) => Promise<TMapped | null | undefined>): AsyncIterator<TMapped> {
// for (const item of source) {
// const mapped = await predicateMapper(item);
// if (mapped != null) yield mapped;
// }
// }
// }

+ 4
- 4
src/system/function.ts View File

@ -17,6 +17,10 @@ export namespace Functions {
return _debounce(fn, wait, options);
}
export function once<T extends Function>(fn: T): T {
return _once(fn);
}
export function propOf<T, K extends keyof T>(o: T, key: K) {
const propOfCore = <T, K extends keyof T>(o: T, key: K) => {
const value: string = (propOfCore as IPropOfValue).value === undefined
@ -29,10 +33,6 @@ export namespace Functions {
return propOfCore(o, key);
}
export function once<T extends Function>(fn: T): T {
return _once(fn);
}
export async function wait(ms: number) {
await new Promise(resolve => setTimeout(resolve, ms));
}

+ 12
- 3
src/system/iterable.ts View File

@ -8,9 +8,18 @@ export namespace Iterables {
return true;
}
export function* filter<T>(source: Iterable<T> | IterableIterator<T>, predicate: (item: T) => boolean): Iterable<T> {
for (const item of source) {
if (predicate(item)) yield item;
export function filter<T>(source: Iterable<T | undefined | null> | IterableIterator<T | undefined | null>): Iterable<T>;
export function filter<T>(source: Iterable<T> | IterableIterator<T>, predicate: (item: T) => boolean): Iterable<T>;
export function* filter<T>(source: Iterable<T> | IterableIterator<T>, predicate?: (item: T) => boolean): Iterable<T> {
if (predicate === undefined) {
for (const item of source) {
if (item != null) yield item;
}
}
else {
for (const item of source) {
if (predicate(item)) yield item;
}
}
}

+ 384
- 0
src/system/searchTree.ts View File

@ -0,0 +1,384 @@
'use strict';
import { Iterables } from '../system/iterable';
// Code stolen from https://github.com/Microsoft/vscode/blob/b3e6d5bb039a4a9362b52a2c8726267ca68cf64e/src/vs/base/common/map.ts#L352
export interface IKeyIterator {
reset(key: string): this;
next(): this;
join(parts: string[]): string;
hasNext(): boolean;
cmp(a: string): number;
value(): string;
}
export class StringIterator implements IKeyIterator {
private _value: string = '';
private _pos: number = 0;
reset(key: string): this {
this._value = key;
this._pos = 0;
return this;
}
next(): this {
this._pos += 1;
return this;
}
join(parts: string[]): string {
return parts.join('');
}
hasNext(): boolean {
return this._pos < this._value.length - 1;
}
cmp(a: string): number {
const aCode = a.charCodeAt(0);
const thisCode = this._value.charCodeAt(this._pos);
return aCode - thisCode;
}
value(): string {
return this._value[this._pos];
}
}
export class PathIterator implements IKeyIterator {
private static _fwd = '/'.charCodeAt(0);
private static _bwd = '\\'.charCodeAt(0);
private _value: string;
private _from: number;
private _to: number;
reset(key: string): this {
this._value = key.replace(/\\$|\/$/, '');
this._from = 0;
this._to = 0;
return this.next();
}
hasNext(): boolean {
return this._to < this._value.length;
}
join(parts: string[]): string {
return parts.join('/');
}
next(): this {
// this._data = key.split(/[\\/]/).filter(s => !!s);
this._from = this._to;
let justSeps = true;
for (; this._to < this._value.length; this._to++) {
const ch = this._value.charCodeAt(this._to);
if (ch === PathIterator._fwd || ch === PathIterator._bwd) {
if (justSeps) {
this._from++;
} else {
break;
}
} else {
justSeps = false;
}
}
return this;
}
cmp(a: string): number {
let aPos = 0;
const aLen = a.length;
let thisPos = this._from;
while (aPos < aLen && thisPos < this._to) {
const cmp = a.charCodeAt(aPos) - this._value.charCodeAt(thisPos);
if (cmp !== 0) {
return cmp;
}
aPos += 1;
thisPos += 1;
}
if (aLen === this._to - this._from) {
return 0;
} else if (aPos < aLen) {
return -1;
} else {
return 1;
}
}
value(): string {
return this._value.substring(this._from, this._to);
}
}
class TernarySearchTreeNode<E> {
str: string;
element: E | undefined;
left: TernarySearchTreeNode<E> | undefined;
mid: TernarySearchTreeNode<E> | undefined;
right: TernarySearchTreeNode<E> | undefined;
isEmpty(): boolean {
return this.left === undefined && this.mid === undefined && this.right === undefined && this.element === undefined;
}
}
export class TernarySearchTree<E> {
static forPaths<E>(): TernarySearchTree<E> {
return new TernarySearchTree<E>(new PathIterator());
}
static forStrings<E>(): TernarySearchTree<E> {
return new TernarySearchTree<E>(new StringIterator());
}
private _iter: IKeyIterator;
private _root: TernarySearchTreeNode<E> | undefined;
constructor(segments: IKeyIterator) {
this._iter = segments;
}
clear(): void {
this._root = undefined;
}
set(key: string, element: E): void {
const iter = this._iter.reset(key);
let node: TernarySearchTreeNode<E>;
if (!this._root) {
this._root = new TernarySearchTreeNode<E>();
this._root.str = iter.value();
}
node = this._root;
while (true) {
const val = iter.cmp(node.str);
if (val > 0) {
// left
if (!node.left) {
node.left = new TernarySearchTreeNode<E>();
node.left.str = iter.value();
}
node = node.left;
} else if (val < 0) {
// right
if (!node.right) {
node.right = new TernarySearchTreeNode<E>();
node.right.str = iter.value();
}
node = node.right;
} else if (iter.hasNext()) {
// mid
iter.next();
if (!node.mid) {
node.mid = new TernarySearchTreeNode<E>();
node.mid.str = iter.value();
}
node = node.mid;
} else {
break;
}
}
node.element = element;
}
get(key: string): E | undefined {
const iter = this._iter.reset(key);
let node = this._root;
while (node) {
const val = iter.cmp(node.str);
if (val > 0) {
// left
node = node.left;
} else if (val < 0) {
// right
node = node.right;
} else if (iter.hasNext()) {
// mid
iter.next();
node = node.mid;
} else {
break;
}
}
return node ? node.element : undefined;
}
delete(key: string): void {
const iter = this._iter.reset(key);
const stack: [-1 | 0 | 1, TernarySearchTreeNode<E>][] = [];
let node = this._root;
// find and unset node
while (node) {
const val = iter.cmp(node.str);
if (val > 0) {
// left
stack.push([1, node]);
node = node.left;
} else if (val < 0) {
// right
stack.push([-1, node]);
node = node.right;
} else if (iter.hasNext()) {
// mid
iter.next();
stack.push([0, node]);
node = node.mid;
} else {
// remove element
node.element = undefined;
// clean up empty nodes
while (stack.length > 0 && node.isEmpty()) {
const [dir, parent] = stack.pop()!;
switch (dir) {
case 1: parent.left = undefined; break;
case 0: parent.mid = undefined; break;
case -1: parent.right = undefined; break;
}
node = parent;
}
break;
}
}
}
findSubstr(key: string): E | undefined {
const iter = this._iter.reset(key);
let node = this._root;
let candidate: E | undefined;
while (node) {
const val = iter.cmp(node.str);
if (val > 0) {
// left
node = node.left;
} else if (val < 0) {
// right
node = node.right;
} else if (iter.hasNext()) {
// mid
iter.next();
candidate = node.element || candidate;
node = node.mid;
} else {
break;
}
}
return node && node.element || candidate;
}
findSuperstr(key: string): TernarySearchTree<E> | undefined {
const iter = this._iter.reset(key);
let node = this._root;
while (node) {
const val = iter.cmp(node.str);
if (val > 0) {
// left
node = node.left;
} else if (val < 0) {
// right
node = node.right;
} else if (iter.hasNext()) {
// mid
iter.next();
node = node.mid;
} else {
// collect
if (!node.mid) {
return undefined;
}
const ret = new TernarySearchTree<E>(this._iter);
ret._root = node.mid;
return ret;
}
}
return undefined;
}
forEach(callback: (value: E, index: string) => any) {
this._forEach(this._root!, [], callback);
}
private _forEach(node: TernarySearchTreeNode<E>, parts: string[], callback: (value: E, index: string) => any) {
if (node === undefined) return;
// left
this._forEach(node.left!, parts, callback);
// node
parts.push(node.str);
if (node.element) {
callback(node.element, this._iter.join(parts));
}
// mid
this._forEach(node.mid!, parts, callback);
parts.pop();
// right
this._forEach(node.right!, parts, callback);
}
any(): boolean {
return this._root !== undefined && !this._root.isEmpty();
}
entries(): Iterable<[E, string]> {
return this._iterator(this._root!, []);
}
values(): Iterable<E> {
return Iterables.map(this.entries(), e => e[0]);
}
highlander(): [E, string] | undefined {
if (this._root === undefined || this._root.isEmpty()) return undefined;
const entries = this.entries() as IterableIterator<[E, string]>;
let count = 0;
let next: IteratorResult<[E, string]>;
while (true) {
next = entries.next();
if (next.done) break;
count++;
if (count > 1) return undefined;
}
return next.value;
}
private *_iterator(node: TernarySearchTreeNode<E> | undefined, parts: string[]): IterableIterator<[E, string]> {
if (node !== undefined) {
// left
yield* this._iterator(node.left!, parts);
// node
parts.push(node.str);
if (node.element) {
yield [node.element, this._iter.join(parts)];
}
// mid
yield* this._iterator(node.mid!, parts);
parts.pop();
// right
yield* this._iterator(node.right!, parts);
}
}
}

+ 2
- 2
src/views/gitExplorer.ts View File

@ -109,7 +109,7 @@ export class GitExplorer implements TreeDataProvider {
}
private onGitChanged(e: GitChangeEvent) {
if (this._root === undefined || this._view !== GitExplorerView.Repository || e.reason !== GitChangeReason.Repositories) return;
if (this._view !== GitExplorerView.Repository || e.reason !== GitChangeReason.Repositories) return;
this.clearRoot();
@ -170,7 +170,7 @@ export class GitExplorer implements TreeDataProvider {
const promise = this.git.getRepositories();
this._loading = promise.then(async _ => await Functions.wait(0));
const repositories = await promise;
const repositories = [...await promise];
if (repositories.length === 0) return undefined; // new MessageNode('No repositories found');
if (repositories.length === 1) {

Loading…
Cancel
Save