@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.
Files changed (98) hide show
  1. package/lib/common/plugin-api-rpc.d.ts +76 -0
  2. package/lib/common/plugin-api-rpc.d.ts.map +1 -1
  3. package/lib/common/plugin-api-rpc.js +9 -1
  4. package/lib/common/plugin-api-rpc.js.map +1 -1
  5. package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
  6. package/lib/hosted/browser/hosted-plugin.js +13 -6
  7. package/lib/hosted/browser/hosted-plugin.js.map +1 -1
  8. package/lib/main/browser/main-context.d.ts.map +1 -1
  9. package/lib/main/browser/main-context.js +2 -6
  10. package/lib/main/browser/main-context.js.map +1 -1
  11. package/lib/main/browser/main-file-system-event-service.d.ts +10 -2
  12. package/lib/main/browser/main-file-system-event-service.d.ts.map +1 -1
  13. package/lib/main/browser/main-file-system-event-service.js +19 -2
  14. package/lib/main/browser/main-file-system-event-service.js.map +1 -1
  15. package/lib/main/browser/menus/menus-contribution-handler.d.ts.map +1 -1
  16. package/lib/main/browser/menus/menus-contribution-handler.js +2 -0
  17. package/lib/main/browser/menus/menus-contribution-handler.js.map +1 -1
  18. package/lib/main/browser/menus/plugin-menu-command-adapter.d.ts +1 -0
  19. package/lib/main/browser/menus/plugin-menu-command-adapter.d.ts.map +1 -1
  20. package/lib/main/browser/menus/plugin-menu-command-adapter.js +13 -1
  21. package/lib/main/browser/menus/plugin-menu-command-adapter.js.map +1 -1
  22. package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts +1 -1
  23. package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts.map +1 -1
  24. package/lib/main/browser/menus/vscode-theia-menu-mappings.js +9 -2
  25. package/lib/main/browser/menus/vscode-theia-menu-mappings.js.map +1 -1
  26. package/lib/main/browser/scm-main.d.ts +33 -2
  27. package/lib/main/browser/scm-main.d.ts.map +1 -1
  28. package/lib/main/browser/scm-main.js +237 -3
  29. package/lib/main/browser/scm-main.js.map +1 -1
  30. package/lib/main/browser/scm-main.spec.d.ts +2 -0
  31. package/lib/main/browser/scm-main.spec.d.ts.map +1 -0
  32. package/lib/main/browser/scm-main.spec.js +87 -0
  33. package/lib/main/browser/scm-main.spec.js.map +1 -0
  34. package/lib/main/browser/test-main.d.ts +3 -2
  35. package/lib/main/browser/test-main.d.ts.map +1 -1
  36. package/lib/main/browser/test-main.js +12 -1
  37. package/lib/main/browser/test-main.js.map +1 -1
  38. package/lib/plugin/file-system-event-service-ext-impl.d.ts +11 -5
  39. package/lib/plugin/file-system-event-service-ext-impl.d.ts.map +1 -1
  40. package/lib/plugin/file-system-event-service-ext-impl.js +28 -9
  41. package/lib/plugin/file-system-event-service-ext-impl.js.map +1 -1
  42. package/lib/plugin/plugin-context.js +4 -4
  43. package/lib/plugin/plugin-context.js.map +1 -1
  44. package/lib/plugin/scm.d.ts +8 -2
  45. package/lib/plugin/scm.d.ts.map +1 -1
  46. package/lib/plugin/scm.js +188 -5
  47. package/lib/plugin/scm.js.map +1 -1
  48. package/lib/plugin/scm.spec.d.ts +2 -0
  49. package/lib/plugin/scm.spec.d.ts.map +1 -0
  50. package/lib/plugin/scm.spec.js +461 -0
  51. package/lib/plugin/scm.spec.js.map +1 -0
  52. package/lib/plugin/terminal-ext.d.ts +13 -3
  53. package/lib/plugin/terminal-ext.d.ts.map +1 -1
  54. package/lib/plugin/terminal-ext.js +51 -10
  55. package/lib/plugin/terminal-ext.js.map +1 -1
  56. package/lib/plugin/terminal-ext.spec.d.ts +2 -0
  57. package/lib/plugin/terminal-ext.spec.d.ts.map +1 -0
  58. package/lib/plugin/terminal-ext.spec.js +285 -0
  59. package/lib/plugin/terminal-ext.spec.js.map +1 -0
  60. package/lib/plugin/test-item.d.ts.map +1 -1
  61. package/lib/plugin/test-item.js +8 -3
  62. package/lib/plugin/test-item.js.map +1 -1
  63. package/lib/plugin/tests.d.ts.map +1 -1
  64. package/lib/plugin/tests.js +15 -3
  65. package/lib/plugin/tests.js.map +1 -1
  66. package/lib/plugin/type-converters.d.ts +2 -2
  67. package/lib/plugin/type-converters.d.ts.map +1 -1
  68. package/lib/plugin/type-converters.js +3 -9
  69. package/lib/plugin/type-converters.js.map +1 -1
  70. package/lib/plugin/types-impl.d.ts +1 -1
  71. package/lib/plugin/types-impl.d.ts.map +1 -1
  72. package/lib/plugin/types-impl.js +1 -1
  73. package/lib/plugin/types-impl.js.map +1 -1
  74. package/lib/plugin/workspace.d.ts.map +1 -1
  75. package/lib/plugin/workspace.js +17 -3
  76. package/lib/plugin/workspace.js.map +1 -1
  77. package/package.json +39 -39
  78. package/src/common/plugin-api-rpc.ts +78 -0
  79. package/src/hosted/browser/hosted-plugin.ts +13 -6
  80. package/src/main/browser/main-context.ts +3 -7
  81. package/src/main/browser/main-file-system-event-service.ts +26 -6
  82. package/src/main/browser/menus/menus-contribution-handler.ts +2 -0
  83. package/src/main/browser/menus/plugin-menu-command-adapter.ts +15 -2
  84. package/src/main/browser/menus/vscode-theia-menu-mappings.ts +12 -3
  85. package/src/main/browser/scm-main.spec.ts +105 -0
  86. package/src/main/browser/scm-main.ts +272 -4
  87. package/src/main/browser/test-main.ts +13 -3
  88. package/src/plugin/file-system-event-service-ext-impl.ts +40 -14
  89. package/src/plugin/plugin-context.ts +7 -7
  90. package/src/plugin/scm.spec.ts +615 -0
  91. package/src/plugin/scm.ts +224 -6
  92. package/src/plugin/terminal-ext.spec.ts +350 -0
  93. package/src/plugin/terminal-ext.ts +58 -12
  94. package/src/plugin/test-item.ts +8 -3
  95. package/src/plugin/tests.ts +14 -3
  96. package/src/plugin/type-converters.ts +7 -13
  97. package/src/plugin/types-impl.ts +2 -2
  98. package/src/plugin/workspace.ts +17 -3
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
1
  {
2
2
  "name": "@theia/plugin-ext",
3
- "version": "1.71.0-next.8+8ec70800c",
3
+ "version": "1.71.0",
4
4
  "description": "Theia - Plugin Extension",
5
5
  "main": "lib/common/index.js",
6
6
  "typings": "lib/common/index.d.ts",
7
7
  "dependencies": {
8
- "@theia/ai-mcp": "1.71.0-next.8+8ec70800c",
9
- "@theia/bulk-edit": "1.71.0-next.8+8ec70800c",
10
- "@theia/callhierarchy": "1.71.0-next.8+8ec70800c",
11
- "@theia/console": "1.71.0-next.8+8ec70800c",
12
- "@theia/core": "1.71.0-next.8+8ec70800c",
13
- "@theia/debug": "1.71.0-next.8+8ec70800c",
14
- "@theia/editor": "1.71.0-next.8+8ec70800c",
15
- "@theia/editor-preview": "1.71.0-next.8+8ec70800c",
16
- "@theia/file-search": "1.71.0-next.8+8ec70800c",
17
- "@theia/filesystem": "1.71.0-next.8+8ec70800c",
18
- "@theia/markers": "1.71.0-next.8+8ec70800c",
19
- "@theia/messages": "1.71.0-next.8+8ec70800c",
20
- "@theia/monaco": "1.71.0-next.8+8ec70800c",
8
+ "@theia/ai-mcp": "1.71.0",
9
+ "@theia/bulk-edit": "1.71.0",
10
+ "@theia/callhierarchy": "1.71.0",
11
+ "@theia/console": "1.71.0",
12
+ "@theia/core": "1.71.0",
13
+ "@theia/debug": "1.71.0",
14
+ "@theia/editor": "1.71.0",
15
+ "@theia/editor-preview": "1.71.0",
16
+ "@theia/file-search": "1.71.0",
17
+ "@theia/filesystem": "1.71.0",
18
+ "@theia/markers": "1.71.0",
19
+ "@theia/messages": "1.71.0",
20
+ "@theia/monaco": "1.71.0",
21
21
  "@theia/monaco-editor-core": "1.108.201",
22
- "@theia/navigator": "1.71.0-next.8+8ec70800c",
23
- "@theia/notebook": "1.71.0-next.8+8ec70800c",
24
- "@theia/output": "1.71.0-next.8+8ec70800c",
25
- "@theia/plugin": "1.71.0-next.8+8ec70800c",
26
- "@theia/preferences": "1.71.0-next.8+8ec70800c",
27
- "@theia/scm": "1.71.0-next.8+8ec70800c",
28
- "@theia/search-in-workspace": "1.71.0-next.8+8ec70800c",
29
- "@theia/task": "1.71.0-next.8+8ec70800c",
30
- "@theia/terminal": "1.71.0-next.8+8ec70800c",
31
- "@theia/test": "1.71.0-next.8+8ec70800c",
32
- "@theia/timeline": "1.71.0-next.8+8ec70800c",
33
- "@theia/typehierarchy": "1.71.0-next.8+8ec70800c",
34
- "@theia/variable-resolver": "1.71.0-next.8+8ec70800c",
35
- "@theia/workspace": "1.71.0-next.8+8ec70800c",
36
- "@types/mime": "^2.0.1",
37
- "@vscode/debugprotocol": "^1.51.0",
22
+ "@theia/navigator": "1.71.0",
23
+ "@theia/notebook": "1.71.0",
24
+ "@theia/output": "1.71.0",
25
+ "@theia/plugin": "1.71.0",
26
+ "@theia/preferences": "1.71.0",
27
+ "@theia/scm": "1.71.0",
28
+ "@theia/search-in-workspace": "1.71.0",
29
+ "@theia/task": "1.71.0",
30
+ "@theia/terminal": "1.71.0",
31
+ "@theia/test": "1.71.0",
32
+ "@theia/timeline": "1.71.0",
33
+ "@theia/typehierarchy": "1.71.0",
34
+ "@theia/variable-resolver": "1.71.0",
35
+ "@theia/workspace": "1.71.0",
36
+ "@types/mime": "^2.0.3",
37
+ "@vscode/debugprotocol": "^1.68.0",
38
38
  "@vscode/proxy-agent": "^0.13.2",
39
- "async-mutex": "^0.4.0",
39
+ "async-mutex": "^0.4.1",
40
40
  "decompress": "^4.2.1",
41
41
  "escape-html": "^1.0.3",
42
- "filenamify": "^4.1.0",
42
+ "filenamify": "^4.3.0",
43
43
  "is-electron": "^2.2.0",
44
- "jsonc-parser": "^2.2.0",
44
+ "jsonc-parser": "^2.3.1",
45
45
  "lodash.clonedeep": "^4.5.0",
46
- "macaddress": "^0.5.3",
47
- "mime": "^2.4.4",
46
+ "macaddress": "^0.5.4",
47
+ "mime": "^2.6.0",
48
48
  "node-pty": "1.2.0-beta.12",
49
- "semver": "^7.5.4",
49
+ "semver": "^7.7.4",
50
50
  "tslib": "^2.6.2",
51
51
  "vhost": "^3.0.2",
52
- "vscode-textmate": "^9.2.0"
52
+ "vscode-textmate": "^9.3.2"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public"
@@ -89,7 +89,7 @@
89
89
  "watch": "theiaext watch"
90
90
  },
91
91
  "devDependencies": {
92
- "@theia/ext-scripts": "1.70.0",
92
+ "@theia/ext-scripts": "1.71.0",
93
93
  "@types/decompress": "^4.2.2",
94
94
  "@types/escape-html": "^0.0.20",
95
95
  "@types/lodash.clonedeep": "^4.5.3"
@@ -97,5 +97,5 @@
97
97
  "nyc": {
98
98
  "extends": "../../configs/nyc.json"
99
99
  },
100
- "gitHead": "8ec70800c488b9fc44915e8896636628b7cb8636"
100
+ "gitHead": "d8a596fc99f0a8e68b466828ed162569d79e3a71"
101
101
  }
@@ -962,6 +962,17 @@ export namespace ScmCommandArg {
962
962
  }
963
963
  }
964
964
 
965
+ export interface ScmHistoryItemCommandArg {
966
+ sourceControlHandle: number;
967
+ id: string;
968
+ type: 'historyItem' | 'historyItemRef';
969
+ }
970
+ export namespace ScmHistoryItemCommandArg {
971
+ export function is(arg: unknown): arg is ScmHistoryItemCommandArg {
972
+ return isObject(arg) && 'sourceControlHandle' in arg && 'id' in arg && 'type' in arg;
973
+ }
974
+ }
975
+
965
976
  export interface ScmExt {
966
977
  createSourceControl(plugin: Plugin, id: string, label: string, rootUri?: theia.Uri): theia.SourceControl;
967
978
  getLastInputBox(plugin: Plugin): theia.SourceControlInputBox | undefined;
@@ -970,6 +981,11 @@ export interface ScmExt {
970
981
  $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string, number] | undefined>;
971
982
  $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void>;
972
983
  $provideOriginalResource(sourceControlHandle: number, uri: string, token: theia.CancellationToken): Promise<UriComponents | undefined>;
984
+ $provideHistoryItemRefs(sourceControlHandle: number, historyItemRefs: string[] | undefined, token: theia.CancellationToken): Promise<ScmHistoryItemRefDto[] | undefined>;
985
+ $provideHistoryItems(sourceControlHandle: number, options: ScmHistoryOptionsDto, token: theia.CancellationToken): Promise<ScmHistoryItemDto[] | undefined>;
986
+ $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: theia.CancellationToken): Promise<ScmHistoryItemChangeDto[] | undefined>;
987
+ $resolveHistoryItem(sourceControlHandle: number, historyItemId: string, token: theia.CancellationToken): Promise<ScmHistoryItemDto | undefined>;
988
+ $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: theia.CancellationToken): Promise<string | undefined>;
973
989
  }
974
990
 
975
991
  export namespace TimelineCommandArg {
@@ -1054,6 +1070,8 @@ export interface ScmMain {
1054
1070
  $setInputBoxEnabled(sourceControlHandle: number, enabled: boolean): void;
1055
1071
 
1056
1072
  $setActionButton(sourceControlHandle: number, actionButton: ScmActionButton | undefined): void;
1073
+ $onDidChangeCurrentHistoryItemRefs(sourceControlHandle: number): void;
1074
+ $onDidChangeHistoryItemRefs(sourceControlHandle: number, event: ScmHistoryItemRefsChangeEventDto): void;
1057
1075
  }
1058
1076
 
1059
1077
  export interface SourceControlProviderFeatures {
@@ -1063,6 +1081,10 @@ export interface SourceControlProviderFeatures {
1063
1081
  acceptInputCommand?: Command;
1064
1082
  statusBarCommands?: Command[];
1065
1083
  contextValue?: string;
1084
+ hasHistoryProvider?: boolean;
1085
+ currentHistoryItemRef?: ScmHistoryItemRefDto;
1086
+ currentHistoryItemRemoteRef?: ScmHistoryItemRefDto;
1087
+ currentHistoryItemBaseRef?: ScmHistoryItemRefDto;
1066
1088
  }
1067
1089
 
1068
1090
  export interface SourceControlGroupFeatures {
@@ -1099,6 +1121,56 @@ export interface ScmRawResourceSplices {
1099
1121
  splices: ScmRawResourceSplice[]
1100
1122
  }
1101
1123
 
1124
+ export interface ScmHistoryItemRefDto {
1125
+ id: string;
1126
+ name: string;
1127
+ description?: string;
1128
+ revision?: string;
1129
+ icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
1130
+ category?: string;
1131
+ }
1132
+
1133
+ export interface ScmHistoryItemRefsChangeEventDto {
1134
+ added: ScmHistoryItemRefDto[];
1135
+ removed: ScmHistoryItemRefDto[];
1136
+ modified: ScmHistoryItemRefDto[];
1137
+ }
1138
+
1139
+ export interface ScmHistoryItemStatisticsDto {
1140
+ files: number;
1141
+ insertions: number;
1142
+ deletions: number;
1143
+ }
1144
+
1145
+ export interface ScmHistoryItemDto {
1146
+ id: string;
1147
+ parentIds?: string[];
1148
+ subject: string;
1149
+ message?: string | MarkdownString;
1150
+ author?: string;
1151
+ authorEmail?: string;
1152
+ authorIcon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
1153
+ displayId?: string;
1154
+ timestamp?: number;
1155
+ tooltip?: string | MarkdownString;
1156
+ statistics?: ScmHistoryItemStatisticsDto;
1157
+ references?: ScmHistoryItemRefDto[];
1158
+ }
1159
+
1160
+ export interface ScmHistoryItemChangeDto {
1161
+ uri: UriComponents;
1162
+ originalUri?: UriComponents;
1163
+ modifiedUri?: UriComponents;
1164
+ renameUri?: UriComponents;
1165
+ }
1166
+
1167
+ export interface ScmHistoryOptionsDto {
1168
+ skip?: number;
1169
+ limit?: number | { id?: string };
1170
+ historyItemRefs?: string[];
1171
+ filterText?: string;
1172
+ }
1173
+
1102
1174
  export interface SourceControlResourceState {
1103
1175
  readonly handle: number
1104
1176
  /**
@@ -2120,6 +2192,11 @@ export interface ExtHostFileSystemEventServiceShape {
2120
2192
  $onDidRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): void;
2121
2193
  }
2122
2194
 
2195
+ export interface MainFileSystemEventServiceShape {
2196
+ $watch(session: number, resource: UriComponents, opts: files.WatchOptions): void;
2197
+ $unwatch(session: number): void;
2198
+ }
2199
+
2123
2200
  export interface ClipboardMain {
2124
2201
  $readText(): Promise<string>;
2125
2202
  $writeText(value: string): Promise<void>;
@@ -2384,6 +2461,7 @@ export const PLUGIN_RPC_CONTEXT = {
2384
2461
  TASKS_MAIN: createProxyIdentifier<TasksMain>('TasksMain'),
2385
2462
  DEBUG_MAIN: createProxyIdentifier<DebugMain>('DebugMain'),
2386
2463
  FILE_SYSTEM_MAIN: createProxyIdentifier<FileSystemMain>('FileSystemMain'),
2464
+ FILE_SYSTEM_EVENT_SERVICE_MAIN: createProxyIdentifier<MainFileSystemEventServiceShape>('FileSystemEventServiceMain'),
2387
2465
  SCM_MAIN: createProxyIdentifier<ScmMain>('ScmMain'),
2388
2466
  SECRETS_MAIN: createProxyIdentifier<SecretsMain>('SecretsMain'),
2389
2467
  DECORATIONS_MAIN: createProxyIdentifier<DecorationsMain>('DecorationsMain'),
@@ -267,10 +267,12 @@ export class HostedPluginSupport extends AbstractHostedPluginSupport<PluginManag
267
267
  }
268
268
 
269
269
  protected override async beforeLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
270
- // make sure that the previous state, including plugin widgets, is restored
271
- // and core layout is initialized, i.e. explorer, scm, debug views are already added to the shell
272
- // but shell is not yet revealed
273
- await this.appState.reachedState('initialized_layout');
270
+ // Make sure the shell is attached so that registries (commands, menus, views, etc.)
271
+ // are ready to accept contributions. We intentionally do NOT wait for initialized_layout
272
+ // here, because layout restoration may depend on plugin-provided file system providers
273
+ // (e.g. git: scheme for merge editors), and those providers are registered during
274
+ // startPlugins which runs after this point. Waiting for initialized_layout would deadlock.
275
+ await this.appState.reachedState('attached_shell');
274
276
  this.workspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust();
275
277
  }
276
278
 
@@ -464,12 +466,17 @@ export class HostedPluginSupport extends AbstractHostedPluginSupport<PluginManag
464
466
  }
465
467
 
466
468
  protected ensureFileSystemActivation(event: FileSystemProviderActivationEvent): void {
467
- event.waitUntil(this.activateByFileSystem(event).then(() => {
469
+ event.waitUntil((async () => {
470
+ // Wait until plugins are synced so that activation events are recorded
471
+ // and will be replayed when managers start. This does not depend on
472
+ // layout initialization, so it cannot deadlock.
473
+ await this.willStart;
474
+ await this.activateByFileSystem(event);
468
475
  if (!this.fileService.hasProvider(event.scheme)) {
469
476
  return waitForEvent(Event.filter(this.fileService.onDidChangeFileSystemProviderRegistrations,
470
477
  ({ added, scheme }) => added && scheme === event.scheme), 3000);
471
478
  }
472
- }));
479
+ })());
473
480
  }
474
481
 
475
482
  protected ensureCommandHandlerRegistration(event: WillExecuteCommandEvent): void {
@@ -171,15 +171,11 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container
171
171
  rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain);
172
172
 
173
173
  const fs = new FileSystemMainImpl(rpc, container);
174
- const fsEventService = new MainFileSystemEventService(rpc, container);
175
- const disposeFS = fs.dispose.bind(fs);
176
- fs.dispose = () => {
177
- fsEventService.dispose();
178
- disposeFS();
179
- };
180
-
181
174
  rpc.set(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN, fs);
182
175
 
176
+ const fsEventService = new MainFileSystemEventService(rpc, container);
177
+ rpc.set(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_EVENT_SERVICE_MAIN, fsEventService);
178
+
183
179
  const scmMain = new ScmMainImpl(rpc, container);
184
180
  rpc.set(PLUGIN_RPC_CONTEXT.SCM_MAIN, scmMain);
185
181
 
@@ -21,21 +21,24 @@
21
21
 
22
22
  import { interfaces } from '@theia/core/shared/inversify';
23
23
  import { RPCProtocol } from '../../common/rpc-protocol';
24
- import { MAIN_RPC_CONTEXT, FileSystemEvents } from '../../common/plugin-api-rpc';
25
- import { DisposableCollection } from '@theia/core/lib/common/disposable';
24
+ import { MAIN_RPC_CONTEXT, FileSystemEvents, MainFileSystemEventServiceShape } from '../../common/plugin-api-rpc';
25
+ import { UriComponents } from '../../common/uri-components';
26
+ import { URI } from '@theia/core';
27
+ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
26
28
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
27
- import { FileChangeType } from '@theia/filesystem/lib/common/files';
29
+ import { FileChangeType, WatchOptions } from '@theia/filesystem/lib/common/files';
28
30
 
29
- export class MainFileSystemEventService {
31
+ export class MainFileSystemEventService implements MainFileSystemEventServiceShape {
30
32
 
31
33
  private readonly toDispose = new DisposableCollection();
34
+ private readonly watches = new Map<number, Disposable>();
32
35
 
33
36
  constructor(
34
37
  rpc: RPCProtocol,
35
- container: interfaces.Container
38
+ container: interfaces.Container,
39
+ private readonly fileService = container.get(FileService)
36
40
  ) {
37
41
  const proxy = rpc.getProxy(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService);
38
- const fileService = container.get(FileService);
39
42
 
40
43
  this.toDispose.push(fileService.onDidFilesChange(event => {
41
44
  // file system events - (changes the editor and others make)
@@ -73,4 +76,21 @@ export class MainFileSystemEventService {
73
76
  dispose(): void {
74
77
  this.toDispose.dispose();
75
78
  }
79
+
80
+ $watch(session: number, resource: UriComponents, options: WatchOptions): void {
81
+ if (this.watches.has(session)) {
82
+ throw new Error(`There is already a watch request for the key ${session}`);
83
+ }
84
+ const watch = this.fileService.watch(URI.fromComponents(resource), options);
85
+ this.toDispose.push(watch);
86
+ this.watches.set(session, watch);
87
+ }
88
+
89
+ $unwatch(session: number): void {
90
+ const watch = this.watches.get(session);
91
+ if (watch) {
92
+ watch.dispose();
93
+ this.watches.delete(session);
94
+ }
95
+ }
76
96
  }
@@ -23,6 +23,7 @@ import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-too
23
23
  import { DeployedPlugin, IconUrl, Menu } from '../../../common';
24
24
  import { ScmWidget } from '@theia/scm/lib/browser/scm-widget';
25
25
  import { ScmRepositoriesWidget, SCM_SOURCE_CONTROL_TITLE_MENU } from '@theia/scm/lib/browser/scm-repositories-widget';
26
+ import { ScmHistoryGraphWidget, SCM_HISTORY_TITLE_MENU } from '@theia/scm/lib/browser/scm-history-graph-widget';
26
27
  import { KeybindingRegistry, QuickCommandService, codicon } from '@theia/core/lib/browser';
27
28
  import {
28
29
  CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint,
@@ -63,6 +64,7 @@ export class MenusContributionPointHandler {
63
64
  });
64
65
  this.tabBarToolbar.registerMenuDelegate(PLUGIN_SCM_TITLE_MENU, widget => widget instanceof ScmWidget);
65
66
  this.tabBarToolbar.registerMenuDelegate(SCM_SOURCE_CONTROL_TITLE_MENU, widget => widget instanceof ScmRepositoriesWidget);
67
+ this.tabBarToolbar.registerMenuDelegate(SCM_HISTORY_TITLE_MENU, widget => widget instanceof ScmHistoryGraphWidget);
66
68
  this.tabBarToolbar.registerMenuDelegate(PLUGIN_VIEW_TITLE_MENU, widget => !CodeEditorWidgetUtil.is(widget));
67
69
  }
68
70
 
@@ -25,7 +25,7 @@ import { DirtyDiffWidget } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-wi
25
25
  import { Change, LineRange } from '@theia/scm/lib/browser/dirty-diff/diff-computer';
26
26
  import { IChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer';
27
27
  import { TimelineItem } from '@theia/timeline/lib/common/timeline-model';
28
- import { ScmCommandArg, TimelineCommandArg, TreeViewItemReference } from '../../../common';
28
+ import { ScmCommandArg, ScmHistoryItemCommandArg, TimelineCommandArg, TreeViewItemReference } from '../../../common';
29
29
  import { TestItemReference, TestMessageArg } from '../../../common/test-types';
30
30
  import { PluginScmProvider, PluginScmResource, PluginScmResourceGroup } from '../scm-main';
31
31
  import { TreeViewWidget } from '../view/tree-view-widget';
@@ -69,9 +69,12 @@ export class PluginMenuCommandAdapter {
69
69
  ['scm/resourceFolder/context', toScmArgs],
70
70
  ['scm/resourceGroup/context', toScmArgs],
71
71
  ['scm/resourceState/context', toScmArgs],
72
+ ['scm/repository', toScmArgs],
73
+ ['scm/history/title', () => [this.toScmArg(this.scmService.selectedRepository)]],
74
+ ['scm/historyItem/context', (...args) => this.toScmHistoryArgs(...args)],
75
+ ['scm/historyItemRef/context', (...args) => this.toScmHistoryArgs(...args)],
72
76
  ['scm/title', () => [this.toScmArg(this.scmService.selectedRepository)]],
73
77
  ['scm/sourceControl', toScmArgs],
74
- ['scm/sourceControl/context', toScmArgs],
75
78
  ['scm/sourceControl/title', () => [this.toScmArg(this.scmService.selectedRepository)]],
76
79
  ['testing/message/context', toTestMessageArgs],
77
80
  ['testing/profiles/context', noArgs],
@@ -148,6 +151,16 @@ export class PluginMenuCommandAdapter {
148
151
  return scmArgs;
149
152
  }
150
153
 
154
+ protected toScmHistoryArgs(...args: any[]): any[] {
155
+ const result: any[] = [];
156
+ for (const arg of args) {
157
+ if (ScmHistoryItemCommandArg.is(arg) || ScmCommandArg.is(arg)) {
158
+ result.push(arg);
159
+ }
160
+ }
161
+ return result;
162
+ }
163
+
151
164
  protected toScmArg(arg: any): ScmCommandArg | undefined {
152
165
  if (arg instanceof ScmRepository && arg.provider instanceof PluginScmProvider) {
153
166
  return {
@@ -27,8 +27,11 @@ import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-c
27
27
  import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget';
28
28
  import { PLUGIN_SCM_CHANGE_TITLE_MENU } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget';
29
29
  import {
30
- SCM_SOURCE_CONTROL_CONTEXT_MENU, SCM_SOURCE_CONTROL_MENU, SCM_SOURCE_CONTROL_TITLE_MENU, SCM_TITLE_MENU
30
+ SCM_REPOSITORY_MENU, SCM_SOURCE_CONTROL_MENU, SCM_SOURCE_CONTROL_TITLE_MENU, SCM_TITLE_MENU
31
31
  } from '@theia/scm/lib/browser/scm-repositories-widget';
32
+ import {
33
+ SCM_HISTORY_TITLE_MENU, SCM_HISTORY_ITEM_CONTEXT_MENU, SCM_HISTORY_ITEM_REF_CONTEXT_MENU
34
+ } from '@theia/scm/lib/browser/scm-history-graph-widget';
32
35
  import { TIMELINE_ITEM_CONTEXT_MENU } from '@theia/timeline/lib/browser/timeline-tree-widget';
33
36
  import { COMMENT_CONTEXT, COMMENT_THREAD_CONTEXT, COMMENT_TITLE } from '../comments/comment-thread-widget';
34
37
  import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-view-widget';
@@ -62,9 +65,12 @@ export const implementedVSCodeContributionPoints = [
62
65
  'scm/resourceFolder/context',
63
66
  'scm/resourceGroup/context',
64
67
  'scm/resourceState/context',
68
+ 'scm/repository',
65
69
  'scm/sourceControl',
66
- 'scm/sourceControl/context',
67
70
  'scm/sourceControl/title',
71
+ 'scm/history/title',
72
+ 'scm/historyItem/context',
73
+ 'scm/historyItemRef/context',
68
74
  'scm/title',
69
75
  'timeline/item/context',
70
76
  'testing/item/context',
@@ -100,9 +106,12 @@ export const codeToTheiaMappings = new Map<string, MenuPath[]>([
100
106
  ['scm/resourceFolder/context', [ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU]],
101
107
  ['scm/resourceGroup/context', [ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU]],
102
108
  ['scm/resourceState/context', [ScmTreeWidget.RESOURCE_CONTEXT_MENU]],
109
+ ['scm/repository', [SCM_REPOSITORY_MENU]],
103
110
  ['scm/sourceControl', [SCM_SOURCE_CONTROL_MENU]],
104
- ['scm/sourceControl/context', [SCM_SOURCE_CONTROL_CONTEXT_MENU]],
105
111
  ['scm/sourceControl/title', [SCM_SOURCE_CONTROL_TITLE_MENU]],
112
+ ['scm/history/title', [SCM_HISTORY_TITLE_MENU]],
113
+ ['scm/historyItem/context', [SCM_HISTORY_ITEM_CONTEXT_MENU]],
114
+ ['scm/historyItemRef/context', [SCM_HISTORY_ITEM_REF_CONTEXT_MENU]],
106
115
  ['scm/title', [SCM_TITLE_MENU]],
107
116
  ['testing/item/context', [TEST_VIEW_CONTEXT_MENU]],
108
117
  ['testing/message/context', [TEST_RUNS_CONTEXT_MENU]],
@@ -0,0 +1,105 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH 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 { expect } from 'chai';
18
+ import { ScmService } from '@theia/scm/lib/browser/scm-service';
19
+ import { ScmMainImpl } from './scm-main';
20
+
21
+ interface ScmMainInternals {
22
+ repositories: Map<number, unknown>;
23
+ repositoryDisposables: Map<number, { dispose(): void }>;
24
+ }
25
+
26
+ function createScmMainImpl(scmService: ScmService): ScmMainImpl {
27
+ // Bypass the constructor's RPC/container wiring. ScmMainImpl's $register and
28
+ // $unregister only touch the proxy for input box validation and selection
29
+ // forwarding — not exercised in this test — so we can stub those safely.
30
+ const proxy = new Proxy({}, {
31
+ get: () => (): unknown => undefined
32
+ });
33
+ const impl = Object.create(ScmMainImpl.prototype) as ScmMainImpl;
34
+ const anyImpl = impl as unknown as Record<string, unknown>;
35
+ anyImpl.proxy = proxy;
36
+ anyImpl.scmService = scmService;
37
+ anyImpl.repositories = new Map();
38
+ anyImpl.repositoryDisposables = new Map();
39
+ anyImpl.disposables = { push: (): void => { } };
40
+ anyImpl.colors = { toCssVariableName: (x: string) => x };
41
+ anyImpl.sharedStyle = { toIconClass: () => ({ object: { iconClass: '' }, dispose: () => { } }) };
42
+ return impl;
43
+ }
44
+
45
+ describe('ScmMainImpl - cascade dispose of children on $unregisterSourceControl', () => {
46
+ let scmService: ScmService;
47
+ let impl: ScmMainImpl;
48
+
49
+ beforeEach(() => {
50
+ scmService = new ScmService();
51
+ // Stub the optional context key dependency used by ScmService.
52
+ (scmService as unknown as Record<string, unknown>).contextKeys = {};
53
+ impl = createScmMainImpl(scmService);
54
+ });
55
+
56
+ it('should unregister worktree children when their parent is unregistered', async () => {
57
+ // Parent
58
+ await impl.$registerSourceControl(1, 'git', 'Main', { scheme: 'file', path: '/repo', authority: '', query: '', fragment: '' });
59
+ // Two worktree children pointing at the parent
60
+ await impl.$registerSourceControl(2, 'git', 'WT-A', { scheme: 'file', path: '/wt-a', authority: '', query: '', fragment: '' }, 1);
61
+ await impl.$registerSourceControl(3, 'git', 'WT-B', { scheme: 'file', path: '/wt-b', authority: '', query: '', fragment: '' }, 1);
62
+
63
+ expect(scmService.repositories).to.have.length(3);
64
+
65
+ const removed: unknown[] = [];
66
+ scmService.onDidRemoveRepository(r => removed.push(r));
67
+
68
+ await impl.$unregisterSourceControl(1);
69
+
70
+ // Parent and both children must be removed from the service.
71
+ expect(scmService.repositories).to.have.length(0);
72
+ expect(removed).to.have.length(3);
73
+
74
+ // Internal bookkeeping must be clean.
75
+ const internals = impl as unknown as ScmMainInternals;
76
+ expect(internals.repositories.size).to.equal(0);
77
+ expect(internals.repositoryDisposables.size).to.equal(0);
78
+ });
79
+
80
+ it('should leave unrelated repositories untouched when a parent is unregistered', async () => {
81
+ await impl.$registerSourceControl(1, 'git', 'Main', { scheme: 'file', path: '/repo', authority: '', query: '', fragment: '' });
82
+ await impl.$registerSourceControl(2, 'git', 'WT-A', { scheme: 'file', path: '/wt-a', authority: '', query: '', fragment: '' }, 1);
83
+ // An independent repository (no parent)
84
+ await impl.$registerSourceControl(3, 'git', 'Other', { scheme: 'file', path: '/other', authority: '', query: '', fragment: '' });
85
+
86
+ await impl.$unregisterSourceControl(1);
87
+
88
+ const remaining = scmService.repositories;
89
+ expect(remaining).to.have.length(1);
90
+ expect(remaining[0].provider.rootUri).to.include('/other');
91
+ });
92
+
93
+ it('should not crash when the child was already unregistered before the parent', async () => {
94
+ await impl.$registerSourceControl(1, 'git', 'Main', { scheme: 'file', path: '/repo', authority: '', query: '', fragment: '' });
95
+ await impl.$registerSourceControl(2, 'git', 'WT-A', { scheme: 'file', path: '/wt-a', authority: '', query: '', fragment: '' }, 1);
96
+
97
+ // Plugin-side unregister for the child arrives first (race scenario).
98
+ await impl.$unregisterSourceControl(2);
99
+ expect(scmService.repositories).to.have.length(1);
100
+
101
+ // Now the parent is unregistered — cascade should find no child and succeed.
102
+ await impl.$unregisterSourceControl(1);
103
+ expect(scmService.repositories).to.have.length(0);
104
+ });
105
+ });