Skip to content

Commit bce9f0a

Browse files
committed
Allow normalizing paths on copy
1 parent 8ffe67e commit bce9f0a

14 files changed

Lines changed: 155 additions & 20 deletions

File tree

app/src/lib/app-state.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { IStashEntry } from '../models/stash-entry'
4141
import { TutorialStep } from '../models/tutorial-step'
4242
import { UncommittedChangesStrategy } from '../models/uncommitted-changes-strategy'
4343
import { ShowBranchNameInRepoListSetting } from '../models/show-branch-name-in-repo-list'
44+
import { CopyPathNormalization } from '../models/copy-path-normalization'
4445
import { BranchSortOrder } from '../models/branch-sort-order'
4546
import { CommitDateDisplay } from '../models/commit-date-display'
4647
import { DragElement } from '../models/drag-drop'
@@ -403,6 +404,9 @@ export interface IAppState {
403404
/** Controls when to show the current branch name next to each repository in the repository list */
404405
readonly showBranchNameInRepoList: ShowBranchNameInRepoListSetting
405406

407+
/** Controls slash normalization applied when copying paths to the clipboard */
408+
readonly copyPathNormalization: CopyPathNormalization
409+
406410
/** Controls the sort order for branch lists in branch-selection views */
407411
readonly branchSortOrder: BranchSortOrder
408412

app/src/lib/helpers/path.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1+
import { CopyPathNormalization } from '../../models/copy-path-normalization'
2+
13
export function normalizePath(path: string): string {
24
// Git expects forward slashes, even on Windows.
35
// Also trim trailing slashes
46
return path.replace(/\\/g, '/').replace(/\/+$/, '')
57
}
8+
9+
export function convertToCopyPath(
10+
path: string,
11+
normalization: CopyPathNormalization
12+
): string {
13+
switch (normalization) {
14+
case CopyPathNormalization.Unix:
15+
return path.replace(/\\/g, '/')
16+
case CopyPathNormalization.Windows:
17+
return path.replace(/\//g, '\\')
18+
case CopyPathNormalization.None:
19+
return path
20+
}
21+
}

app/src/lib/stores/app-store.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ import {
182182
defaultShowBranchNameInRepoListSetting,
183183
ShowBranchNameInRepoListSetting,
184184
} from '../../models/show-branch-name-in-repo-list'
185+
import {
186+
CopyPathNormalization,
187+
defaultCopyPathNormalization,
188+
} from '../../models/copy-path-normalization'
185189
import {
186190
BranchSortOrder,
187191
DEFAULT_BRANCH_SORT_ORDER,
@@ -504,6 +508,7 @@ export const showDiffCheckMarksDefault = true
504508
export const showDiffCheckMarksKey = 'diff-check-marks-visible'
505509

506510
export const showBranchNameInRepoListKey = 'show-branch-name-in-repo-list'
511+
const copyPathNormalizationKey = 'copy-path-normalization'
507512
const branchSortOrderKey = 'branch-sort-order'
508513
const commitDateDisplayKey = 'commit-date-display'
509514

@@ -676,6 +681,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
676681
private showBranchNameInRepoList: ShowBranchNameInRepoListSetting =
677682
defaultShowBranchNameInRepoListSetting
678683

684+
private copyPathNormalization: CopyPathNormalization =
685+
defaultCopyPathNormalization
686+
679687
private branchSortOrder: BranchSortOrder = DEFAULT_BRANCH_SORT_ORDER
680688

681689
private commitDateDisplay: CommitDateDisplay = defaultCommitDateDisplay
@@ -1231,6 +1239,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
12311239
underlineLinks: this.underlineLinks,
12321240
showDiffCheckMarks: this.showDiffCheckMarks,
12331241
showBranchNameInRepoList: this.showBranchNameInRepoList,
1242+
copyPathNormalization: this.copyPathNormalization,
12341243
branchSortOrder: this.branchSortOrder,
12351244
commitDateDisplay: this.commitDateDisplay,
12361245
updateState: updateStore.state,
@@ -2643,6 +2652,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
26432652
getEnum(showBranchNameInRepoListKey, ShowBranchNameInRepoListSetting) ??
26442653
defaultShowBranchNameInRepoListSetting
26452654

2655+
this.copyPathNormalization =
2656+
getEnum(copyPathNormalizationKey, CopyPathNormalization) ??
2657+
defaultCopyPathNormalization
2658+
26462659
this.branchSortOrder =
26472660
getEnum(branchSortOrderKey, BranchSortOrder) ?? DEFAULT_BRANCH_SORT_ORDER
26482661

@@ -9253,6 +9266,14 @@ export class AppStore extends TypedBaseStore<IAppState> {
92539266
}
92549267
}
92559268

9269+
public _setCopyPathNormalization(value: CopyPathNormalization) {
9270+
if (value !== this.copyPathNormalization) {
9271+
this.copyPathNormalization = value
9272+
localStorage.setItem(copyPathNormalizationKey, value)
9273+
this.emitUpdate()
9274+
}
9275+
}
9276+
92569277
public _updateBranchSortOrder(branchSortOrder: BranchSortOrder) {
92579278
if (branchSortOrder !== this.branchSortOrder) {
92589279
this.branchSortOrder = branchSortOrder
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export enum CopyPathNormalization {
2+
None = 'None',
3+
Unix = 'Unix',
4+
Windows = 'Windows',
5+
}
6+
7+
export const defaultCopyPathNormalization = CopyPathNormalization.None

app/src/ui/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,7 @@ export class App extends React.Component<IAppProps, IAppState> {
16971697
showBranchNameInRepoList={this.state.showBranchNameInRepoList}
16981698
branchSortOrder={this.state.branchSortOrder}
16991699
commitDateDisplay={this.state.commitDateDisplay}
1700+
copyPathNormalization={this.state.copyPathNormalization}
17001701
/>
17011702
)
17021703
case PopupType.RepositorySettings: {
@@ -3327,6 +3328,7 @@ export class App extends React.Component<IAppProps, IAppState> {
33273328
shellLabel: this.state.useCustomShell
33283329
? undefined
33293330
: this.state.selectedShell,
3331+
onCopyRepoPath: path => this.props.dispatcher.copyPathToClipboard(path),
33303332
})
33313333

33323334
showContextualMenu(items)

app/src/ui/changes/filter-changes-list.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import { ChangedFile } from './changed-file'
3636
import { IAutocompletionProvider } from '../autocompletion'
3737
import { showContextualMenu } from '../../lib/menu-item'
3838
import { arrayEquals } from '../../lib/equality'
39-
import { clipboard } from 'electron'
4039
import { basename } from 'path'
4140
import { Commit, ICommitContext } from '../../models/commit'
4241
import {
@@ -53,7 +52,6 @@ import { hasWritePermission } from '../../models/github-repository'
5352
import { hasConflictedFiles } from '../../lib/status'
5453
import { createObservableRef } from '../lib/observable-ref'
5554
import { Popup, PopupType } from '../../models/popup'
56-
import { EOL } from 'os'
5755
import { RepoRulesInfo } from '../../models/repo-rules'
5856
import { IAheadBehind } from '../../models/branch'
5957
import { StashDiffViewerId } from '../stashing'
@@ -656,7 +654,7 @@ export class FilterChangesList extends React.Component<
656654
label: CopyFilePathLabel,
657655
action: () => {
658656
const fullPath = Path.join(this.props.repository.path, file.path)
659-
clipboard.writeText(fullPath)
657+
this.props.dispatcher.copyPathToClipboard(fullPath)
660658
},
661659
}
662660
}
@@ -666,7 +664,8 @@ export class FilterChangesList extends React.Component<
666664
): IMenuItem => {
667665
return {
668666
label: CopyRelativeFilePathLabel,
669-
action: () => clipboard.writeText(Path.normalize(file.path)),
667+
action: () =>
668+
this.props.dispatcher.copyPathToClipboard(Path.normalize(file.path)),
670669
}
671670
}
672671

@@ -679,7 +678,7 @@ export class FilterChangesList extends React.Component<
679678
const fullPaths = files.map(file =>
680679
Path.join(this.props.repository.path, file.path)
681680
)
682-
clipboard.writeText(fullPaths.join(EOL))
681+
this.props.dispatcher.copyPathsToClipboard(fullPaths)
683682
},
684683
}
685684
}
@@ -691,7 +690,7 @@ export class FilterChangesList extends React.Component<
691690
label: CopySelectedRelativePathsLabel,
692691
action: () => {
693692
const paths = files.map(file => Path.normalize(file.path))
694-
clipboard.writeText(paths.join(EOL))
693+
this.props.dispatcher.copyPathsToClipboard(paths)
695694
},
696695
}
697696
}

app/src/ui/dispatcher/dispatcher.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Disposable, DisposableLike } from 'event-kit'
2+
import { clipboard } from 'electron'
23

34
import {
45
IAPIOrganization,
@@ -106,6 +107,7 @@ import { UncommittedChangesStrategy } from '../../models/uncommitted-changes-str
106107
import { BranchSortOrder } from '../../models/branch-sort-order'
107108
import { ShowBranchNameInRepoListSetting } from '../../models/show-branch-name-in-repo-list'
108109
import { CommitDateDisplay } from '../../models/commit-date-display'
110+
import { CopyPathNormalization } from '../../models/copy-path-normalization'
109111
import { IStashEntry } from '../../models/stash-entry'
110112
import { WorkflowPreferences } from '../../models/workflow-preferences'
111113
import { resolveWithin } from '../../lib/path'
@@ -133,6 +135,8 @@ import { CLIAction } from '../../lib/cli-action'
133135
import { IBranchNamePreset } from '../../models/branch-preset'
134136
import { BypassReasonType } from '../secret-scanning/bypass-push-protection-dialog'
135137
import { EditorOverride } from '../../models/editor-override'
138+
import { convertToCopyPath } from '../../lib/helpers/path'
139+
import { EOL } from 'os'
136140

137141
/**
138142
* An error handler function.
@@ -4213,6 +4217,24 @@ export class Dispatcher {
42134217
)
42144218
}
42154219

4220+
public setCopyPathNormalization(value: CopyPathNormalization) {
4221+
return this.appStore._setCopyPathNormalization(value)
4222+
}
4223+
4224+
public copyPathToClipboard(path: string) {
4225+
this.copyPathsToClipboard([path])
4226+
}
4227+
4228+
public copyPathsToClipboard(paths: ReadonlyArray<string>) {
4229+
clipboard.writeText(
4230+
paths
4231+
.map(p =>
4232+
convertToCopyPath(p, this.appStore.getState().copyPathNormalization)
4233+
)
4234+
.join(EOL)
4235+
)
4236+
}
4237+
42164238
public setBranchSortOrder(branchSortOrder: BranchSortOrder) {
42174239
return this.appStore._updateBranchSortOrder(branchSortOrder)
42184240
}

app/src/ui/history/selected-commits.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as React from 'react'
2-
import { clipboard } from 'electron'
32
import * as Path from 'path'
43

54
import { Repository } from '../../models/repository'
@@ -21,7 +20,6 @@ import {
2120
RevealInFileManagerLabel,
2221
OpenWithDefaultProgramLabel,
2322
} from '../lib/context-menu'
24-
import { EOL } from 'os'
2523
import { ThrottledScheduler } from '../lib/throttled-scheduler'
2624

2725
import { Dispatcher } from '../dispatcher'
@@ -422,28 +420,29 @@ export class SelectedCommits extends React.Component<
422420
? [
423421
{
424422
label: CopyFilePathLabel,
425-
action: () => clipboard.writeText(fullPath),
423+
action: () => this.props.dispatcher.copyPathToClipboard(fullPath),
426424
},
427425
{
428426
label: CopyRelativeFilePathLabel,
429-
action: () => clipboard.writeText(Path.normalize(file.path)),
427+
action: () =>
428+
this.props.dispatcher.copyPathToClipboard(
429+
Path.normalize(file.path)
430+
),
430431
},
431432
]
432433
: [
433434
{
434435
label: CopySelectedPathsLabel,
435436
action: () =>
436-
clipboard.writeText(
437-
filesToCopy
438-
.map(f => Path.join(repository.path, f.path))
439-
.join(EOL)
437+
this.props.dispatcher.copyPathsToClipboard(
438+
filesToCopy.map(f => Path.join(repository.path, f.path))
440439
),
441440
},
442441
{
443442
label: CopySelectedRelativePathsLabel,
444443
action: () =>
445-
clipboard.writeText(
446-
filesToCopy.map(f => Path.normalize(f.path)).join(EOL)
444+
this.props.dispatcher.copyPathsToClipboard(
445+
filesToCopy.map(f => Path.normalize(f.path))
447446
),
448447
},
449448
]

app/src/ui/preferences/integrations.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { suggestedExternalEditor } from '../../lib/editors/shared'
88
import { CustomIntegrationForm } from './custom-integration-form'
99
import { ICustomIntegration } from '../../lib/custom-integration'
1010
import { enableCustomIntegration } from '../../lib/feature-flag'
11+
import {
12+
CopyPathNormalization,
13+
defaultCopyPathNormalization,
14+
} from '../../models/copy-path-normalization'
1115

1216
const CustomIntegrationValue = 'other'
1317

@@ -33,6 +37,10 @@ interface IIntegrationsPreferencesProps {
3337
readonly onBranchPresetScriptChanged: (
3438
branchPresetScript: ICustomIntegration
3539
) => void
40+
readonly copyPathNormalization: CopyPathNormalization
41+
readonly onCopyPathNormalizationChanged: (
42+
value: CopyPathNormalization
43+
) => void
3644
}
3745

3846
interface IIntegrationsPreferencesState {
@@ -43,6 +51,7 @@ interface IIntegrationsPreferencesState {
4351
readonly useCustomShell: boolean
4452
readonly customShell: ICustomIntegration
4553
readonly branchPresetScript: ICustomIntegration
54+
readonly copyPathNormalization: CopyPathNormalization
4655
}
4756

4857
export class Integrations extends React.Component<
@@ -63,6 +72,8 @@ export class Integrations extends React.Component<
6372
useCustomShell: this.props.useCustomShell,
6473
customShell: this.props.customShell,
6574
branchPresetScript: this.props.branchPresetScript,
75+
copyPathNormalization:
76+
this.props.copyPathNormalization ?? defaultCopyPathNormalization,
6677
}
6778
}
6879

@@ -395,13 +406,38 @@ export class Integrations extends React.Component<
395406
this.props.onBranchPresetScriptChanged(branchPresetScript)
396407
}
397408

409+
private onCopyPathNormalizationChanged = (
410+
event: React.FormEvent<HTMLSelectElement>
411+
) => {
412+
const value = event.currentTarget.value as CopyPathNormalization
413+
this.setState({ copyPathNormalization: value })
414+
this.props.onCopyPathNormalizationChanged(value)
415+
}
416+
417+
private renderCopyPathNormalization() {
418+
return (
419+
<Select
420+
label="Normalize copied paths"
421+
value={this.state.copyPathNormalization}
422+
onChange={this.onCopyPathNormalizationChanged}
423+
>
424+
<option value={CopyPathNormalization.None}>{"Don't normalize"}</option>
425+
<option value={CopyPathNormalization.Unix}>Convert to UNIX (/)</option>
426+
<option value={CopyPathNormalization.Windows}>
427+
Convert to Windows (\)
428+
</option>
429+
</Select>
430+
)
431+
}
432+
398433
public render() {
399434
if (!enableCustomIntegration()) {
400435
return (
401436
<DialogContent>
402437
<h2>Applications</h2>
403438
<Row>{this.renderExternalEditor()}</Row>
404439
<Row>{this.renderSelectedShell()}</Row>
440+
<Row>{this.renderCopyPathNormalization()}</Row>
405441
</DialogContent>
406442
)
407443
}
@@ -435,6 +471,7 @@ export class Integrations extends React.Component<
435471
</LinkButton>
436472
</p>
437473
</fieldset>
474+
<Row>{this.renderCopyPathNormalization()}</Row>
438475
</DialogContent>
439476
)
440477
}

0 commit comments

Comments
 (0)