diff --git a/CHANGELOG.md b/CHANGELOG.md index 1627a69..bbc28ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 05b3550..d8f9b18 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index dc6119a..8952d51 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/src/configuration.ts b/src/configuration.ts index 59404cc..b9d63a7 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -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 } diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index c060430..8b06423 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -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(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(section, this.folder.uri)); + this._providerMap = undefined; if (!initializing) { this._remotes = undefined; @@ -234,6 +241,11 @@ export class Repository extends Disposable { async getRemotes(): Promise { if (this._remotes === undefined) { + if (this._providerMap === undefined) { + const remotesCfg = configuration.get(configuration.name('remotes').value, this.folder.uri); + this._providerMap = RemoteProviderFactory.createMap(remotesCfg); + } + this._remotes = await this.git.getRemotesCore(this.path, this._providerMap); } diff --git a/src/gitService.ts b/src/gitService.ts index 574d304..ba57954 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -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; private _gitCache: Map; - private _repositories: Map; - private _repositoriesPromise: Promise | undefined; + private _repositoryTree: TernarySearchTree; + private _repositoriesLoadingPromise: Promise | undefined; private _suspended: boolean = false; private _trackedCache: Map>; private _versionedUriCache: Map; @@ -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 { - // 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 { + 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(configuration.name('advanced')('repositorySearchDepth').value, folderUri); + } + catch (ex) { + Logger.error(ex); + depth = configuration.get(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 { + return new Promise((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((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 { return Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath)); } - async getRepositories(): Promise { - const repositories = await this.getRepositoriesCore(); - return [...Iterables.filter(repositories.values(), r => r !== undefined) as Iterable]; + async getRepositories(): Promise> { + const repositoryTree = await this.getRepositoryTree(); + return repositoryTree.values(); } - private async getRepositoriesCore(): Promise> { - if (this._repositoriesPromise !== undefined) { - await this._repositoriesPromise; - this._repositoriesPromise = undefined; + private async getRepositoryTree(): Promise> { + if (this._repositoriesLoadingPromise !== undefined) { + await this._repositoriesLoadingPromise; + this._repositoriesLoadingPromise = undefined; } - return this._repositories; + return this._repositoryTree; } async getRepository(repoPath: string): Promise; async getRepository(uri: Uri): Promise; + async getRepository(repoPathOrUri: string | Uri): Promise; async getRepository(repoPathOrUri: string | Uri): Promise { - 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 { @@ -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) { diff --git a/src/system.ts b/src/system.ts index 90e8186..11ed5a1 100644 --- a/src/system.ts +++ b/src/system.ts @@ -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'; \ No newline at end of file +export * from './system/searchTree'; +export * from './system/string'; diff --git a/src/system/asyncIterable.ts b/src/system/asyncIterable.ts new file mode 100644 index 0000000..2d81ca3 --- /dev/null +++ b/src/system/asyncIterable.ts @@ -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(source: Iterable, predicateMapper: (item: T) => Promise): AsyncIterator { +// for (const item of source) { +// const mapped = await predicateMapper(item); +// if (mapped != null) yield mapped; +// } +// } +// } \ No newline at end of file diff --git a/src/system/function.ts b/src/system/function.ts index fd16d2d..a2e313d 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -17,6 +17,10 @@ export namespace Functions { return _debounce(fn, wait, options); } + export function once(fn: T): T { + return _once(fn); + } + export function propOf(o: T, key: K) { const propOfCore = (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(fn: T): T { - return _once(fn); - } - export async function wait(ms: number) { await new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/src/system/iterable.ts b/src/system/iterable.ts index ae19ef2..3f1a173 100644 --- a/src/system/iterable.ts +++ b/src/system/iterable.ts @@ -8,9 +8,18 @@ export namespace Iterables { return true; } - export function* filter(source: Iterable | IterableIterator, predicate: (item: T) => boolean): Iterable { - for (const item of source) { - if (predicate(item)) yield item; + export function filter(source: Iterable | IterableIterator): Iterable; + export function filter(source: Iterable | IterableIterator, predicate: (item: T) => boolean): Iterable; + export function* filter(source: Iterable | IterableIterator, predicate?: (item: T) => boolean): Iterable { + if (predicate === undefined) { + for (const item of source) { + if (item != null) yield item; + } + } + else { + for (const item of source) { + if (predicate(item)) yield item; + } } } diff --git a/src/system/searchTree.ts b/src/system/searchTree.ts new file mode 100644 index 0000000..7a7e0f8 --- /dev/null +++ b/src/system/searchTree.ts @@ -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 { + str: string; + element: E | undefined; + left: TernarySearchTreeNode | undefined; + mid: TernarySearchTreeNode | undefined; + right: TernarySearchTreeNode | undefined; + + isEmpty(): boolean { + return this.left === undefined && this.mid === undefined && this.right === undefined && this.element === undefined; + } +} + +export class TernarySearchTree { + + static forPaths(): TernarySearchTree { + return new TernarySearchTree(new PathIterator()); + } + + static forStrings(): TernarySearchTree { + return new TernarySearchTree(new StringIterator()); + } + + private _iter: IKeyIterator; + private _root: TernarySearchTreeNode | 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; + + if (!this._root) { + this._root = new TernarySearchTreeNode(); + 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(); + node.left.str = iter.value(); + } + node = node.left; + + } else if (val < 0) { + // right + if (!node.right) { + node.right = new TernarySearchTreeNode(); + node.right.str = iter.value(); + } + node = node.right; + + } else if (iter.hasNext()) { + // mid + iter.next(); + if (!node.mid) { + node.mid = new TernarySearchTreeNode(); + 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][] = []; + 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 | 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(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, 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 { + 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 | 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); + } + } +} diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index 2e976ff..0e11ff2 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -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) {