diff --git a/src/activeEditorTracker.ts b/src/activeEditorTracker.ts index 6d9297b..e8c598f 100644 --- a/src/activeEditorTracker.ts +++ b/src/activeEditorTracker.ts @@ -7,7 +7,7 @@ import { BuiltInCommands } from './constants'; export class ActiveEditorTracker extends Disposable { private _disposable: Disposable; - private _resolver: ((value?: TextEditor | PromiseLike) => void) | undefined; + private _resolver: ((editor: TextEditor | undefined) => void) | undefined; constructor() { super(() => this.dispose()); @@ -42,11 +42,11 @@ export class ActiveEditorTracker extends Disposable { const editor = await new Promise((resolve, reject) => { let timer: any; - this._resolver = (e: TextEditor) => { + this._resolver = (editor: TextEditor | undefined) => { if (timer) { clearTimeout(timer as any); timer = 0; - resolve(e); + resolve(editor); } }; diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 239f888..5d8c852 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -1,5 +1,5 @@ 'use strict'; -import { Iterables } from '../system'; +import { Arrays, Iterables } from '../system'; import { CancellationToken, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, TextDocument, TextEditor, TextEditorDecorationType } from 'vscode'; import { FileAnnotationType } from './annotationController'; import { AnnotationProviderBase } from './annotationProvider'; @@ -56,9 +56,8 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase return; } - const highlightDecorationRanges = blame.lines - .filter(l => l.sha === sha) - .map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000))); + const highlightDecorationRanges = Arrays.filterMap(blame.lines, + l => l.sha !== sha ? this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)) : undefined); this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges); } diff --git a/src/commands/externalDiff.ts b/src/commands/externalDiff.ts index c83ab7b..161961f 100644 --- a/src/commands/externalDiff.ts +++ b/src/commands/externalDiff.ts @@ -1,4 +1,5 @@ 'use strict'; +import { Arrays } from '../system'; import { commands, SourceControlResourceState, Uri, window } from 'vscode'; import { Command, Commands } from './common'; import { BuiltInCommands } from '../constants'; @@ -63,16 +64,14 @@ export class ExternalDiffCommand extends Command { if (context.type === 'scm-states') { args = { ...args }; args.files = context.scmResourceStates - .map((r: Resource) => new ExternalDiffFile(r.resourceUri, r.resourceGroupType === ResourceGroupType.Index)); + .map(r => new ExternalDiffFile(r.resourceUri, (r as Resource).resourceGroupType === ResourceGroupType.Index)); return this.execute(args); - } else if (context.type === 'scm-groups') { - const isModified = (status: Status): boolean => status === Status.BOTH_MODIFIED || status === Status.INDEX_MODIFIED || status === Status.MODIFIED; - + } + else if (context.type === 'scm-groups') { args = { ...args }; - args.files = context.scmResourceGroups[0].resourceStates - .filter((r: Resource) => isModified(r.type)) - .map((r: Resource) => new ExternalDiffFile(r.resourceUri, r.resourceGroupType === ResourceGroupType.Index)); + args.files = Arrays.filterMap(context.scmResourceGroups[0].resourceStates, + r => this.isModified(r) ? new ExternalDiffFile(r.resourceUri, (r as Resource).resourceGroupType === ResourceGroupType.Index) : undefined); return this.execute(args); } @@ -80,6 +79,11 @@ export class ExternalDiffCommand extends Command { return this.execute(args); } + private isModified(resource: SourceControlResourceState) { + const status = (resource as Resource).type; + return status === Status.BOTH_MODIFIED || status === Status.INDEX_MODIFIED || status === Status.MODIFIED; + } + async execute(args: ExternalDiffCommandArgs = {}) { try { const diffTool = await this.git.getConfig('diff.tool'); diff --git a/src/commands/openChangedFiles.ts b/src/commands/openChangedFiles.ts index 05426ae..29b9da6 100644 --- a/src/commands/openChangedFiles.ts +++ b/src/commands/openChangedFiles.ts @@ -1,4 +1,5 @@ 'use strict'; +import { Arrays } from '../system'; import { TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { ActiveEditorCommand, Commands, getCommandUri, openEditor } from './common'; import { GitService } from '../gitService'; @@ -30,7 +31,8 @@ export class OpenChangedFilesCommand extends ActiveEditorCommand { const status = await this.git.getStatusForRepo(repoPath); if (status === undefined) return window.showWarningMessage(`Unable to open changed files`); - args.uris = status.files.filter(f => f.status !== 'D').map(f => f.Uri); + args.uris = Arrays.filterMap(status.files, + f => f.status !== 'D' ? f.Uri : undefined); } for (const uri of args.uris) { diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts index 1a8696d..84b36dd 100644 --- a/src/git/models/logCommit.ts +++ b/src/git/models/logCommit.ts @@ -63,9 +63,25 @@ export class GitLogCommit extends GitCommit { } getDiffStatus(): string { - const added = this.fileStatuses.filter(f => f.status === 'A' || f.status === '?').length; - const deleted = this.fileStatuses.filter(f => f.status === 'D').length; - const changed = this.fileStatuses.filter(f => f.status !== 'A' && f.status !== '?' && f.status !== 'D').length; + let added = 0; + let deleted = 0; + let changed = 0; + + for (const f of this.fileStatuses) { + switch (f.status) { + case 'A': + case '?': + added++; + break; + case 'D': + deleted++; + break; + default: + changed++; + break; + } + } + return `+${added} ~${changed} -${deleted}`; } diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index 91ce3ea..b973487 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -37,11 +37,9 @@ export class GitBlameParser { let line: string; let lineParts: string[]; - let i = -1; let first = true; for (line of Strings.lines(data)) { - i++; lineParts = line.split(' '); if (lineParts.length < 2) continue; diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 4d4a5de..00e734e 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -1,5 +1,5 @@ 'use strict'; -import { Strings } from '../../system'; +import { Arrays, Strings } from '../../system'; import { Range } from 'vscode'; import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatus, IGitStatusFile } from './../git'; // import { Logger } from '../../logger'; @@ -170,7 +170,8 @@ export class GitLogParser { } if (entry.fileStatuses) { - entry.fileName = entry.fileStatuses.filter(f => !!f.fileName).map(f => f.fileName).join(', '); + entry.fileName = Arrays.filterMap(entry.fileStatuses, + f => !!f.fileName ? f.fileName : undefined).join(', '); } } else { diff --git a/src/git/parsers/stashParser.ts b/src/git/parsers/stashParser.ts index adbf7b9..a55629a 100644 --- a/src/git/parsers/stashParser.ts +++ b/src/git/parsers/stashParser.ts @@ -1,4 +1,5 @@ 'use strict'; +import { Arrays } from '../../system'; import { Git, GitStash, GitStashCommit, GitStatusFileStatus, IGitStatusFile } from './../git'; // import { Logger } from '../../logger'; @@ -120,7 +121,8 @@ export class GitStashParser { } if (entry.fileStatuses) { - entry.fileNames = entry.fileStatuses.filter(f => !!f.fileName).map(f => f.fileName).join(', '); + entry.fileNames = Arrays.filterMap(entry.fileStatuses, + f => !!f.fileName ? f.fileName : undefined).join(', '); } entries.push(entry); diff --git a/src/quickPicks/commitDetails.ts b/src/quickPicks/commitDetails.ts index a0cf7a4..2f0af6c 100644 --- a/src/quickPicks/commitDetails.ts +++ b/src/quickPicks/commitDetails.ts @@ -1,5 +1,5 @@ 'use strict'; -import { Iterables, Strings } from '../system'; +import { Arrays, Iterables, Strings } from '../system'; import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode'; import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffDirectoryCommandCommandArgs, DiffWithPreviousCommandArgs, ShowQuickCommitDetailsCommandArgs, StashApplyCommandArgs, StashDeleteCommandArgs } from '../commands'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem, QuickPickItem } from './common'; @@ -74,9 +74,8 @@ export class OpenCommitFilesCommandQuickPickItem extends OpenFilesCommandQuickPi item?: QuickPickItem ) { const repoPath = commit.repoPath; - const uris = commit.fileStatuses - .filter(s => s.status !== 'D') - .map(s => GitUri.fromFileStatus(s, repoPath)); + const uris = Arrays.filterMap(commit.fileStatuses, + f => f.status !== 'D' ? GitUri.fromFileStatus(f, repoPath) : undefined); super(uris, item || { label: `$(file-symlink-file) Open Changed Files`, @@ -92,9 +91,8 @@ export class OpenCommitFileRevisionsCommandQuickPickItem extends OpenFilesComman commit: GitLogCommit, item?: QuickPickItem ) { - const uris = commit.fileStatuses - .filter(s => s.status !== 'D') - .map(s => GitService.toGitContentUri(commit.sha, s.fileName, commit.repoPath, s.originalFileName)); + const uris = Arrays.filterMap(commit.fileStatuses, + f => f.status !== 'D' ? GitService.toGitContentUri(commit.sha, f.fileName, commit.repoPath, f.originalFileName) : undefined); super(uris, item || { label: `$(file-symlink-file) Open Changed Revisions`, diff --git a/src/quickPicks/repoStatus.ts b/src/quickPicks/repoStatus.ts index 458efa6..ef2c6e7 100644 --- a/src/quickPicks/repoStatus.ts +++ b/src/quickPicks/repoStatus.ts @@ -56,34 +56,84 @@ export class OpenStatusFilesCommandQuickPickItem extends CommandQuickPickItem { } } +interface ComputedStatus { + staged: number; + stagedAddsAndChanges: GitStatusFile[]; + stagedStatus: string; + + unstaged: number; + unstagedAddsAndChanges: GitStatusFile[]; + unstagedStatus: string; +} + export class RepoStatusQuickPick { - static async show(status: GitStatus, goBackCommand?: CommandQuickPickItem): Promise { - // Sort the status by staged and then filename - const files = status.files; - files.sort((a, b) => (a.staged ? -1 : 1) - (b.staged ? -1 : 1) || a.fileName.localeCompare(b.fileName)); + private static computeStatus(files: GitStatusFile[]): ComputedStatus { + let stagedAdds = 0; + let unstagedAdds = 0; + let stagedChanges = 0; + let unstagedChanges = 0; + let stagedDeletes = 0; + let unstagedDeletes = 0; - const added = files.filter(f => f.status === 'A' || f.status === '?'); - const deleted = files.filter(f => f.status === 'D'); - const changed = files.filter(f => f.status !== 'A' && f.status !== '?' && f.status !== 'D'); + const stagedAddsAndChanges: GitStatusFile[] = []; + const unstagedAddsAndChanges: GitStatusFile[] = []; - const hasStaged = files.some(f => f.staged); + for (const f of files) { + switch (f.status) { + case 'A': + case '?': + if (f.staged) { + stagedAdds++; + stagedAddsAndChanges.push(f); + } + else { + unstagedAdds++; + unstagedAddsAndChanges.push(f); + } + break; - let stagedStatus = ''; - let unstagedStatus = ''; - if (hasStaged) { - const stagedAdded = added.filter(f => f.staged).length; - const stagedChanged = changed.filter(f => f.staged).length; - const stagedDeleted = deleted.filter(f => f.staged).length; + case 'D': + if (f.staged) { + stagedDeletes++; + } + else { + unstagedDeletes++; + } + break; - stagedStatus = `+${stagedAdded} ~${stagedChanged} -${stagedDeleted}`; - unstagedStatus = `+${added.length - stagedAdded} ~${changed.length - stagedChanged} -${deleted.length - stagedDeleted}`; - } - else { - unstagedStatus = `+${added.length} ~${changed.length} -${deleted.length}`; + default: + if (f.staged) { + stagedChanges++; + stagedAddsAndChanges.push(f); + } + else { + unstagedChanges++; + unstagedAddsAndChanges.push(f); + } + break; + } } - const items = Array.from(Iterables.map(files, s => new OpenStatusFileCommandQuickPickItem(s))) as (OpenStatusFileCommandQuickPickItem | OpenStatusFilesCommandQuickPickItem | CommandQuickPickItem)[]; + const staged = stagedAdds + stagedChanges + stagedDeletes; + const unstaged = unstagedAdds + unstagedChanges + unstagedDeletes; + + return { + staged: staged, + stagedStatus: staged > 0 ? `+${stagedAdds} ~${stagedChanges} -${stagedDeletes}` : '', + stagedAddsAndChanges: stagedAddsAndChanges, + unstaged: unstaged, + unstagedStatus: unstaged > 0 ? `+${unstagedAdds} ~${unstagedChanges} -${unstagedDeletes}` : '', + unstagedAddsAndChanges: unstagedAddsAndChanges + }; + } + + static async show(status: GitStatus, goBackCommand?: CommandQuickPickItem): Promise { + // Sort the status by staged and then filename + const files = status.files; + files.sort((a, b) => (a.staged ? -1 : 1) - (b.staged ? -1 : 1) || a.fileName.localeCompare(b.fileName)); + + const items = [...Iterables.map(files, s => new OpenStatusFileCommandQuickPickItem(s))] as (OpenStatusFileCommandQuickPickItem | OpenStatusFilesCommandQuickPickItem | CommandQuickPickItem)[]; const currentCommand = new CommandQuickPickItem({ label: `go back ${GlyphChars.ArrowBack}`, @@ -95,13 +145,14 @@ export class RepoStatusQuickPick { } as ShowQuickRepoStatusCommandArgs ]); - if (hasStaged) { + const computed = this.computeStatus(files); + if (computed.staged > 0) { let index = 0; - const unstagedIndex = files.findIndex(f => !f.staged); + const unstagedIndex = computed.unstaged > 0 ? files.findIndex(f => !f.staged) : -1; if (unstagedIndex > -1) { items.splice(unstagedIndex, 0, new CommandQuickPickItem({ label: `Unstaged Files`, - description: unstagedStatus + description: computed.unstagedStatus }, Commands.ShowQuickRepoStatus, [ undefined, { @@ -109,12 +160,12 @@ export class RepoStatusQuickPick { } as ShowQuickRepoStatusCommandArgs ])); - items.splice(unstagedIndex, 0, new OpenStatusFilesCommandQuickPickItem(files.filter(f => f.status !== 'D' && f.staged), { + items.splice(unstagedIndex, 0, new OpenStatusFilesCommandQuickPickItem(computed.stagedAddsAndChanges, { label: `${GlyphChars.Space.repeat(4)} $(file-symlink-file) Open Staged Files`, description: '' })); - items.push(new OpenStatusFilesCommandQuickPickItem(files.filter(f => f.status !== 'D' && !f.staged), { + items.push(new OpenStatusFilesCommandQuickPickItem(computed.unstagedAddsAndChanges, { label: `${GlyphChars.Space.repeat(4)} $(file-symlink-file) Open Unstaged Files`, description: '' })); @@ -122,7 +173,7 @@ export class RepoStatusQuickPick { items.splice(index++, 0, new CommandQuickPickItem({ label: `Staged Files`, - description: stagedStatus + description: computed.stagedStatus }, Commands.ShowQuickRepoStatus, [ undefined, { @@ -133,7 +184,7 @@ export class RepoStatusQuickPick { else if (files.some(f => !f.staged)) { items.splice(0, 0, new CommandQuickPickItem({ label: `Unstaged Files`, - description: unstagedStatus + description: computed.unstagedStatus }, Commands.ShowQuickRepoStatus, [ undefined, { @@ -143,7 +194,7 @@ export class RepoStatusQuickPick { } if (files.length) { - items.push(new OpenStatusFilesCommandQuickPickItem(files.filter(f => f.status !== 'D'))); + items.push(new OpenStatusFilesCommandQuickPickItem(computed.stagedAddsAndChanges.concat(computed.unstagedAddsAndChanges))); items.push(new CommandQuickPickItem({ label: '$(x) Close Unchanged Files', description: '' diff --git a/src/system/array.ts b/src/system/array.ts index 7b45b88..4d66e04 100644 --- a/src/system/array.ts +++ b/src/system/array.ts @@ -2,21 +2,41 @@ import { Objects } from './object'; export namespace Arrays { - export function countUniques(array: T[], accessor: (item: T) => string): { [key: string]: number } { + export function countUniques(source: T[], accessor: (item: T) => string): { [key: string]: number } { const uniqueCounts = Object.create(null); - for (const item of array) { + for (const item of source) { const value = accessor(item); uniqueCounts[value] = (uniqueCounts[value] || 0) + 1; } return uniqueCounts; } - export function groupBy(array: T[], accessor: (item: T) => string): { [key: string]: T[] } { - return array.reduce((previous, current) => { + export function filterMap(source: T[], predicateMapper: (item: T) => TMapped | null | undefined): TMapped[] { + return source.reduce((accumulator, current) => { + const mapped = predicateMapper(current); + if (mapped != null) { + accumulator.push(mapped); + } + return accumulator; + }, [] as any); + } + + export async function filterMapAsync(source: T[], predicateMapper: (item: T) => Promise): Promise { + return source.reduce(async (accumulator, current) => { + const mapped = await predicateMapper(current); + if (mapped != null) { + accumulator.push(mapped); + } + return accumulator; + }, [] as any); + } + + export function groupBy(source: T[], accessor: (item: T) => string): { [key: string]: T[] } { + return source.reduce((groupings, current) => { const value = accessor(current); - previous[value] = previous[value] || []; - previous[value].push(current); - return previous; + groupings[value] = groupings[value] || []; + groupings[value].push(current); + return groupings; }, Object.create(null)); } @@ -110,9 +130,9 @@ export namespace Arrays { return root; } - export function uniqueBy(array: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] { + export function uniqueBy(source: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] { const uniqueValues = Object.create(null); - return array.filter(item => { + return source.filter(item => { const value = accessor(item); if (uniqueValues[value]) return false; diff --git a/src/system/iterable.ts b/src/system/iterable.ts index b2d05cb..ae19ef2 100644 --- a/src/system/iterable.ts +++ b/src/system/iterable.ts @@ -17,7 +17,7 @@ export namespace Iterables { export function* filterMap(source: Iterable | IterableIterator, predicateMapper: (item: T) => TMapped | undefined | null): Iterable { for (const item of source) { const mapped = predicateMapper(item); - if (mapped) yield mapped; + if (mapped != null) yield mapped; } } diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index d653101..65affe0 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -1,5 +1,5 @@ 'use strict'; -import { Functions, Objects } from '../system'; +import { Arrays, Functions, Objects } from '../system'; import { commands, Disposable, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode'; import { Commands, DiffWithCommandArgs, DiffWithCommandArgsRevision, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands'; import { UriComparer } from '../comparers'; @@ -334,28 +334,25 @@ export class GitExplorer implements TreeDataProvider { private async openChangedFileChangesWithWorking(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) { const repoPath = node.commit.repoPath; - const uris = node.commit.fileStatuses - .filter(s => s.status !== 'D') - .map(s => GitUri.fromFileStatus(s, repoPath)); + const uris = Arrays.filterMap(node.commit.fileStatuses, + f => f.status !== 'D' ? GitUri.fromFileStatus(f, repoPath) : undefined); for (const uri of uris) { - await this.openDiffWith(repoPath, - { uri: uri, sha: node.commit.sha }, - { uri: uri, sha: '' }, options); + await this.openDiffWith(repoPath, { uri: uri, sha: node.commit.sha }, { uri: uri, sha: '' }, options); } } private async openChangedFiles(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) { const repoPath = node.commit.repoPath; - const uris = node.commit.fileStatuses.filter(s => s.status !== 'D').map(s => GitUri.fromFileStatus(s, repoPath)); + const uris = Arrays.filterMap(node.commit.fileStatuses, + f => f.status !== 'D' ? GitUri.fromFileStatus(f, repoPath) : undefined); for (const uri of uris) { await openEditor(uri, options); } } private async openChangedFileRevisions(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) { - const uris = node.commit.fileStatuses - .filter(s => s.status !== 'D') - .map(s => GitService.toGitContentUri(node.commit.sha, s.fileName, node.commit.repoPath, s.originalFileName)); + const uris = Arrays.filterMap(node.commit.fileStatuses, + f => f.status !== 'D' ? GitService.toGitContentUri(node.commit.sha, f.fileName, node.commit.repoPath, f.originalFileName) : undefined); for (const uri of uris) { await openEditor(uri, options); }