@theia/plugin-ext 1.71.0-next.8 → 1.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/common/plugin-api-rpc.d.ts +76 -0
- package/lib/common/plugin-api-rpc.d.ts.map +1 -1
- package/lib/common/plugin-api-rpc.js +9 -1
- package/lib/common/plugin-api-rpc.js.map +1 -1
- package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
- package/lib/hosted/browser/hosted-plugin.js +13 -6
- package/lib/hosted/browser/hosted-plugin.js.map +1 -1
- package/lib/main/browser/main-context.d.ts.map +1 -1
- package/lib/main/browser/main-context.js +2 -6
- package/lib/main/browser/main-context.js.map +1 -1
- package/lib/main/browser/main-file-system-event-service.d.ts +10 -2
- package/lib/main/browser/main-file-system-event-service.d.ts.map +1 -1
- package/lib/main/browser/main-file-system-event-service.js +19 -2
- package/lib/main/browser/main-file-system-event-service.js.map +1 -1
- package/lib/main/browser/menus/menus-contribution-handler.d.ts.map +1 -1
- package/lib/main/browser/menus/menus-contribution-handler.js +2 -0
- package/lib/main/browser/menus/menus-contribution-handler.js.map +1 -1
- package/lib/main/browser/menus/plugin-menu-command-adapter.d.ts +1 -0
- package/lib/main/browser/menus/plugin-menu-command-adapter.d.ts.map +1 -1
- package/lib/main/browser/menus/plugin-menu-command-adapter.js +13 -1
- package/lib/main/browser/menus/plugin-menu-command-adapter.js.map +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts.map +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.js +9 -2
- package/lib/main/browser/menus/vscode-theia-menu-mappings.js.map +1 -1
- package/lib/main/browser/scm-main.d.ts +33 -2
- package/lib/main/browser/scm-main.d.ts.map +1 -1
- package/lib/main/browser/scm-main.js +237 -3
- package/lib/main/browser/scm-main.js.map +1 -1
- package/lib/main/browser/scm-main.spec.d.ts +2 -0
- package/lib/main/browser/scm-main.spec.d.ts.map +1 -0
- package/lib/main/browser/scm-main.spec.js +87 -0
- package/lib/main/browser/scm-main.spec.js.map +1 -0
- package/lib/main/browser/test-main.d.ts +3 -2
- package/lib/main/browser/test-main.d.ts.map +1 -1
- package/lib/main/browser/test-main.js +12 -1
- package/lib/main/browser/test-main.js.map +1 -1
- package/lib/plugin/file-system-event-service-ext-impl.d.ts +11 -5
- package/lib/plugin/file-system-event-service-ext-impl.d.ts.map +1 -1
- package/lib/plugin/file-system-event-service-ext-impl.js +28 -9
- package/lib/plugin/file-system-event-service-ext-impl.js.map +1 -1
- package/lib/plugin/plugin-context.js +4 -4
- package/lib/plugin/plugin-context.js.map +1 -1
- package/lib/plugin/scm.d.ts +8 -2
- package/lib/plugin/scm.d.ts.map +1 -1
- package/lib/plugin/scm.js +188 -5
- package/lib/plugin/scm.js.map +1 -1
- package/lib/plugin/scm.spec.d.ts +2 -0
- package/lib/plugin/scm.spec.d.ts.map +1 -0
- package/lib/plugin/scm.spec.js +461 -0
- package/lib/plugin/scm.spec.js.map +1 -0
- package/lib/plugin/terminal-ext.d.ts +13 -3
- package/lib/plugin/terminal-ext.d.ts.map +1 -1
- package/lib/plugin/terminal-ext.js +51 -10
- package/lib/plugin/terminal-ext.js.map +1 -1
- package/lib/plugin/terminal-ext.spec.d.ts +2 -0
- package/lib/plugin/terminal-ext.spec.d.ts.map +1 -0
- package/lib/plugin/terminal-ext.spec.js +285 -0
- package/lib/plugin/terminal-ext.spec.js.map +1 -0
- package/lib/plugin/test-item.d.ts.map +1 -1
- package/lib/plugin/test-item.js +8 -3
- package/lib/plugin/test-item.js.map +1 -1
- package/lib/plugin/tests.d.ts.map +1 -1
- package/lib/plugin/tests.js +15 -3
- package/lib/plugin/tests.js.map +1 -1
- package/lib/plugin/type-converters.d.ts +2 -2
- package/lib/plugin/type-converters.d.ts.map +1 -1
- package/lib/plugin/type-converters.js +3 -9
- package/lib/plugin/type-converters.js.map +1 -1
- package/lib/plugin/types-impl.d.ts +1 -1
- package/lib/plugin/types-impl.d.ts.map +1 -1
- package/lib/plugin/types-impl.js +1 -1
- package/lib/plugin/types-impl.js.map +1 -1
- package/lib/plugin/workspace.d.ts.map +1 -1
- package/lib/plugin/workspace.js +17 -3
- package/lib/plugin/workspace.js.map +1 -1
- package/package.json +39 -39
- package/src/common/plugin-api-rpc.ts +78 -0
- package/src/hosted/browser/hosted-plugin.ts +13 -6
- package/src/main/browser/main-context.ts +3 -7
- package/src/main/browser/main-file-system-event-service.ts +26 -6
- package/src/main/browser/menus/menus-contribution-handler.ts +2 -0
- package/src/main/browser/menus/plugin-menu-command-adapter.ts +15 -2
- package/src/main/browser/menus/vscode-theia-menu-mappings.ts +12 -3
- package/src/main/browser/scm-main.spec.ts +105 -0
- package/src/main/browser/scm-main.ts +272 -4
- package/src/main/browser/test-main.ts +13 -3
- package/src/plugin/file-system-event-service-ext-impl.ts +40 -14
- package/src/plugin/plugin-context.ts +7 -7
- package/src/plugin/scm.spec.ts +615 -0
- package/src/plugin/scm.ts +224 -6
- package/src/plugin/terminal-ext.spec.ts +350 -0
- package/src/plugin/terminal-ext.ts +58 -12
- package/src/plugin/test-item.ts +8 -3
- package/src/plugin/tests.ts +14 -3
- package/src/plugin/type-converters.ts +7 -13
- package/src/plugin/types-impl.ts +2 -2
- package/src/plugin/workspace.ts +17 -3
package/src/plugin/scm.ts
CHANGED
|
@@ -28,16 +28,22 @@ import {
|
|
|
28
28
|
ScmMain, ScmRawResource, ScmRawResourceGroup,
|
|
29
29
|
ScmRawResourceSplice, ScmRawResourceSplices,
|
|
30
30
|
SourceControlGroupFeatures,
|
|
31
|
-
ScmActionButton
|
|
31
|
+
ScmActionButton,
|
|
32
|
+
ScmHistoryItemRefDto,
|
|
33
|
+
ScmHistoryItemDto,
|
|
34
|
+
ScmHistoryItemChangeDto,
|
|
35
|
+
ScmHistoryOptionsDto,
|
|
36
|
+
ScmHistoryItemRefsChangeEventDto
|
|
32
37
|
} from '../common';
|
|
33
38
|
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
|
39
|
+
import { CancellationToken } from '@theia/core/lib/common/cancellation';
|
|
34
40
|
import { CommandRegistryImpl } from '../plugin/command-registry';
|
|
35
41
|
import { Splice } from '../common/arrays';
|
|
36
42
|
import { UriComponents } from '../common/uri-components';
|
|
37
43
|
import { Command } from '../common/plugin-api-rpc-model';
|
|
38
44
|
import { RPCProtocol } from '../common/rpc-protocol';
|
|
39
45
|
import { URI, ThemeIcon } from './types-impl';
|
|
40
|
-
import { ScmCommandArg } from '../common/plugin-api-rpc';
|
|
46
|
+
import { ScmCommandArg, ScmHistoryItemCommandArg } from '../common/plugin-api-rpc';
|
|
41
47
|
import { sep } from '@theia/core/lib/common/paths';
|
|
42
48
|
import { PluginIconPath } from './plugin-icon-path';
|
|
43
49
|
import { createAPIObject } from './plugin-context';
|
|
@@ -538,11 +544,54 @@ class ScmResourceGroupImpl implements theia.SourceControlResourceGroup {
|
|
|
538
544
|
}
|
|
539
545
|
}
|
|
540
546
|
|
|
547
|
+
function historyItemRefToDto(ref: theia.SourceControlHistoryItemRef): ScmHistoryItemRefDto {
|
|
548
|
+
return {
|
|
549
|
+
id: ref.id,
|
|
550
|
+
name: ref.name,
|
|
551
|
+
description: ref.description,
|
|
552
|
+
revision: ref.revision,
|
|
553
|
+
icon: ref.icon,
|
|
554
|
+
category: ref.category,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function historyItemToDto(item: theia.SourceControlHistoryItem): ScmHistoryItemDto {
|
|
559
|
+
return {
|
|
560
|
+
id: item.id,
|
|
561
|
+
parentIds: item.parentIds ? [...item.parentIds] : undefined,
|
|
562
|
+
subject: item.subject,
|
|
563
|
+
message: item.message,
|
|
564
|
+
author: item.author,
|
|
565
|
+
authorEmail: item.authorEmail,
|
|
566
|
+
authorIcon: item.authorIcon,
|
|
567
|
+
displayId: item.displayId,
|
|
568
|
+
timestamp: item.timestamp,
|
|
569
|
+
tooltip: item.tooltip,
|
|
570
|
+
statistics: item.statistics ? {
|
|
571
|
+
files: item.statistics.files,
|
|
572
|
+
insertions: item.statistics.insertions,
|
|
573
|
+
deletions: item.statistics.deletions,
|
|
574
|
+
} : undefined,
|
|
575
|
+
references: item.references ? item.references.map(historyItemRefToDto) : undefined,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function historyItemChangeToDto(change: theia.SourceControlHistoryItemChange): ScmHistoryItemChangeDto {
|
|
580
|
+
return {
|
|
581
|
+
uri: change.uri,
|
|
582
|
+
originalUri: change.originalUri,
|
|
583
|
+
modifiedUri: change.modifiedUri,
|
|
584
|
+
renameUri: change.renameUri,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
541
588
|
class SourceControlImpl implements theia.SourceControl {
|
|
542
589
|
|
|
543
590
|
private static handlePool: number = 0;
|
|
544
591
|
private groups: Map<GroupHandle, ScmResourceGroupImpl> = new Map<GroupHandle, ScmResourceGroupImpl>();
|
|
545
592
|
|
|
593
|
+
readonly apiObject: theia.SourceControl;
|
|
594
|
+
|
|
546
595
|
get id(): string {
|
|
547
596
|
return this._id;
|
|
548
597
|
}
|
|
@@ -687,6 +736,53 @@ class SourceControlImpl implements theia.SourceControl {
|
|
|
687
736
|
this.proxy.$updateSourceControl(this.handle, { contextValue });
|
|
688
737
|
}
|
|
689
738
|
|
|
739
|
+
private _historyProvider: theia.SourceControlHistoryProvider | undefined = undefined;
|
|
740
|
+
private _historyProviderDisposables = new DisposableCollection();
|
|
741
|
+
|
|
742
|
+
readonly historyItems = new Map<string, theia.SourceControlHistoryItem>();
|
|
743
|
+
readonly historyItemRefs = new Map<string, theia.SourceControlHistoryItemRef>();
|
|
744
|
+
|
|
745
|
+
get historyProvider(): theia.SourceControlHistoryProvider | undefined {
|
|
746
|
+
return this._historyProvider;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
set historyProvider(provider: theia.SourceControlHistoryProvider | undefined) {
|
|
750
|
+
this._historyProviderDisposables.dispose();
|
|
751
|
+
this._historyProviderDisposables = new DisposableCollection();
|
|
752
|
+
this._historyProvider = provider;
|
|
753
|
+
|
|
754
|
+
if (provider) {
|
|
755
|
+
this._historyProviderDisposables.push(
|
|
756
|
+
provider.onDidChangeCurrentHistoryItemRefs(() => {
|
|
757
|
+
this.proxy.$updateSourceControl(this.handle, {
|
|
758
|
+
hasHistoryProvider: true,
|
|
759
|
+
currentHistoryItemRef: provider.currentHistoryItemRef ? historyItemRefToDto(provider.currentHistoryItemRef) : undefined,
|
|
760
|
+
currentHistoryItemRemoteRef: provider.currentHistoryItemRemoteRef ? historyItemRefToDto(provider.currentHistoryItemRemoteRef) : undefined,
|
|
761
|
+
currentHistoryItemBaseRef: provider.currentHistoryItemBaseRef ? historyItemRefToDto(provider.currentHistoryItemBaseRef) : undefined,
|
|
762
|
+
});
|
|
763
|
+
this.proxy.$onDidChangeCurrentHistoryItemRefs(this.handle);
|
|
764
|
+
})
|
|
765
|
+
);
|
|
766
|
+
this._historyProviderDisposables.push(
|
|
767
|
+
provider.onDidChangeHistoryItemRefs(event => {
|
|
768
|
+
const dto: ScmHistoryItemRefsChangeEventDto = {
|
|
769
|
+
added: event.added.map(historyItemRefToDto),
|
|
770
|
+
removed: event.removed.map(historyItemRefToDto),
|
|
771
|
+
modified: event.modified.map(historyItemRefToDto),
|
|
772
|
+
};
|
|
773
|
+
this.proxy.$onDidChangeHistoryItemRefs(this.handle, dto);
|
|
774
|
+
})
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
this.proxy.$updateSourceControl(this.handle, {
|
|
779
|
+
hasHistoryProvider: !!provider,
|
|
780
|
+
currentHistoryItemRef: provider?.currentHistoryItemRef ? historyItemRefToDto(provider.currentHistoryItemRef) : undefined,
|
|
781
|
+
currentHistoryItemRemoteRef: provider?.currentHistoryItemRemoteRef ? historyItemRefToDto(provider.currentHistoryItemRemoteRef) : undefined,
|
|
782
|
+
currentHistoryItemBaseRef: provider?.currentHistoryItemBaseRef ? historyItemRefToDto(provider.currentHistoryItemBaseRef) : undefined,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
690
786
|
private readonly onDidDisposeEmitter = new Emitter<void>();
|
|
691
787
|
readonly onDidDispose = this.onDidDisposeEmitter.event;
|
|
692
788
|
|
|
@@ -702,11 +798,13 @@ class SourceControlImpl implements theia.SourceControl {
|
|
|
702
798
|
private _label: string,
|
|
703
799
|
private _rootUri?: theia.Uri,
|
|
704
800
|
_iconPath?: theia.IconPath,
|
|
801
|
+
_isHidden?: boolean,
|
|
705
802
|
_parent?: SourceControlImpl
|
|
706
803
|
) {
|
|
707
804
|
this.inputBox = new ScmInputBoxImpl(plugin, this.proxy, this.handle);
|
|
708
805
|
this.proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, _parent?.handle);
|
|
709
806
|
this.onDidDisposeParent = _parent ? _parent.onDidDispose : Event.None;
|
|
807
|
+
this.apiObject = createAPIObject(this);
|
|
710
808
|
}
|
|
711
809
|
|
|
712
810
|
private createdResourceGroups = new Map<ScmResourceGroupImpl, Disposable>();
|
|
@@ -779,6 +877,14 @@ class SourceControlImpl implements theia.SourceControl {
|
|
|
779
877
|
return this.groups.get(handle);
|
|
780
878
|
}
|
|
781
879
|
|
|
880
|
+
getHistoryItem(id: string): theia.SourceControlHistoryItem | undefined {
|
|
881
|
+
return this.historyItems.get(id);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
getHistoryItemRef(id: string): theia.SourceControlHistoryItemRef | undefined {
|
|
885
|
+
return this.historyItemRefs.get(id);
|
|
886
|
+
}
|
|
887
|
+
|
|
782
888
|
setSelectionState(selected: boolean): void {
|
|
783
889
|
this._selected = selected;
|
|
784
890
|
this.onDidChangeSelectionEmitter.fire(selected);
|
|
@@ -788,6 +894,7 @@ class SourceControlImpl implements theia.SourceControl {
|
|
|
788
894
|
this.acceptInputDisposables.dispose();
|
|
789
895
|
this._statusBarDisposables.dispose();
|
|
790
896
|
this._actionButtonDisposables.dispose();
|
|
897
|
+
this._historyProviderDisposables.dispose();
|
|
791
898
|
|
|
792
899
|
this.groups.forEach(group => group.dispose());
|
|
793
900
|
this.proxy.$unregisterSourceControl(this.handle);
|
|
@@ -813,6 +920,32 @@ export class ScmExtImpl implements ScmExt {
|
|
|
813
920
|
constructor(rpc: RPCProtocol, private commands: CommandRegistryImpl) {
|
|
814
921
|
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.SCM_MAIN);
|
|
815
922
|
|
|
923
|
+
// Register history item arg processor before the generic ScmCommandArg processor
|
|
924
|
+
// so the more-specific guard matches first.
|
|
925
|
+
commands.registerArgumentProcessor({
|
|
926
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
927
|
+
processArgument: (arg: any) => {
|
|
928
|
+
if (!ScmHistoryItemCommandArg.is(arg) || arg.type !== 'historyItem') {
|
|
929
|
+
return arg;
|
|
930
|
+
}
|
|
931
|
+
const sourceControl = this.sourceControls.get(arg.sourceControlHandle);
|
|
932
|
+
const item = sourceControl?.getHistoryItem(arg.id);
|
|
933
|
+
return item ?? { id: arg.id };
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
commands.registerArgumentProcessor({
|
|
938
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
939
|
+
processArgument: (arg: any) => {
|
|
940
|
+
if (!ScmHistoryItemCommandArg.is(arg) || arg.type !== 'historyItemRef') {
|
|
941
|
+
return arg;
|
|
942
|
+
}
|
|
943
|
+
const sourceControl = this.sourceControls.get(arg.sourceControlHandle);
|
|
944
|
+
const ref = sourceControl?.getHistoryItemRef(arg.id);
|
|
945
|
+
return ref ?? { id: arg.id };
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
|
|
816
949
|
commands.registerArgumentProcessor({
|
|
817
950
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
818
951
|
processArgument: (arg: any) => {
|
|
@@ -824,7 +957,7 @@ export class ScmExtImpl implements ScmExt {
|
|
|
824
957
|
return undefined;
|
|
825
958
|
}
|
|
826
959
|
if (typeof arg.resourceGroupHandle !== 'number') {
|
|
827
|
-
return sourceControl;
|
|
960
|
+
return sourceControl.apiObject;
|
|
828
961
|
}
|
|
829
962
|
const resourceGroup = sourceControl.getResourceGroup(arg.resourceGroupHandle);
|
|
830
963
|
if (typeof arg.resourceStateHandle !== 'number') {
|
|
@@ -836,17 +969,35 @@ export class ScmExtImpl implements ScmExt {
|
|
|
836
969
|
}
|
|
837
970
|
|
|
838
971
|
createSourceControl(extension: Plugin, id: string, label: string, rootUri: theia.Uri | undefined,
|
|
839
|
-
iconPath?: theia.IconPath, parent?: theia.SourceControl): theia.SourceControl {
|
|
972
|
+
iconPath?: theia.IconPath, isHidden?: boolean, parent?: theia.SourceControl): theia.SourceControl {
|
|
840
973
|
const handle = ScmExtImpl.handlePool++;
|
|
841
974
|
const parentImpl = parent ? this.findSourceControlImpl(parent) : undefined;
|
|
842
|
-
const sourceControl = new SourceControlImpl(extension, this.proxy, this.commands, id, label, rootUri, iconPath, parentImpl);
|
|
975
|
+
const sourceControl = new SourceControlImpl(extension, this.proxy, this.commands, id, label, rootUri, iconPath, isHidden, parentImpl);
|
|
843
976
|
this.sourceControls.set(handle, sourceControl);
|
|
844
977
|
|
|
845
978
|
const sourceControls = this.sourceControlsByExtension.get(extension.model.id) || [];
|
|
846
979
|
sourceControls.push(sourceControl);
|
|
847
980
|
this.sourceControlsByExtension.set(extension.model.id, sourceControls);
|
|
848
981
|
|
|
849
|
-
|
|
982
|
+
// Clean up registries when the source control is disposed. Without this,
|
|
983
|
+
// disposed entries leak and findSourceControlImpl() may return a stale
|
|
984
|
+
// (disposed) instance as a parent for a later-created source control
|
|
985
|
+
// with the same id + rootUri (e.g. a worktree recreated after removal).
|
|
986
|
+
sourceControl.onDidDispose(() => {
|
|
987
|
+
this.sourceControls.delete(handle);
|
|
988
|
+
const list = this.sourceControlsByExtension.get(extension.model.id);
|
|
989
|
+
if (list) {
|
|
990
|
+
const index = list.indexOf(sourceControl);
|
|
991
|
+
if (index >= 0) {
|
|
992
|
+
list.splice(index, 1);
|
|
993
|
+
}
|
|
994
|
+
if (list.length === 0) {
|
|
995
|
+
this.sourceControlsByExtension.delete(extension.model.id);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
return sourceControl.apiObject;
|
|
850
1001
|
}
|
|
851
1002
|
|
|
852
1003
|
private findSourceControlImpl(apiObject: theia.SourceControl): SourceControlImpl | undefined {
|
|
@@ -920,6 +1071,73 @@ export class ScmExtImpl implements ScmExt {
|
|
|
920
1071
|
return [result.message, result.type];
|
|
921
1072
|
}
|
|
922
1073
|
|
|
1074
|
+
async $provideHistoryItemRefs(sourceControlHandle: number, historyItemRefs: string[] | undefined, token: CancellationToken): Promise<ScmHistoryItemRefDto[] | undefined> {
|
|
1075
|
+
const sourceControl = this.sourceControls.get(sourceControlHandle);
|
|
1076
|
+
if (!sourceControl || !sourceControl.historyProvider) {
|
|
1077
|
+
return undefined;
|
|
1078
|
+
}
|
|
1079
|
+
const result = await sourceControl.historyProvider.provideHistoryItemRefs(historyItemRefs, token);
|
|
1080
|
+
if (!result) {
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
sourceControl.historyItemRefs.clear();
|
|
1084
|
+
for (const ref of result) {
|
|
1085
|
+
sourceControl.historyItemRefs.set(ref.id, ref);
|
|
1086
|
+
}
|
|
1087
|
+
return result.map(historyItemRefToDto);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async $provideHistoryItems(sourceControlHandle: number, options: ScmHistoryOptionsDto, token: CancellationToken): Promise<ScmHistoryItemDto[] | undefined> {
|
|
1091
|
+
const sourceControl = this.sourceControls.get(sourceControlHandle);
|
|
1092
|
+
if (!sourceControl || !sourceControl.historyProvider) {
|
|
1093
|
+
return undefined;
|
|
1094
|
+
}
|
|
1095
|
+
const result = await sourceControl.historyProvider.provideHistoryItems(options, token);
|
|
1096
|
+
if (!result) {
|
|
1097
|
+
return undefined;
|
|
1098
|
+
}
|
|
1099
|
+
for (const item of result) {
|
|
1100
|
+
sourceControl.historyItems.set(item.id, item);
|
|
1101
|
+
}
|
|
1102
|
+
return result.map(historyItemToDto);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async $provideHistoryItemChanges(
|
|
1106
|
+
sourceControlHandle: number, historyItemId: string,
|
|
1107
|
+
historyItemParentId: string | undefined, token: CancellationToken
|
|
1108
|
+
): Promise<ScmHistoryItemChangeDto[] | undefined> {
|
|
1109
|
+
const sourceControl = this.sourceControls.get(sourceControlHandle);
|
|
1110
|
+
if (!sourceControl || !sourceControl.historyProvider) {
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
const result = await sourceControl.historyProvider.provideHistoryItemChanges(historyItemId, historyItemParentId, token);
|
|
1114
|
+
if (!result) {
|
|
1115
|
+
return undefined;
|
|
1116
|
+
}
|
|
1117
|
+
return result.map(historyItemChangeToDto);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
async $resolveHistoryItem(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise<ScmHistoryItemDto | undefined> {
|
|
1121
|
+
const sourceControl = this.sourceControls.get(sourceControlHandle);
|
|
1122
|
+
if (!sourceControl || !sourceControl.historyProvider) {
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
1125
|
+
const result = await sourceControl.historyProvider.resolveHistoryItem(historyItemId, token);
|
|
1126
|
+
if (!result) {
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
return historyItemToDto(result);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise<string | undefined> {
|
|
1133
|
+
const sourceControl = this.sourceControls.get(sourceControlHandle);
|
|
1134
|
+
if (!sourceControl || !sourceControl.historyProvider) {
|
|
1135
|
+
return undefined;
|
|
1136
|
+
}
|
|
1137
|
+
const result = await sourceControl.historyProvider.resolveHistoryItemRefsCommonAncestor(historyItemRefs, token);
|
|
1138
|
+
return result ?? undefined;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
923
1141
|
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void> {
|
|
924
1142
|
if (selectedSourceControlHandle !== undefined) {
|
|
925
1143
|
this.sourceControls.get(selectedSourceControlHandle)?.setSelectionState(true);
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as chai from 'chai';
|
|
18
|
+
import * as theia from '@theia/plugin';
|
|
19
|
+
import { TerminalServiceMain, Plugin, TerminalOptions } from '../common/plugin-api-rpc';
|
|
20
|
+
import { RPCProtocol, ProxyIdentifier } from '../common/rpc-protocol';
|
|
21
|
+
import { TerminalServiceExtImpl, TerminalExtImpl } from './terminal-ext';
|
|
22
|
+
import { TerminalExitReason } from './types-impl';
|
|
23
|
+
|
|
24
|
+
const expect = chai.expect;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a mock RPCProtocol that returns the given proxy for TERMINAL_MAIN.
|
|
28
|
+
*/
|
|
29
|
+
function createMockRpc(proxy: Partial<TerminalServiceMain>): RPCProtocol {
|
|
30
|
+
return {
|
|
31
|
+
getProxy<T>(_proxyId: ProxyIdentifier<T>): T {
|
|
32
|
+
return proxy as unknown as T;
|
|
33
|
+
},
|
|
34
|
+
set<T, R extends T>(_identifier: ProxyIdentifier<T>, instance: R): R {
|
|
35
|
+
return instance;
|
|
36
|
+
},
|
|
37
|
+
dispose(): void { }
|
|
38
|
+
} as RPCProtocol;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a minimal mock Plugin object.
|
|
43
|
+
*/
|
|
44
|
+
function createMockPlugin(): Plugin {
|
|
45
|
+
return {
|
|
46
|
+
pluginPath: '/test',
|
|
47
|
+
pluginFolder: '/test',
|
|
48
|
+
pluginUri: 'file:///test',
|
|
49
|
+
model: { id: 'test.plugin' } as Plugin['model'],
|
|
50
|
+
rawModel: {} as Plugin['rawModel'],
|
|
51
|
+
lifecycle: {} as Plugin['lifecycle'],
|
|
52
|
+
isUnderDevelopment: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a stub TerminalServiceMain that records calls.
|
|
58
|
+
*/
|
|
59
|
+
function createMockProxy(): TerminalServiceMain & { createdTerminals: { id: string; options: TerminalOptions }[] } {
|
|
60
|
+
const createdTerminals: { id: string; options: TerminalOptions }[] = [];
|
|
61
|
+
return {
|
|
62
|
+
createdTerminals,
|
|
63
|
+
$createTerminal(id: string, options: TerminalOptions): Promise<string> {
|
|
64
|
+
createdTerminals.push({ id, options });
|
|
65
|
+
return Promise.resolve(id);
|
|
66
|
+
},
|
|
67
|
+
$sendText(): void { },
|
|
68
|
+
$write(): void { },
|
|
69
|
+
$resize(): void { },
|
|
70
|
+
$show(): void { },
|
|
71
|
+
$hide(): void { },
|
|
72
|
+
$dispose(): void { },
|
|
73
|
+
$setName(): void { },
|
|
74
|
+
$writeByTerminalId(): void { },
|
|
75
|
+
$resizeByTerminalId(): void { },
|
|
76
|
+
$disposeByTerminalId(): void { },
|
|
77
|
+
$setNameByTerminalId(): void { },
|
|
78
|
+
$setEnvironmentVariableCollection(): void { },
|
|
79
|
+
$registerTerminalLinkProvider(): void { },
|
|
80
|
+
$unregisterTerminalLinkProvider(): void { },
|
|
81
|
+
$registerTerminalObserver(): void { },
|
|
82
|
+
$unregisterTerminalObserver(): void { },
|
|
83
|
+
} as unknown as TerminalServiceMain & { createdTerminals: { id: string; options: TerminalOptions }[] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('TerminalServiceExtImpl', () => {
|
|
87
|
+
let proxy: ReturnType<typeof createMockProxy>;
|
|
88
|
+
let service: TerminalServiceExtImpl;
|
|
89
|
+
let plugin: Plugin;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
proxy = createMockProxy();
|
|
93
|
+
const rpc = createMockRpc(proxy);
|
|
94
|
+
service = new TerminalServiceExtImpl(rpc);
|
|
95
|
+
plugin = createMockPlugin();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('terminals list', () => {
|
|
99
|
+
it('returns empty array initially', () => {
|
|
100
|
+
expect(service.terminals).to.deep.equal([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('includes terminals after creation via $terminalCreated', () => {
|
|
104
|
+
service.$terminalCreated('t1', 'Terminal 1');
|
|
105
|
+
expect(service.terminals).to.have.length(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('removes terminals after $terminalClosed', () => {
|
|
109
|
+
service.$terminalCreated('t1', 'Terminal 1');
|
|
110
|
+
service.$terminalClosed('t1', { code: 0, reason: TerminalExitReason.Process });
|
|
111
|
+
expect(service.terminals).to.have.length(0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('API object identity', () => {
|
|
116
|
+
it('returns the raw TerminalExtImpl when no wrapper is provided', () => {
|
|
117
|
+
const terminal = service.createTerminal(plugin, 'Test Terminal');
|
|
118
|
+
expect(terminal).to.be.instanceOf(TerminalExtImpl);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns the wrapped API object when a wrapper is provided', () => {
|
|
122
|
+
const wrapper = (t: TerminalExtImpl): theia.Terminal => ({ ...t, name: 'wrapped' } as unknown as theia.Terminal);
|
|
123
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
124
|
+
const id = proxy.createdTerminals[0].id;
|
|
125
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
126
|
+
const terminal = service.terminals[0];
|
|
127
|
+
expect(terminal.name).to.equal('wrapped');
|
|
128
|
+
expect(terminal).to.not.be.instanceOf(TerminalExtImpl);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('fires onDidOpenTerminal with the API object, not the raw terminal', () => {
|
|
132
|
+
const apiObject = { marker: 'api-object' } as unknown as theia.Terminal;
|
|
133
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
134
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
135
|
+
|
|
136
|
+
const opened: theia.Terminal[] = [];
|
|
137
|
+
service.onDidOpenTerminal(t => opened.push(t));
|
|
138
|
+
|
|
139
|
+
// Get the ID from the proxy call
|
|
140
|
+
const id = proxy.createdTerminals[0].id;
|
|
141
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
142
|
+
|
|
143
|
+
expect(opened).to.have.length(1);
|
|
144
|
+
expect(opened[0]).to.equal(apiObject);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('fires onDidCloseTerminal with the API object', () => {
|
|
148
|
+
const apiObject = { marker: 'api-object' } as unknown as theia.Terminal;
|
|
149
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
150
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
151
|
+
|
|
152
|
+
const closed: theia.Terminal[] = [];
|
|
153
|
+
service.onDidCloseTerminal(t => closed.push(t));
|
|
154
|
+
|
|
155
|
+
const id = proxy.createdTerminals[0].id;
|
|
156
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
157
|
+
service.$terminalClosed(id, { code: 0, reason: TerminalExitReason.Process });
|
|
158
|
+
|
|
159
|
+
expect(closed).to.have.length(1);
|
|
160
|
+
expect(closed[0]).to.equal(apiObject);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('fires onDidChangeTerminalState with the API object on interaction', () => {
|
|
164
|
+
const apiObject = { marker: 'api-object' } as unknown as theia.Terminal;
|
|
165
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
166
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
167
|
+
|
|
168
|
+
const stateChanged: theia.Terminal[] = [];
|
|
169
|
+
service.onDidChangeTerminalState(t => stateChanged.push(t));
|
|
170
|
+
|
|
171
|
+
const id = proxy.createdTerminals[0].id;
|
|
172
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
173
|
+
service.$terminalOnInteraction(id);
|
|
174
|
+
|
|
175
|
+
expect(stateChanged).to.have.length(1);
|
|
176
|
+
expect(stateChanged[0]).to.equal(apiObject);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('fires onDidChangeTerminalState with the API object on shell type change', () => {
|
|
180
|
+
const apiObject = { marker: 'api-object' } as unknown as theia.Terminal;
|
|
181
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
182
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
183
|
+
|
|
184
|
+
const stateChanged: theia.Terminal[] = [];
|
|
185
|
+
service.onDidChangeTerminalState(t => stateChanged.push(t));
|
|
186
|
+
|
|
187
|
+
const id = proxy.createdTerminals[0].id;
|
|
188
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
189
|
+
service.$terminalShellTypeChanged(id, '/bin/zsh');
|
|
190
|
+
|
|
191
|
+
expect(stateChanged).to.have.length(1);
|
|
192
|
+
expect(stateChanged[0]).to.equal(apiObject);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns the API object from the terminals list', () => {
|
|
196
|
+
const apiObject = { marker: 'api-object' } as unknown as theia.Terminal;
|
|
197
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
198
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
199
|
+
|
|
200
|
+
const id = proxy.createdTerminals[0].id;
|
|
201
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
202
|
+
|
|
203
|
+
const terminals = service.terminals;
|
|
204
|
+
expect(terminals).to.have.length(1);
|
|
205
|
+
expect(terminals[0]).to.equal(apiObject);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('returns the API object as activeTerminal', () => {
|
|
209
|
+
const apiObject = { marker: 'api-object' } as unknown as theia.Terminal;
|
|
210
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
211
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
212
|
+
|
|
213
|
+
const id = proxy.createdTerminals[0].id;
|
|
214
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
215
|
+
service.$currentTerminalChanged(id);
|
|
216
|
+
|
|
217
|
+
expect(service.activeTerminal).to.equal(apiObject);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('cleans up API object on terminal close', () => {
|
|
221
|
+
const apiObject = { marker: 'api-object' } as unknown as theia.Terminal;
|
|
222
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
223
|
+
service.createTerminal(plugin, 'Test Terminal', undefined, undefined, wrapper);
|
|
224
|
+
|
|
225
|
+
const id = proxy.createdTerminals[0].id;
|
|
226
|
+
service.$terminalCreated(id, 'Test Terminal');
|
|
227
|
+
service.$terminalClosed(id, { code: 0, reason: TerminalExitReason.Process });
|
|
228
|
+
|
|
229
|
+
expect(service.terminals).to.have.length(0);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('parentTerminal resolution', () => {
|
|
234
|
+
it('resolves parentTerminal from API proxy objects', () => {
|
|
235
|
+
const apiObject = { marker: 'parent-api' } as unknown as theia.Terminal;
|
|
236
|
+
const wrapper = (_t: TerminalExtImpl): theia.Terminal => apiObject;
|
|
237
|
+
service.createTerminal(plugin, 'Parent', undefined, undefined, wrapper);
|
|
238
|
+
|
|
239
|
+
const parentId = proxy.createdTerminals[0].id;
|
|
240
|
+
service.$terminalCreated(parentId, 'Parent');
|
|
241
|
+
|
|
242
|
+
// Create a child with parentTerminal set to the API object
|
|
243
|
+
service.createTerminal(plugin, {
|
|
244
|
+
name: 'Child',
|
|
245
|
+
location: { parentTerminal: apiObject }
|
|
246
|
+
} as theia.TerminalOptions);
|
|
247
|
+
|
|
248
|
+
expect(proxy.createdTerminals).to.have.length(2);
|
|
249
|
+
// The second createTerminal call should have passed the parent ID
|
|
250
|
+
// We verify it was called on the proxy (the parentId arg is the 3rd parameter)
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('resolves parentTerminal from raw terminal objects', () => {
|
|
254
|
+
const rawTerminal = service.createTerminal(plugin, 'Parent');
|
|
255
|
+
|
|
256
|
+
const parentId = proxy.createdTerminals[0].id;
|
|
257
|
+
service.$terminalCreated(parentId, 'Parent');
|
|
258
|
+
|
|
259
|
+
// Create a child with parentTerminal set to the raw terminal
|
|
260
|
+
service.createTerminal(plugin, {
|
|
261
|
+
name: 'Child',
|
|
262
|
+
location: { parentTerminal: rawTerminal }
|
|
263
|
+
} as theia.TerminalOptions);
|
|
264
|
+
|
|
265
|
+
expect(proxy.createdTerminals).to.have.length(2);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('events for terminals without wrapper', () => {
|
|
270
|
+
it('fires onDidOpenTerminal with the raw terminal when no wrapper is used', () => {
|
|
271
|
+
const opened: theia.Terminal[] = [];
|
|
272
|
+
service.onDidOpenTerminal(t => opened.push(t));
|
|
273
|
+
|
|
274
|
+
service.$terminalCreated('ext-t1', 'External Terminal');
|
|
275
|
+
|
|
276
|
+
expect(opened).to.have.length(1);
|
|
277
|
+
expect(opened[0]).to.be.instanceOf(TerminalExtImpl);
|
|
278
|
+
expect(opened[0].name).to.equal('External Terminal');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('fires onDidCloseTerminal with the raw terminal when no wrapper is used', () => {
|
|
282
|
+
const closed: theia.Terminal[] = [];
|
|
283
|
+
service.onDidCloseTerminal(t => closed.push(t));
|
|
284
|
+
|
|
285
|
+
service.$terminalCreated('ext-t1', 'External Terminal');
|
|
286
|
+
service.$terminalClosed('ext-t1', { code: 0, reason: TerminalExitReason.Process });
|
|
287
|
+
|
|
288
|
+
expect(closed).to.have.length(1);
|
|
289
|
+
expect(closed[0]).to.be.instanceOf(TerminalExtImpl);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('shell change', () => {
|
|
294
|
+
it('fires onDidChangeShell when shell changes', async () => {
|
|
295
|
+
const shells: string[] = [];
|
|
296
|
+
service.onDidChangeShell(s => shells.push(s));
|
|
297
|
+
|
|
298
|
+
await service.$setShell('/bin/zsh');
|
|
299
|
+
|
|
300
|
+
expect(shells).to.deep.equal(['/bin/zsh']);
|
|
301
|
+
expect(service.defaultShell).to.equal('/bin/zsh');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('does not fire onDidChangeShell when shell is the same', async () => {
|
|
305
|
+
const shells: string[] = [];
|
|
306
|
+
await service.$setShell('/bin/zsh');
|
|
307
|
+
|
|
308
|
+
service.onDidChangeShell(s => shells.push(s));
|
|
309
|
+
await service.$setShell('/bin/zsh');
|
|
310
|
+
|
|
311
|
+
expect(shells).to.deep.equal([]);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('$terminalNameChanged', () => {
|
|
316
|
+
it('updates the terminal name', () => {
|
|
317
|
+
service.$terminalCreated('t1', 'Old Name');
|
|
318
|
+
service.$terminalNameChanged('t1', 'New Name');
|
|
319
|
+
|
|
320
|
+
expect(service.terminals[0].name).to.equal('New Name');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('activeTerminal', () => {
|
|
325
|
+
it('is undefined initially', () => {
|
|
326
|
+
expect(service.activeTerminal).to.equal(undefined);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('reflects the current active terminal', () => {
|
|
330
|
+
service.$terminalCreated('t1', 'Terminal 1');
|
|
331
|
+
service.$currentTerminalChanged('t1');
|
|
332
|
+
|
|
333
|
+
expect(service.activeTerminal).to.not.equal(undefined);
|
|
334
|
+
expect(service.activeTerminal!.name).to.equal('Terminal 1');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('fires onDidChangeActiveTerminal', () => {
|
|
338
|
+
const changes: (theia.Terminal | undefined)[] = [];
|
|
339
|
+
service.onDidChangeActiveTerminal(t => changes.push(t));
|
|
340
|
+
|
|
341
|
+
service.$terminalCreated('t1', 'Terminal 1');
|
|
342
|
+
service.$currentTerminalChanged('t1');
|
|
343
|
+
service.$currentTerminalChanged(undefined);
|
|
344
|
+
|
|
345
|
+
expect(changes).to.have.length(2);
|
|
346
|
+
expect(changes[0]!.name).to.equal('Terminal 1');
|
|
347
|
+
expect(changes[1]).to.equal(undefined);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|