Skip to content

Commit 5af552d

Browse files
fix(file-tree): support copy path keybindings
1 parent 92cde92 commit 5af552d

3 files changed

Lines changed: 93 additions & 17 deletions

File tree

packages/core-browser/src/common/common.command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export namespace FILE_COMMANDS {
8383
export const COPY_RELATIVE_PATH: Command = {
8484
id: 'filetree.copy.relativepath',
8585
category: CATEGORY,
86+
label: '%file.copy.relativepath%',
8687
};
8788

8889
export const COPY_FILE: Command = {

packages/file-tree-next/__tests__/browser/file-tree-contribution.test.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
IApplicationService,
33
IClipboardService,
44
IContextKeyService,
5-
OS,
5+
OperatingSystem,
66
PreferenceService,
77
QuickOpenService,
88
} from '@opensumi/ide-core-browser';
@@ -15,6 +15,7 @@ import { EXPLORER_CONTAINER_ID } from '@opensumi/ide-explorer/lib/browser/explor
1515
import { IFileServiceClient } from '@opensumi/ide-file-service';
1616
import { IFileTreeAPI, IFileTreeService } from '@opensumi/ide-file-tree-next';
1717
import { FileTreeContribution } from '@opensumi/ide-file-tree-next/lib/browser/file-tree-contribution';
18+
import { FileTreeModelService } from '@opensumi/ide-file-tree-next/lib/browser/services/file-tree-model.service';
1819
import { IMainLayoutService, IViewsRegistry } from '@opensumi/ide-main-layout';
1920
import { ViewsRegistry } from '@opensumi/ide-main-layout/lib/browser/views-registry';
2021
import { IDialogService, IMessageService, IWindowDialogService } from '@opensumi/ide-overlay';
@@ -26,6 +27,8 @@ import { MockQuickOpenService } from '../../../quick-open/src/common/mocks/quick
2627

2728
describe('FileTreeContribution', () => {
2829
let mockInjector: MockInjector;
30+
let mockClipboardService;
31+
let mockFileTreeModelService;
2932
const tabbarHandlerMap = new Map();
3033

3134
const mockMainLayoutService = {
@@ -38,6 +41,7 @@ describe('FileTreeContribution', () => {
3841
updateViewTitle: jest.fn(),
3942
onActivate: jest.fn(),
4043
onInActivate: jest.fn(),
44+
isActivated: jest.fn(() => true),
4145
};
4246
tabbarHandlerMap.set(name, handler);
4347
return handler;
@@ -72,6 +76,22 @@ describe('FileTreeContribution', () => {
7276

7377
beforeEach(() => {
7478
mockInjector = createBrowserInjector([]);
79+
mockClipboardService = {
80+
writeText: jest.fn(),
81+
};
82+
mockFileTreeModelService = {
83+
selectedFiles: [],
84+
focusedFile: undefined,
85+
contextMenuFile: undefined,
86+
whenReady: Promise.resolve(),
87+
contextKey: {
88+
explorerViewletVisibleContext: {
89+
set: jest.fn(),
90+
},
91+
},
92+
performLocationOnHandleShow: jest.fn(),
93+
handleTreeBlur: jest.fn(),
94+
};
7595

7696
mockInjector.overrideProviders(
7797
{
@@ -112,7 +132,11 @@ describe('FileTreeContribution', () => {
112132
},
113133
{
114134
token: IClipboardService,
115-
useValue: {},
135+
useValue: mockClipboardService,
136+
},
137+
{
138+
token: FileTreeModelService,
139+
useValue: mockFileTreeModelService,
116140
},
117141
{
118142
token: IDialogService,
@@ -137,7 +161,8 @@ describe('FileTreeContribution', () => {
137161
{
138162
token: IApplicationService,
139163
useValue: {
140-
getBackendOS: () => Promise.resolve(OS.type()),
164+
backendOS: Promise.resolve(OperatingSystem.Linux),
165+
getBackendOS: () => Promise.resolve(OperatingSystem.Linux),
141166
},
142167
},
143168
);
@@ -205,4 +230,42 @@ describe('FileTreeContribution', () => {
205230
expect(register).toHaveBeenCalled();
206231
});
207232
});
233+
234+
describe('copy path commands', () => {
235+
const registerCommands = () => {
236+
const contribution = mockInjector.get(FileTreeContribution);
237+
const commands = new Map<string, any>();
238+
contribution.registerCommands({
239+
registerCommand: jest.fn((command, handler) => {
240+
commands.set(command.id, { command, ...handler });
241+
}),
242+
} as any);
243+
return commands;
244+
};
245+
246+
it('uses the selected explorer file when copy path is triggered from a shortcut', async () => {
247+
mockFileTreeModelService.selectedFiles = [{ uri: URI.file('/userhome/test.ts') }];
248+
const commands = registerCommands();
249+
250+
await commands.get('filetree.copy.path').execute();
251+
252+
expect(mockClipboardService.writeText).toHaveBeenCalledWith('/userhome/test.ts');
253+
});
254+
255+
it('uses the selected explorer file when copy relative path is triggered from a shortcut', async () => {
256+
mockFileTreeModelService.selectedFiles = [{ uri: URI.file('/userhome/test.ts') }];
257+
const commands = registerCommands();
258+
259+
await commands.get('filetree.copy.relativepath').execute();
260+
261+
expect(mockClipboardService.writeText).toHaveBeenCalledWith('test.ts');
262+
});
263+
264+
it('exposes labels for copy path commands so users can configure shortcuts', () => {
265+
const commands = registerCommands();
266+
267+
expect(commands.get('filetree.copy.path').command.label).toBe('%file.copy.path%');
268+
expect(commands.get('filetree.copy.relativepath').command.label).toBe('%file.copy.relativepath%');
269+
});
270+
});
208271
});

packages/file-tree-next/src/browser/file-tree-contribution.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ export class FileTreeContribution
210210
return resourceTitle;
211211
}
212212

213+
private getExplorerTargetUri(uri?: URI): URI | undefined {
214+
return (
215+
uri ||
216+
this.fileTreeModelService.activeUri ||
217+
this.fileTreeModelService.focusedFile?.uri ||
218+
this.fileTreeModelService.selectedFiles?.[0]?.uri ||
219+
this.fileTreeModelService.contextMenuFile?.uri
220+
);
221+
}
222+
213223
private revealFile(locationUri: URI) {
214224
if (locationUri) {
215225
if (this.isRendered) {
@@ -749,13 +759,14 @@ export class FileTreeContribution
749759

750760
commands.registerCommand<ExplorerContextCallback>(FILE_COMMANDS.COPY_PATH, {
751761
execute: async (uri) => {
752-
if (!uri) {
762+
const targetUri = this.getExplorerTargetUri(uri);
763+
if (!targetUri) {
753764
return;
754765
}
755-
const copyUri: URI = uri;
766+
const copyUri: URI = targetUri;
756767
let uriPath = copyUri.path.toString();
757-
if (uri.scheme === DIFF_SCHEME) {
758-
const query = uri.getParsedQuery();
768+
if (targetUri.scheme === DIFF_SCHEME) {
769+
const query = targetUri.getParsedQuery();
759770
uriPath = new URI(query.modified).path.toString();
760771
}
761772
let pathStr: string = decodeURIComponent(uriPath);
@@ -770,14 +781,15 @@ export class FileTreeContribution
770781

771782
commands.registerCommand<ExplorerContextCallback>(FILE_COMMANDS.COPY_RELATIVE_PATH, {
772783
execute: async (uri) => {
773-
if (!uri) {
784+
let targetUri = this.getExplorerTargetUri(uri);
785+
if (!targetUri) {
774786
return;
775787
}
776-
if (uri.scheme === DIFF_SCHEME) {
777-
const query = uri.getParsedQuery();
778-
uri = new URI(query.modified).withScheme('file');
788+
if (targetUri.scheme === DIFF_SCHEME) {
789+
const query = targetUri.getParsedQuery();
790+
targetUri = new URI(query.modified).withScheme('file');
779791
}
780-
const node = this.fileTreeService.getNodeByPathOrUri(uri);
792+
const node = this.fileTreeService.getNodeByPathOrUri(targetUri);
781793
if (node) {
782794
if (node.filestat.isInSymbolicDirectory) {
783795
// 软链接文件需要通过直接通过文件树 Path 获取
@@ -789,20 +801,20 @@ export class FileTreeContribution
789801
// 多工作区额外处理
790802
for (const root of await this.workspaceService.roots) {
791803
rootUri = new URI(root.uri);
792-
if (rootUri.isEqual(uri)) {
804+
if (rootUri.isEqual(targetUri)) {
793805
return await this.clipboardService.writeText('./');
794806
}
795-
if (rootUri.isEqualOrParent(uri)) {
796-
return await this.clipboardService.writeText(decodeURIComponent(rootUri.relative(uri)!.toString()));
807+
if (rootUri.isEqualOrParent(targetUri)) {
808+
return await this.clipboardService.writeText(decodeURIComponent(rootUri.relative(targetUri)!.toString()));
797809
}
798810
}
799811
} else {
800812
if (this.workspaceService.workspace) {
801813
rootUri = new URI(this.workspaceService.workspace.uri);
802-
if (rootUri.isEqual(uri)) {
814+
if (rootUri.isEqual(targetUri)) {
803815
return await this.clipboardService.writeText('./');
804816
}
805-
return await this.clipboardService.writeText(decodeURIComponent(rootUri.relative(uri)!.toString()));
817+
return await this.clipboardService.writeText(decodeURIComponent(rootUri.relative(targetUri)!.toString()));
806818
}
807819
}
808820
},

0 commit comments

Comments
 (0)