diff --git a/src/commands/diffWithRevision.ts b/src/commands/diffWithRevision.ts index 3936285..78c3f72 100644 --- a/src/commands/diffWithRevision.ts +++ b/src/commands/diffWithRevision.ts @@ -1,7 +1,7 @@ 'use strict'; import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common'; -import { GlyphChars } from '../constants'; +import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; import { DiffWithCommandArgs } from './diffWith'; import { GitRevision } from '../git/git'; @@ -44,10 +44,12 @@ export class DiffWithRevisionCommand extends ActiveEditorCommand { : undefined), ); + const title = `Open Changes with Revision${Strings.pad(GlyphChars.Dot, 2, 2)}`; const pick = await CommitPicker.show( log, - `Open Changes with Revision${Strings.pad(GlyphChars.Dot, 2, 2)}${gitUri.getFormattedPath({ + `${title}${gitUri.getFormattedFilename({ suffix: gitUri.sha ? `:${GitRevision.shorten(gitUri.sha)}` : undefined, + truncateTo: quickPickTitleMaxChars - title.length, })}`, 'Choose a commit to compare with', { diff --git a/src/commands/diffWithRevisionFrom.ts b/src/commands/diffWithRevisionFrom.ts index 166f9c8..92febc2 100644 --- a/src/commands/diffWithRevisionFrom.ts +++ b/src/commands/diffWithRevisionFrom.ts @@ -2,7 +2,7 @@ import * as paths from 'path'; import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common'; -import { GlyphChars } from '../constants'; +import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; import { DiffWithCommandArgs } from './diffWith'; import { GitReference, GitRevision } from '../git/git'; @@ -38,9 +38,10 @@ export class DiffWithRevisionFromCommand extends ActiveEditorCommand { args.line = editor?.selection.active.line ?? 0; } + const title = `Open Changes with Branch or Tag${Strings.pad(GlyphChars.Dot, 2, 2)}`; const pick = await ReferencePicker.show( gitUri.repoPath, - `Open Changes with Branch or Tag${Strings.pad(GlyphChars.Dot, 2, 2)}${gitUri.getFormattedPath()}`, + `${title}${gitUri.getFormattedFilename({ truncateTo: quickPickTitleMaxChars - title.length })}`, 'Choose a branch or tag to compare with', { allowEnteringRefs: true, diff --git a/src/commands/git/log.ts b/src/commands/git/log.ts index fcbf7b0..be1b2c7 100644 --- a/src/commands/git/log.ts +++ b/src/commands/git/log.ts @@ -12,7 +12,7 @@ import { StepResult, StepState, } from '../quickCommand'; -import { GlyphChars } from '../../constants'; +import { GlyphChars, quickPickTitleMaxChars } from '../../constants'; import { GitUri } from '../../git/gitUri'; import { Strings } from '../../system'; @@ -128,19 +128,17 @@ export class LogGitCommand extends QuickCommand { context.selectedBranchOrTag = state.reference; } - context.title = `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${GitReference.toString( - context.selectedBranchOrTag, - { - icon: false, - }, - )}${ - state.fileName - ? `${Strings.pad(GlyphChars.Dot, 2, 2)}${GitUri.getFormattedPath(state.fileName, { - relativeTo: state.repo.path, - truncateTo: 35, - })}` - : '' - }`; + context.title = `${this.title}${Strings.pad( + GlyphChars.Dot, + 2, + 2, + )}${GitReference.toString(context.selectedBranchOrTag, { icon: false })}`; + + if (state.fileName) { + context.title += `${Strings.pad(GlyphChars.Dot, 2, 2)}${GitUri.getFormattedFilename(state.fileName, { + truncateTo: quickPickTitleMaxChars - context.title.length - 3, + })}`; + } if (state.counter < 3 && context.selectedBranchOrTag != null) { const ref = context.selectedBranchOrTag.ref; diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts index f2ead4b..adaf530 100644 --- a/src/commands/git/stash.ts +++ b/src/commands/git/stash.ts @@ -1,5 +1,6 @@ 'use strict'; import { QuickInputButtons, QuickPickItem, Uri, window } from 'vscode'; +import { GlyphChars } from '../../constants'; import { Container } from '../../container'; import { GitReference, GitStashCommit, GitStashReference, Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; @@ -21,6 +22,7 @@ import { import { FlagsQuickPickItem, QuickPickItemOfT } from '../../quickpicks'; import { Logger } from '../../logger'; import { Messages } from '../../messages'; +import { Strings } from '../../system'; interface Context { repos: Repository[]; @@ -506,7 +508,18 @@ export class StashGitCommand extends QuickCommand { private async *pushCommandInputMessageStep(state: PushStepState, context: Context): StepResultGenerator { const step = QuickCommand.createInputStep({ - title: appendReposToTitle(context.title, state, context), + title: appendReposToTitle( + context.title, + state, + context, + state.uris != null + ? `${Strings.pad(GlyphChars.Dot, 2, 2)}${ + state.uris.length === 1 + ? GitUri.getFormattedFilename(state.uris[0]) + : `${state.uris.length} files` + }` + : undefined, + ), placeholder: 'Please provide a stash message', value: state.message, prompt: 'Enter stash message', @@ -546,17 +559,17 @@ export class StashGitCommand extends QuickCommand { : [ FlagsQuickPickItem.create(state.flags, [], { label: context.title, - detail: `Will stash changes in ${ + detail: `Will stash changes from ${ state.uris.length === 1 - ? GitUri.getFormattedPath(state.uris[0], { relativeTo: state.repo.path }) + ? GitUri.getFormattedFilename(state.uris[0]) : `${state.uris.length} files` }`, }), FlagsQuickPickItem.create(state.flags, ['--keep-index'], { label: `${context.title} & Keep Staged`, - detail: `Will stash changes in ${ + detail: `Will stash changes from ${ state.uris.length === 1 - ? GitUri.getFormattedPath(state.uris[0], { relativeTo: state.repo.path }) + ? GitUri.getFormattedFilename(state.uris[0]) : `${state.uris.length} files` }, but will keep staged files intact`, }), diff --git a/src/commands/openFileAtRevision.ts b/src/commands/openFileAtRevision.ts index 1fe41c4..4e6da21 100644 --- a/src/commands/openFileAtRevision.ts +++ b/src/commands/openFileAtRevision.ts @@ -2,7 +2,7 @@ import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { ActiveEditorCommand, command, Commands, getCommandUri } from './common'; import { FileAnnotationType } from '../configuration'; -import { GlyphChars } from '../constants'; +import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; import { GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; @@ -72,10 +72,12 @@ export class OpenFileAtRevisionCommand extends ActiveEditorCommand { : undefined), ); + const title = `Open File at Revision${Strings.pad(GlyphChars.Dot, 2, 2)}`; const pick = await CommitPicker.show( log, - `Open File at Revision${Strings.pad(GlyphChars.Dot, 2, 2)}${gitUri.getFormattedPath({ + `${title}${gitUri.getFormattedFilename({ suffix: gitUri.sha ? `:${GitRevision.shorten(gitUri.sha)}` : undefined, + truncateTo: quickPickTitleMaxChars - title.length, })}`, 'Choose a commit to open the file revision from', { diff --git a/src/commands/openFileAtRevisionFrom.ts b/src/commands/openFileAtRevisionFrom.ts index 0d0b485..0024c8c 100644 --- a/src/commands/openFileAtRevisionFrom.ts +++ b/src/commands/openFileAtRevisionFrom.ts @@ -2,7 +2,7 @@ import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { ActiveEditorCommand, command, Commands, getCommandUri } from './common'; import { FileAnnotationType } from '../configuration'; -import { GlyphChars } from '../constants'; +import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { GitReference } from '../git/git'; import { GitUri } from '../git/gitUri'; import { GitActions } from './gitCommands'; @@ -40,9 +40,10 @@ export class OpenFileAtRevisionFromCommand extends ActiveEditorCommand { } if (args.reference == null) { + const title = `Open File at Revision${Strings.pad(GlyphChars.Dot, 2, 2)}`; const pick = await ReferencePicker.show( gitUri.repoPath, - `Open File at Revision${Strings.pad(GlyphChars.Dot, 2, 2)}${gitUri.getFormattedPath()}`, + `${title}${gitUri.getFormattedFilename({ truncateTo: quickPickTitleMaxChars - title.length })}`, 'Choose a branch or tag to open the file revision from', { allowEnteringRefs: true, diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index 812710d..7a40125 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -3,7 +3,7 @@ import { QuickInputButton, QuickPick } from 'vscode'; import { Commands } from './common'; import { configuration } from '../configuration'; import { Container } from '../container'; -import { GlyphChars } from '../constants'; +import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { GitBranch, GitBranchReference, @@ -78,17 +78,26 @@ export function appendReposToTitle< State extends { repo: Repository } | { repos: Repository[] }, Context extends { repos: Repository[] } >(title: string, state: State, context: Context, additionalContext?: string) { - if (context.repos.length === 1) return `${title}${additionalContext ?? ''}`; + if (context.repos.length === 1) { + return `${title}${Strings.truncate(additionalContext ?? '', quickPickTitleMaxChars - title.length)}`; + } + let repoContext; if ((state as { repo: Repository }).repo != null) { - return `${title}${Strings.pad(GlyphChars.Dot, 2, 2)}${(state as { repo: Repository }).repo.formattedName}`; + repoContext = `${additionalContext ?? ''}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + (state as { repo: Repository }).repo.formattedName + }`; + } else if ((state as { repos: Repository[] }).repos.length === 1) { + repoContext = `${additionalContext ?? ''}${Strings.pad(GlyphChars.Dot, 2, 2)}${ + (state as { repos: Repository[] }).repos[0].formattedName + }`; + } else { + repoContext = `${Strings.pad(GlyphChars.Dot, 2, 2)}${ + (state as { repos: Repository[] }).repos.length + } repositories`; } - return `${title}${Strings.pad(GlyphChars.Dot, 2, 2)}${ - (state as { repos: Repository[] }).repos.length === 1 - ? `${(state as { repos: Repository[] }).repos[0].formattedName}${additionalContext ?? ''}` - : `${(state as { repos: Repository[] }).repos.length} repositories` - }`; + return `${title}${Strings.truncate(repoContext, quickPickTitleMaxChars - title.length)}`; } export async function getBranches( @@ -1521,10 +1530,7 @@ export async function* showCommitOrStashFileStep< }), state, context, - `${Strings.pad(GlyphChars.Dot, 2, 2)}${GitUri.getFormattedPath(state.fileName, { - relativeTo: state.repo.path, - truncateTo: 35, - })}`, + `${Strings.pad(GlyphChars.Dot, 2, 2)}${GitUri.getFormattedFilename(state.fileName)}`, ), placeholder: `${GitUri.getFormattedPath(state.fileName, { relativeTo: state.repo.path, diff --git a/src/constants.ts b/src/constants.ts index ca9c6ee..50eae5a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,6 +8,8 @@ export const extensionOutputChannelName = 'GitLens'; export const extensionQualifiedId = `eamodio.${extensionId}`; export const extensionTerminalName = 'GitLens'; +export const quickPickTitleMaxChars = 80; + export enum BuiltInCommands { CloseActiveEditor = 'workbench.action.closeActiveEditor', CloseAllEditors = 'workbench.action.closeAllEditors', diff --git a/src/git/formatters/statusFormatter.ts b/src/git/formatters/statusFormatter.ts index 1ec34df..18be82a 100644 --- a/src/git/formatters/statusFormatter.ts +++ b/src/git/formatters/statusFormatter.ts @@ -31,7 +31,10 @@ export class StatusFileFormatter extends Formatter } get filePath() { - const filePath = GitFile.getFormattedPath(this._item, { relativeTo: this._options.relativePath }); + const filePath = GitFile.getFormattedPath(this._item, { + relativeTo: this._options.relativePath, + truncateTo: this._options.tokenOptions.filePath?.truncateTo, + }); return this._padOrTruncate(filePath, this._options.tokenOptions.filePath); } diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index 09ab353..3e31433 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -2,7 +2,7 @@ import * as paths from 'path'; import { Uri } from 'vscode'; import { UriComparer } from '../comparers'; -import { DocumentSchemes, GlyphChars } from '../constants'; +import { DocumentSchemes } from '../constants'; import { Container } from '../container'; import { GitCommit, GitFile, GitRevision } from '../git/git'; import { Logger } from '../logger'; @@ -180,15 +180,12 @@ export class GitUri extends ((Uri as any) as UriEx) { return this.sha === (GitUri.is(uri) ? uri.sha : undefined); } - getFormattedPath(options: { relativeTo?: string; separator?: string; suffix?: string } = {}): string { - const { - relativeTo = this.repoPath, - separator = Strings.pad(GlyphChars.Dot, 1, 1), - suffix = emptyStr, - } = options; + getFormattedFilename(options: { suffix?: string; truncateTo?: number } = {}): string { + return GitUri.getFormattedFilename(this.fsPath, options); + } - const directory = GitUri.getDirectory(this.fsPath, relativeTo); - return `${paths.basename(this.fsPath)}${suffix}${directory ? `${separator}${directory}` : emptyStr}`; + getFormattedPath(options: { relativeTo?: string; suffix?: string; truncateTo?: number } = {}): string { + return GitUri.getFormattedPath(this.fsPath, { relativeTo: this.repoPath, ...options }); } @memoize() @@ -330,39 +327,78 @@ export class GitUri extends ((Uri as any) as UriEx) { return directory == null || directory.length === 0 || directory === '.' ? emptyStr : directory; } - static getFormattedPath( + static getFormattedFilename( fileNameOrUri: string | Uri, - options: { relativeTo?: string; separator?: string; suffix?: string; truncateTo?: number }, + options: { + suffix?: string; + truncateTo?: number; + } = {}, ): string { - const { relativeTo, separator = Strings.pad(GlyphChars.Dot, 1, 1), suffix = emptyStr, truncateTo } = options; + const { suffix = emptyStr, truncateTo } = options; let fileName: string; if (fileNameOrUri instanceof Uri) { - if (GitUri.is(fileNameOrUri)) return fileNameOrUri.getFormattedPath(options); - fileName = fileNameOrUri.fsPath; } else { fileName = fileNameOrUri; } - const file = `${paths.basename(fileName)}${suffix}`; - if (truncateTo != null && file.length > truncateTo) { + let file = paths.basename(fileName); + if (truncateTo != null && file.length >= truncateTo) { return Strings.truncateMiddle(file, truncateTo); } - let directory = GitUri.getDirectory(fileName, relativeTo); - if (!directory) { - return file; + if (suffix) { + if (truncateTo != null && file.length + suffix.length >= truncateTo) { + return `${Strings.truncateMiddle(file, truncateTo - suffix.length)}${suffix}`; + } + + file += suffix; + } + + return file; + } + + static getFormattedPath( + fileNameOrUri: string | Uri, + options: { + relativeTo?: string; + suffix?: string; + truncateTo?: number; + }, + ): string { + const { relativeTo, suffix = emptyStr, truncateTo } = options; + + let fileName: string; + if (fileNameOrUri instanceof Uri) { + fileName = fileNameOrUri.fsPath; + } else { + fileName = fileNameOrUri; + } + + let file = paths.basename(fileName); + if (truncateTo != null && file.length >= truncateTo) { + return Strings.truncateMiddle(file, truncateTo); } - if (truncateTo != null) { - const dirTruncateTo = truncateTo - (file.length + separator.length); - if (directory.length > dirTruncateTo) { - directory = Strings.truncateMiddle(directory, dirTruncateTo); + if (suffix) { + if (truncateTo != null && file.length + suffix.length >= truncateTo) { + return `${Strings.truncateMiddle(file, truncateTo - suffix.length)}${suffix}`; } + + file += suffix; + } + + const directory = GitUri.getDirectory(fileName, relativeTo); + if (!directory) return file; + + file = `/${file}`; + + if (truncateTo != null && file.length + directory.length >= truncateTo) { + return `${Strings.truncateLeft(directory, truncateTo - file.length)}${file}`; } - return `${file}${separator}${directory}`; + return `${directory}${file}`; } static relativeTo(fileNameOrUri: string | Uri, relativeTo: string | undefined): string { diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index 55d6e15..144fbfb 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -213,9 +213,7 @@ export abstract class GitCommit implements GitRevisionReference { return this.dateFormatter.fromNow(); } - getFormattedPath( - options: { relativeTo?: string; separator?: string; suffix?: string; truncateTo?: number } = {}, - ): string { + getFormattedPath(options: { relativeTo?: string; suffix?: string; truncateTo?: number } = {}): string { return GitUri.getFormattedPath(this.fileName, options); } diff --git a/src/git/models/file.ts b/src/git/models/file.ts index 2708cb2..84ab58f 100644 --- a/src/git/models/file.ts +++ b/src/git/models/file.ts @@ -44,7 +44,7 @@ export namespace GitFile { export function getFormattedPath( file: GitFile, - options: { relativeTo?: string; separator?: string; suffix?: string; truncateTo?: number } = {}, + options: { relativeTo?: string; suffix?: string; truncateTo?: number } = {}, ): string { return GitUri.getFormattedPath(file.fileName, options); } diff --git a/src/git/models/status.ts b/src/git/models/status.ts index 044550d..79a23d7 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -241,7 +241,7 @@ export class GitStatusFile implements GitFile { return GitFile.getFormattedDirectory(this, includeOriginal); } - getFormattedPath(options: { relativeTo?: string; separator?: string; suffix?: string } = {}): string { + getFormattedPath(options: { relativeTo?: string; suffix?: string; truncateTo?: number } = {}): string { return GitFile.getFormattedPath(this, options); } diff --git a/src/system/string.ts b/src/system/string.ts index 8d86b33..00e6a4a 100644 --- a/src/system/string.ts +++ b/src/system/string.ts @@ -250,6 +250,7 @@ export namespace Strings { export function truncate(s: string, truncateTo: number, ellipsis: string = '\u2026', width?: number) { if (!s) return s; + if (truncateTo <= 1) return ellipsis; width = width ?? getWidth(s); if (width <= truncateTo) return s; @@ -269,8 +270,31 @@ export namespace Strings { return `${s.substring(0, chars)}${ellipsis}`; } + export function truncateLeft(s: string, truncateTo: number, ellipsis: string = '\u2026', width?: number) { + if (!s) return s; + if (truncateTo <= 1) return ellipsis; + + width = width ?? getWidth(s); + if (width <= truncateTo) return s; + if (width === s.length) return `${ellipsis}${s.substring(width - truncateTo)}`; + + // Skip ahead to start as far as we can by assuming all the double-width characters won't be truncated + let chars = Math.floor(truncateTo / (width / s.length)); + let count = getWidth(s.substring(0, chars)); + while (count < truncateTo) { + count += getWidth(s[chars++]); + } + + if (count >= truncateTo) { + chars--; + } + + return `${ellipsis}${s.substring(s.length - chars)}`; + } + export function truncateMiddle(s: string, truncateTo: number, ellipsis: string = '\u2026') { if (!s) return s; + if (truncateTo <= 1) return ellipsis; const width = getWidth(s); if (width <= truncateTo) return s;