@theia/workspace 1.68.0-next.34 → 1.68.0-next.48

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 (26) hide show
  1. package/lib/browser/workspace-commands.d.ts +1 -0
  2. package/lib/browser/workspace-commands.d.ts.map +1 -1
  3. package/lib/browser/workspace-commands.js +5 -0
  4. package/lib/browser/workspace-commands.js.map +1 -1
  5. package/lib/browser/workspace-frontend-contribution.d.ts +5 -1
  6. package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
  7. package/lib/browser/workspace-frontend-contribution.js +41 -0
  8. package/lib/browser/workspace-frontend-contribution.js.map +1 -1
  9. package/lib/browser/workspace-trust-service.d.ts +21 -1
  10. package/lib/browser/workspace-trust-service.d.ts.map +1 -1
  11. package/lib/browser/workspace-trust-service.js +179 -9
  12. package/lib/browser/workspace-trust-service.js.map +1 -1
  13. package/lib/browser/workspace-trust-service.spec.d.ts +2 -0
  14. package/lib/browser/workspace-trust-service.spec.d.ts.map +1 -0
  15. package/lib/browser/workspace-trust-service.spec.js +204 -0
  16. package/lib/browser/workspace-trust-service.spec.js.map +1 -0
  17. package/lib/common/workspace-trust-preferences.d.ts +2 -0
  18. package/lib/common/workspace-trust-preferences.d.ts.map +1 -1
  19. package/lib/common/workspace-trust-preferences.js +11 -1
  20. package/lib/common/workspace-trust-preferences.js.map +1 -1
  21. package/package.json +5 -5
  22. package/src/browser/workspace-commands.ts +5 -0
  23. package/src/browser/workspace-frontend-contribution.ts +43 -2
  24. package/src/browser/workspace-trust-service.spec.ts +259 -0
  25. package/src/browser/workspace-trust-service.ts +206 -13
  26. package/src/common/workspace-trust-preferences.ts +11 -0
@@ -148,6 +148,11 @@ export namespace WorkspaceCommands {
148
148
  id: 'navigator.copyRelativeFilePath',
149
149
  label: 'Copy Relative Path'
150
150
  });
151
+ export const MANAGE_WORKSPACE_TRUST = Command.toDefaultLocalizedCommand({
152
+ id: 'workspace:manageTrust',
153
+ category: WORKSPACE_CATEGORY,
154
+ label: 'Manage Workspace Trust'
155
+ });
151
156
  }
152
157
 
153
158
  @injectable()
@@ -19,13 +19,15 @@ import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegist
19
19
  import { isOSX, environment } from '@theia/core';
20
20
  import {
21
21
  open, OpenerService, CommonMenus, KeybindingRegistry, KeybindingContribution,
22
- FrontendApplicationContribution, SHELL_TABBAR_CONTEXT_COPY, OnWillStopAction, Navigatable, SaveableSource, Widget
22
+ FrontendApplicationContribution, SHELL_TABBAR_CONTEXT_COPY, OnWillStopAction, Navigatable, SaveableSource, Widget,
23
+ QuickInputService, QuickPickItem
23
24
  } from '@theia/core/lib/browser';
24
25
  import { FileDialogService, OpenFileDialogProps, FileDialogTreeFilters } from '@theia/filesystem/lib/browser';
25
26
  import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
26
27
  import { WorkspaceService } from './workspace-service';
27
28
  import { WorkspaceFileService, THEIA_EXT, VSCODE_EXT } from '../common';
28
29
  import { WorkspaceCommands } from './workspace-commands';
30
+ import { WorkspaceTrustService } from './workspace-trust-service';
29
31
  import { QuickOpenWorkspace } from './quick-open-workspace';
30
32
  import URI from '@theia/core/lib/common/uri';
31
33
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
@@ -74,6 +76,10 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
74
76
  @inject(PreferenceConfigurations) protected readonly preferenceConfigurations: PreferenceConfigurations;
75
77
  @inject(FilesystemSaveableService) protected readonly saveService: FilesystemSaveableService;
76
78
  @inject(WorkspaceFileService) protected readonly workspaceFileService: WorkspaceFileService;
79
+ @inject(QuickInputService)
80
+ protected readonly quickInputService: QuickInputService;
81
+ @inject(WorkspaceTrustService)
82
+ protected readonly workspaceTrustService: WorkspaceTrustService;
77
83
 
78
84
  configure(): void {
79
85
  const workspaceExtensions = this.workspaceFileService.getWorkspaceFileExtensions();
@@ -165,7 +171,9 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
165
171
  open(this.openerService, this.workspaceService.workspace.resource);
166
172
  }
167
173
  }
168
-
174
+ });
175
+ commands.registerCommand(WorkspaceCommands.MANAGE_WORKSPACE_TRUST, {
176
+ execute: () => this.manageWorkspaceTrust()
169
177
  });
170
178
  }
171
179
 
@@ -495,6 +503,39 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
495
503
  return this.workspaceService.workspace?.resource;
496
504
  }
497
505
 
506
+ protected async manageWorkspaceTrust(): Promise<void> {
507
+ const currentTrust = await this.workspaceTrustService.getWorkspaceTrust();
508
+ const trust = nls.localizeByDefault('Trust');
509
+ const dontTrust = nls.localizeByDefault("Don't Trust");
510
+ const currentSuffix = `(${nls.localizeByDefault('Current')})`;
511
+
512
+ const items: QuickPickItem[] = [
513
+ {
514
+ label: trust,
515
+ description: currentTrust ? currentSuffix : undefined
516
+ },
517
+ {
518
+ label: dontTrust,
519
+ description: !currentTrust ? currentSuffix : undefined
520
+ }
521
+ ];
522
+
523
+ const selected = await this.quickInputService.showQuickPick(items, {
524
+ title: nls.localizeByDefault('Manage Workspace Trust'),
525
+ placeholder: nls.localize('theia/workspace/manageTrustPlaceholder', 'Select trust state for this workspace')
526
+ });
527
+
528
+ if (selected) {
529
+ const newTrust = selected.label === trust;
530
+ if (newTrust !== currentTrust) {
531
+ this.workspaceTrustService.setWorkspaceTrust(newTrust);
532
+ if (newTrust) {
533
+ await this.workspaceTrustService.addToTrustedFolders();
534
+ }
535
+ }
536
+ }
537
+ }
538
+
498
539
  onWillStop(): OnWillStopAction<boolean> | undefined {
499
540
  const { workspace } = this.workspaceService;
500
541
  if (workspace && this.workspaceService.isUntitledWorkspace(workspace.resource)) {
@@ -0,0 +1,259 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
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 * as sinon from 'sinon';
19
+ import { PreferenceChange, PreferenceScope } from '@theia/core/lib/common/preferences';
20
+ import { WorkspaceTrustService } from './workspace-trust-service';
21
+ import { WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_TRUSTED_FOLDERS } from '../common/workspace-trust-preferences';
22
+
23
+ class TestableWorkspaceTrustService extends WorkspaceTrustService {
24
+ public async testHandlePreferenceChange(change: PreferenceChange): Promise<void> {
25
+ return this.handlePreferenceChange(change);
26
+ }
27
+
28
+ public async testHandleWorkspaceChanged(): Promise<void> {
29
+ return this.handleWorkspaceChanged();
30
+ }
31
+
32
+ public setCurrentTrust(trust: boolean): void {
33
+ this.currentTrust = trust;
34
+ }
35
+
36
+ public getCurrentTrust(): boolean | undefined {
37
+ return this.currentTrust;
38
+ }
39
+
40
+ public override isWorkspaceTrustResolved(): boolean {
41
+ return super.isWorkspaceTrustResolved();
42
+ }
43
+ }
44
+
45
+ describe('WorkspaceTrustService', () => {
46
+ let service: TestableWorkspaceTrustService;
47
+
48
+ beforeEach(() => {
49
+ service = new TestableWorkspaceTrustService();
50
+ });
51
+
52
+ describe('handleWorkspaceChanged', () => {
53
+ let resolveWorkspaceTrustStub: sinon.SinonStub;
54
+ let getWorkspaceTrustStub: sinon.SinonStub;
55
+ let updateRestrictedModeIndicatorStub: sinon.SinonStub;
56
+
57
+ beforeEach(() => {
58
+ resolveWorkspaceTrustStub = sinon.stub(service as unknown as { resolveWorkspaceTrust: () => Promise<void> }, 'resolveWorkspaceTrust').resolves();
59
+ getWorkspaceTrustStub = sinon.stub(service, 'getWorkspaceTrust').resolves(true);
60
+ updateRestrictedModeIndicatorStub = sinon.stub(
61
+ service as unknown as { updateRestrictedModeIndicator: (trust: boolean) => void },
62
+ 'updateRestrictedModeIndicator'
63
+ );
64
+ });
65
+
66
+ afterEach(() => {
67
+ sinon.restore();
68
+ });
69
+
70
+ it('should reset trust state when workspace changes', async () => {
71
+ service.setCurrentTrust(true);
72
+
73
+ await service.testHandleWorkspaceChanged();
74
+
75
+ expect(service.getCurrentTrust()).to.be.undefined;
76
+ });
77
+
78
+ it('should re-evaluate trust when workspace changes', async () => {
79
+ service.setCurrentTrust(true);
80
+
81
+ await service.testHandleWorkspaceChanged();
82
+
83
+ expect(resolveWorkspaceTrustStub.calledOnce).to.be.true;
84
+ });
85
+
86
+ it('should update restricted mode indicator after workspace change if not trusted', async () => {
87
+ getWorkspaceTrustStub.resolves(false);
88
+
89
+ await service.testHandleWorkspaceChanged();
90
+
91
+ expect(updateRestrictedModeIndicatorStub.calledOnceWith(false)).to.be.true;
92
+ });
93
+
94
+ it('should reset workspaceTrust deferred to unresolved state', async () => {
95
+ // First resolve the trust
96
+ service.setCurrentTrust(true);
97
+
98
+ await service.testHandleWorkspaceChanged();
99
+
100
+ // After workspace change, it should be reset and resolved again via resolveWorkspaceTrust
101
+ expect(resolveWorkspaceTrustStub.calledOnce).to.be.true;
102
+ });
103
+ });
104
+
105
+ describe('handlePreferenceChange', () => {
106
+ let isWorkspaceInTrustedFoldersStub: sinon.SinonStub;
107
+ let setWorkspaceTrustStub: sinon.SinonStub;
108
+ let workspaceTrustPrefStub: { [key: string]: unknown };
109
+ let workspaceServiceStub: { workspace: unknown };
110
+
111
+ beforeEach(() => {
112
+ isWorkspaceInTrustedFoldersStub = sinon.stub(service as unknown as { isWorkspaceInTrustedFolders: () => boolean }, 'isWorkspaceInTrustedFolders');
113
+ setWorkspaceTrustStub = sinon.stub(service, 'setWorkspaceTrust');
114
+ // Mock workspaceTrustPref - default emptyWindow to false so trusted folders logic runs
115
+ workspaceTrustPrefStub = { [WORKSPACE_TRUST_EMPTY_WINDOW]: false };
116
+ (service as unknown as { workspaceTrustPref: { [key: string]: unknown } }).workspaceTrustPref = workspaceTrustPrefStub;
117
+ // Mock workspaceService with a workspace (non-empty)
118
+ workspaceServiceStub = { workspace: { resource: { toString: () => 'file:///some/workspace' } } };
119
+ (service as unknown as { workspaceService: { workspace: unknown } }).workspaceService = workspaceServiceStub;
120
+ });
121
+
122
+ afterEach(() => {
123
+ sinon.restore();
124
+ });
125
+
126
+ it('should update trust to true when folder is added to trustedFolders', async () => {
127
+ service.setCurrentTrust(false);
128
+ isWorkspaceInTrustedFoldersStub.returns(true);
129
+
130
+ const change: PreferenceChange = {
131
+ preferenceName: WORKSPACE_TRUST_TRUSTED_FOLDERS,
132
+ scope: PreferenceScope.User,
133
+ domain: [],
134
+ newValue: ['/some/path'],
135
+ oldValue: [],
136
+ affects: () => true
137
+ };
138
+
139
+ await service.testHandlePreferenceChange(change);
140
+
141
+ expect(setWorkspaceTrustStub.calledOnceWith(true)).to.be.true;
142
+ });
143
+
144
+ it('should update trust to false when folder is removed from trustedFolders', async () => {
145
+ service.setCurrentTrust(true);
146
+ isWorkspaceInTrustedFoldersStub.returns(false);
147
+
148
+ const change: PreferenceChange = {
149
+ preferenceName: WORKSPACE_TRUST_TRUSTED_FOLDERS,
150
+ scope: PreferenceScope.User,
151
+ domain: [],
152
+ newValue: [],
153
+ oldValue: ['/some/path'],
154
+ affects: () => true
155
+ };
156
+
157
+ await service.testHandlePreferenceChange(change);
158
+
159
+ expect(setWorkspaceTrustStub.calledOnceWith(false)).to.be.true;
160
+ });
161
+
162
+ it('should not update trust when trustedFolders change does not affect current workspace', async () => {
163
+ service.setCurrentTrust(false);
164
+ isWorkspaceInTrustedFoldersStub.returns(false);
165
+
166
+ const change: PreferenceChange = {
167
+ preferenceName: WORKSPACE_TRUST_TRUSTED_FOLDERS,
168
+ scope: PreferenceScope.User,
169
+ domain: [],
170
+ newValue: ['/other/path'],
171
+ oldValue: [],
172
+ affects: () => true
173
+ };
174
+
175
+ await service.testHandlePreferenceChange(change);
176
+
177
+ expect(setWorkspaceTrustStub.called).to.be.false;
178
+ });
179
+
180
+ describe('emptyWindow setting changes', () => {
181
+ beforeEach(() => {
182
+ // Reset workspace to undefined for empty window tests
183
+ workspaceServiceStub.workspace = undefined;
184
+ });
185
+
186
+ it('should update trust to true when emptyWindow setting changes to true for empty window', async () => {
187
+ service.setCurrentTrust(false);
188
+ workspaceServiceStub.workspace = undefined;
189
+
190
+ const change: PreferenceChange = {
191
+ preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
192
+ scope: PreferenceScope.User,
193
+ domain: [],
194
+ newValue: true,
195
+ oldValue: false,
196
+ affects: () => true
197
+ };
198
+
199
+ await service.testHandlePreferenceChange(change);
200
+
201
+ expect(setWorkspaceTrustStub.calledOnceWith(true)).to.be.true;
202
+ });
203
+
204
+ it('should update trust to false when emptyWindow setting changes to false for empty window', async () => {
205
+ service.setCurrentTrust(true);
206
+ workspaceServiceStub.workspace = undefined;
207
+
208
+ const change: PreferenceChange = {
209
+ preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
210
+ scope: PreferenceScope.User,
211
+ domain: [],
212
+ newValue: false,
213
+ oldValue: true,
214
+ affects: () => true
215
+ };
216
+
217
+ await service.testHandlePreferenceChange(change);
218
+
219
+ expect(setWorkspaceTrustStub.calledOnceWith(false)).to.be.true;
220
+ });
221
+
222
+ it('should not update trust when emptyWindow setting changes but workspace is open', async () => {
223
+ service.setCurrentTrust(false);
224
+ workspaceServiceStub.workspace = { resource: { toString: () => 'file:///some/path' } };
225
+
226
+ const change: PreferenceChange = {
227
+ preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
228
+ scope: PreferenceScope.User,
229
+ domain: [],
230
+ newValue: true,
231
+ oldValue: false,
232
+ affects: () => true
233
+ };
234
+
235
+ await service.testHandlePreferenceChange(change);
236
+
237
+ expect(setWorkspaceTrustStub.called).to.be.false;
238
+ });
239
+
240
+ it('should not update trust when emptyWindow setting changes but trust already matches', async () => {
241
+ service.setCurrentTrust(true);
242
+ workspaceServiceStub.workspace = undefined;
243
+
244
+ const change: PreferenceChange = {
245
+ preferenceName: WORKSPACE_TRUST_EMPTY_WINDOW,
246
+ scope: PreferenceScope.User,
247
+ domain: [],
248
+ newValue: true,
249
+ oldValue: false,
250
+ affects: () => true
251
+ };
252
+
253
+ await service.testHandlePreferenceChange(change);
254
+
255
+ expect(setWorkspaceTrustStub.called).to.be.false;
256
+ });
257
+ });
258
+ });
259
+ });
@@ -15,20 +15,27 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
18
- import { PreferenceChange, PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
18
+ import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
19
+ import { OS } from '@theia/core';
20
+ import { DisposableCollection } from '@theia/core/lib/common/disposable';
21
+ import { Emitter, Event } from '@theia/core/lib/common';
22
+ import URI from '@theia/core/lib/common/uri';
23
+ import { PreferenceChange, PreferenceSchemaService, PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
19
24
  import { MessageService } from '@theia/core/lib/common/message-service';
20
25
  import { nls } from '@theia/core/lib/common/nls';
21
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
22
- import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
27
+ import { inject, injectable, postConstruct, preDestroy } from '@theia/core/shared/inversify';
23
28
  import { WindowService } from '@theia/core/lib/browser/window/window-service';
24
29
  import {
25
- WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WorkspaceTrustPrompt
30
+ WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_TRUSTED_FOLDERS, WorkspaceTrustPrompt
26
31
  } from '../common/workspace-trust-preferences';
27
32
  import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
28
33
  import { WorkspaceService } from './workspace-service';
34
+ import { WorkspaceCommands } from './workspace-commands';
29
35
  import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
30
36
 
31
37
  const STORAGE_TRUSTED = 'trusted';
38
+ export const WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';
32
39
 
33
40
  @injectable()
34
41
  export class WorkspaceTrustService {
@@ -47,13 +54,26 @@ export class WorkspaceTrustService {
47
54
  @inject(WorkspaceTrustPreferences)
48
55
  protected readonly workspaceTrustPref: WorkspaceTrustPreferences;
49
56
 
57
+ @inject(PreferenceSchemaService)
58
+ protected readonly preferenceSchemaService: PreferenceSchemaService;
59
+
50
60
  @inject(WindowService)
51
61
  protected readonly windowService: WindowService;
52
62
 
53
63
  @inject(ContextKeyService)
54
64
  protected readonly contextKeyService: ContextKeyService;
55
65
 
66
+ @inject(StatusBar)
67
+ protected readonly statusBar: StatusBar;
68
+
56
69
  protected workspaceTrust = new Deferred<boolean>();
70
+ protected currentTrust: boolean | undefined;
71
+ protected pendingTrustDialog: Deferred<boolean> | undefined;
72
+
73
+ protected readonly onDidChangeWorkspaceTrustEmitter = new Emitter<boolean>();
74
+ readonly onDidChangeWorkspaceTrust: Event<boolean> = this.onDidChangeWorkspaceTrustEmitter.event;
75
+
76
+ protected readonly toDispose = new DisposableCollection(this.onDidChangeWorkspaceTrustEmitter);
57
77
 
58
78
  @postConstruct()
59
79
  protected init(): void {
@@ -62,8 +82,31 @@ export class WorkspaceTrustService {
62
82
 
63
83
  protected async doInit(): Promise<void> {
64
84
  await this.workspaceService.ready;
85
+ await this.workspaceTrustPref.ready;
86
+ await this.preferenceSchemaService.ready;
65
87
  await this.resolveWorkspaceTrust();
66
- this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change));
88
+ this.toDispose.push(
89
+ this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change))
90
+ );
91
+ this.toDispose.push(
92
+ this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChanged())
93
+ );
94
+
95
+ // Show status bar item if starting in restricted mode
96
+ const initialTrust = await this.getWorkspaceTrust();
97
+ this.updateRestrictedModeIndicator(initialTrust);
98
+
99
+ // React to trust changes
100
+ this.toDispose.push(
101
+ this.onDidChangeWorkspaceTrust(trust => {
102
+ this.updateRestrictedModeIndicator(trust);
103
+ })
104
+ );
105
+ }
106
+
107
+ @preDestroy()
108
+ protected onStop(): void {
109
+ this.toDispose.dispose();
67
110
  }
68
111
 
69
112
  getWorkspaceTrust(): Promise<boolean> {
@@ -76,18 +119,35 @@ export class WorkspaceTrustService {
76
119
  if (trust !== undefined) {
77
120
  await this.storeWorkspaceTrust(trust);
78
121
  this.contextKeyService.setContext('isWorkspaceTrusted', trust);
122
+ this.currentTrust = trust;
79
123
  this.workspaceTrust.resolve(trust);
124
+ this.onDidChangeWorkspaceTrustEmitter.fire(trust);
125
+ if (trust && this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
126
+ await this.addToTrustedFolders();
127
+ }
80
128
  }
81
129
  }
82
130
  }
83
131
 
132
+ setWorkspaceTrust(trusted: boolean): void {
133
+ if (this.currentTrust === trusted) {
134
+ return;
135
+ }
136
+ this.currentTrust = trusted;
137
+ this.contextKeyService.setContext('isWorkspaceTrusted', trusted);
138
+ if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
139
+ this.storeWorkspaceTrust(trusted);
140
+ }
141
+ this.onDidChangeWorkspaceTrustEmitter.fire(trusted);
142
+ }
143
+
84
144
  protected isWorkspaceTrustResolved(): boolean {
85
145
  return this.workspaceTrust.state !== 'unresolved';
86
146
  }
87
147
 
88
148
  protected async calculateWorkspaceTrust(): Promise<boolean | undefined> {
89
- if (!this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
90
- // in VS Code if workspace trust is disabled, we implicitly trust the workspace
149
+ const trustEnabled = this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED];
150
+ if (!trustEnabled) {
91
151
  return true;
92
152
  }
93
153
 
@@ -95,11 +155,89 @@ export class WorkspaceTrustService {
95
155
  return true;
96
156
  }
97
157
 
158
+ if (this.isWorkspaceInTrustedFolders()) {
159
+ return true;
160
+ }
161
+
98
162
  if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.NEVER) {
99
163
  return false;
100
164
  }
101
165
 
102
- return this.loadWorkspaceTrust();
166
+ // For ONCE mode, check stored trust first
167
+ if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
168
+ const storedTrust = await this.loadWorkspaceTrust();
169
+ if (storedTrust !== undefined) {
170
+ return storedTrust;
171
+ }
172
+ }
173
+
174
+ // For ALWAYS mode or ONCE mode with no stored decision, show dialog
175
+ return this.showTrustPromptDialog();
176
+ }
177
+
178
+ protected async showTrustPromptDialog(): Promise<boolean> {
179
+ // If dialog is already open, wait for its result
180
+ if (this.pendingTrustDialog) {
181
+ return this.pendingTrustDialog.promise;
182
+ }
183
+
184
+ this.pendingTrustDialog = new Deferred<boolean>();
185
+ try {
186
+ const trust = nls.localizeByDefault('Yes, I trust the authors');
187
+ const dontTrust = nls.localizeByDefault("No, I don't trust the authors");
188
+ const folderPath = this.workspaceService.workspace?.resource?.path?.toString() ?? '';
189
+
190
+ const dialog = new ConfirmDialog({
191
+ title: nls.localizeByDefault('Do you trust the authors of the files in this folder?'),
192
+ msg: nls.localize('theia/workspace/trustDialogMessage',
193
+ 'If you trust the authors of this folder, code inside may be executed. Only trust folders that you trust the contents of.') +
194
+ (folderPath ? `\n\n"${folderPath}"` : ''),
195
+ ok: trust,
196
+ cancel: dontTrust,
197
+ });
198
+
199
+ const result = await dialog.open();
200
+ const trusted = result === true;
201
+ this.pendingTrustDialog.resolve(trusted);
202
+ return trusted;
203
+ } catch (e) {
204
+ this.pendingTrustDialog.resolve(false);
205
+ throw e;
206
+ } finally {
207
+ this.pendingTrustDialog = undefined;
208
+ }
209
+ }
210
+
211
+ async addToTrustedFolders(): Promise<void> {
212
+ const workspaceUri = this.workspaceService.workspace?.resource;
213
+ if (!workspaceUri) {
214
+ return;
215
+ }
216
+ if (!this.isWorkspaceInTrustedFolders()) {
217
+ const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
218
+ await this.preferences.set(
219
+ WORKSPACE_TRUST_TRUSTED_FOLDERS,
220
+ [...currentFolders, workspaceUri.toString()],
221
+ PreferenceScope.User
222
+ );
223
+ }
224
+ }
225
+
226
+ protected isWorkspaceInTrustedFolders(): boolean {
227
+ const workspaceUri = this.workspaceService.workspace?.resource;
228
+ if (!workspaceUri) {
229
+ return false;
230
+ }
231
+ const trustedFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
232
+ const caseSensitive = !OS.backend.isWindows;
233
+ return trustedFolders.some(folder => {
234
+ try {
235
+ const folderUri = new URI(folder).normalizePath();
236
+ return workspaceUri.normalizePath().isEqual(folderUri, caseSensitive);
237
+ } catch {
238
+ return false; // Invalid URI in preferences
239
+ }
240
+ });
103
241
  }
104
242
 
105
243
  protected async loadWorkspaceTrust(): Promise<boolean | undefined> {
@@ -115,6 +253,19 @@ export class WorkspaceTrustService {
115
253
  }
116
254
 
117
255
  protected async handlePreferenceChange(change: PreferenceChange): Promise<void> {
256
+ // Handle trustedFolders changes regardless of scope
257
+ if (change.preferenceName === WORKSPACE_TRUST_TRUSTED_FOLDERS) {
258
+ // For empty windows with emptyWindow setting enabled, trust should remain true
259
+ if (this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW] && !this.workspaceService.workspace) {
260
+ return;
261
+ }
262
+ const isNowInTrustedFolders = this.isWorkspaceInTrustedFolders();
263
+ if (isNowInTrustedFolders !== this.currentTrust) {
264
+ this.setWorkspaceTrust(isNowInTrustedFolders);
265
+ }
266
+ return;
267
+ }
268
+
118
269
  if (change.scope === PreferenceScope.User) {
119
270
  if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && change.newValue !== WorkspaceTrustPrompt.ONCE) {
120
271
  this.storage.setData(STORAGE_TRUSTED, undefined);
@@ -125,12 +276,34 @@ export class WorkspaceTrustService {
125
276
  this.windowService.reload();
126
277
  }
127
278
 
128
- if (change.preferenceName === WORKSPACE_TRUST_ENABLED || change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW) {
279
+ if (change.preferenceName === WORKSPACE_TRUST_ENABLED) {
129
280
  this.resolveWorkspaceTrust();
130
281
  }
282
+
283
+ // Handle emptyWindow setting change for empty windows
284
+ if (change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW && !this.workspaceService.workspace) {
285
+ // For empty windows, directly update trust based on the new setting value
286
+ const shouldTrust = !!change.newValue;
287
+ if (this.currentTrust !== shouldTrust) {
288
+ this.setWorkspaceTrust(shouldTrust);
289
+ }
290
+ }
131
291
  }
132
292
  }
133
293
 
294
+ protected async handleWorkspaceChanged(): Promise<void> {
295
+ // Reset trust state for the new workspace
296
+ this.workspaceTrust = new Deferred<boolean>();
297
+ this.currentTrust = undefined;
298
+
299
+ // Re-evaluate trust for the new workspace
300
+ await this.resolveWorkspaceTrust();
301
+
302
+ // Update status bar indicator
303
+ const trust = await this.getWorkspaceTrust();
304
+ this.updateRestrictedModeIndicator(trust);
305
+ }
306
+
134
307
  protected async confirmRestart(): Promise<boolean> {
135
308
  const shouldRestart = await new ConfirmDialog({
136
309
  title: nls.localizeByDefault('A setting has changed that requires a restart to take effect.'),
@@ -141,13 +314,33 @@ export class WorkspaceTrustService {
141
314
  return shouldRestart === true;
142
315
  }
143
316
 
317
+ protected updateRestrictedModeIndicator(trusted: boolean): void {
318
+ if (trusted) {
319
+ this.hideRestrictedModeStatusBarItem();
320
+ } else {
321
+ this.showRestrictedModeStatusBarItem();
322
+ }
323
+ }
324
+
325
+ protected showRestrictedModeStatusBarItem(): void {
326
+ this.statusBar.setElement(WORKSPACE_TRUST_STATUS_BAR_ID, {
327
+ text: '$(shield) ' + nls.localizeByDefault('Restricted Mode'),
328
+ alignment: StatusBarAlignment.LEFT,
329
+ priority: 5000,
330
+ tooltip: nls.localize('theia/workspace/restrictedModeTooltip',
331
+ 'Running in Restricted Mode. Some features are disabled because this folder is not trusted. Click to manage trust settings.'),
332
+ command: WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id
333
+ });
334
+ }
335
+
336
+ protected hideRestrictedModeStatusBarItem(): void {
337
+ this.statusBar.removeElement(WORKSPACE_TRUST_STATUS_BAR_ID);
338
+ }
339
+
144
340
  async requestWorkspaceTrust(): Promise<boolean | undefined> {
145
341
  if (!this.isWorkspaceTrustResolved()) {
146
- const isTrusted = await this.messageService.info(nls.localize('theia/workspace/trustRequest',
147
- 'An extension requests workspace trust but the corresponding API is not yet fully supported. Do you want to trust this workspace?'),
148
- Dialog.YES, Dialog.NO);
149
- const trusted = isTrusted === Dialog.YES;
150
- this.resolveWorkspaceTrust(trusted);
342
+ const trusted = await this.showTrustPromptDialog();
343
+ await this.resolveWorkspaceTrust(trusted);
151
344
  }
152
345
  return this.workspaceTrust.promise;
153
346
  }