diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 6e2a7acfd77..c521c15900d 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry\n\n// current milestone name\n$milestone=milestone:\"October 2021\"" + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub\n\n// current milestone name\n$milestone=milestone:\"October 2021\"" }, { "kind": 1, diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 3a469148da6..e6afeb22d1d 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -83,7 +83,7 @@ export class SettingsDocument { completions.push(this.newSimpleCompletionItem('${folderPath}', range, localize('folderPath', "file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)"))); completions.push(this.newSimpleCompletionItem('${appName}', range, localize('appName', "e.g. VS Code"))); completions.push(this.newSimpleCompletionItem('${remoteName}', range, localize('remoteName', "e.g. SSH"))); - completions.push(this.newSimpleCompletionItem('${dirty}', range, localize('dirty', "a dirty indicator if the active editor is dirty"))); + completions.push(this.newSimpleCompletionItem('${dirty}', range, localize('dirty', "an indicator for when the active editor has unsaved changes"))); completions.push(this.newSimpleCompletionItem('${separator}', range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values"))); return Promise.resolve(completions); diff --git a/extensions/xml/xml.language-configuration.json b/extensions/xml/xml.language-configuration.json index 22842960497..d04db08380c 100644 --- a/extensions/xml/xml.language-configuration.json +++ b/extensions/xml/xml.language-configuration.json @@ -25,6 +25,8 @@ { "open": "(", "close": ")" }, { "open": "<", "close": ">" } ], + "colorizedBracketPairs": [ + ], "folding": { "markers": { "start": "^\\s*", diff --git a/resources/linux/snap/electron-launch b/resources/linux/snap/electron-launch index 9f4eb6a23b8..87011928b49 100755 --- a/resources/linux/snap/electron-launch +++ b/resources/linux/snap/electron-launch @@ -29,6 +29,6 @@ if [ -f "$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" ]; then fi # Create $XDG_RUNTIME_DIR if not exists (to be removed when https://pad.lv/1656340 is fixed) -[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p "$XDG_RUNTIME_DIR" -m 700 +[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p -m 700 "$XDG_RUNTIME_DIR" exec "$@" diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 132b8f79e65..b45efcf3784 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -741,13 +741,17 @@ interface ResourceMapKeyFn { (resource: URI): string; } +class ResourceMapEntry { + constructor(readonly uri: URI, readonly value: T) { } +} + export class ResourceMap implements Map { private static readonly defaultToKey = (resource: URI) => resource.toString(); readonly [Symbol.toStringTag] = 'ResourceMap'; - private readonly map: Map; + private readonly map: Map>; private readonly toKey: ResourceMapKeyFn; /** @@ -774,12 +778,12 @@ export class ResourceMap implements Map { } set(resource: URI, value: T): this { - this.map.set(this.toKey(resource), value); + this.map.set(this.toKey(resource), new ResourceMapEntry(resource, value)); return this; } get(resource: URI): T | undefined { - return this.map.get(this.toKey(resource)); + return this.map.get(this.toKey(resource))?.value; } has(resource: URI): boolean { @@ -802,30 +806,32 @@ export class ResourceMap implements Map { if (typeof thisArg !== 'undefined') { clb = clb.bind(thisArg); } - for (let [index, value] of this.map) { - clb(value, URI.parse(index), this); + for (let [_, entry] of this.map) { + clb(entry.value, entry.uri, this); } } - values(): IterableIterator { - return this.map.values(); + *values(): IterableIterator { + for (let entry of this.map.values()) { + yield entry.value; + } } *keys(): IterableIterator { - for (let key of this.map.keys()) { - yield URI.parse(key); + for (let entry of this.map.values()) { + yield entry.uri; } } *entries(): IterableIterator<[URI, T]> { - for (let tuple of this.map.entries()) { - yield [URI.parse(tuple[0]), tuple[1]]; + for (let entry of this.map.values()) { + yield [entry.uri, entry.value]; } } *[Symbol.iterator](): IterableIterator<[URI, T]> { - for (let item of this.map) { - yield [URI.parse(item[0]), item[1]]; + for (let [, entry] of this.map) { + yield [entry.uri, entry.value]; } } } diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index 5b4af59bcb1..824d6b22f52 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -13,6 +13,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { Promises } from 'vs/base/node/pfs'; import { isStorageItemsChangeEvent, IStorageDatabase, IStorageItemsChangeEvent, Storage } from 'vs/base/parts/storage/common/storage'; import { ISQLiteStorageDatabaseOptions, SQLiteStorageDatabase } from 'vs/base/parts/storage/node/storage'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; flakySuite('Storage Library', function () { @@ -29,139 +30,142 @@ flakySuite('Storage Library', function () { return Promises.rm(testDir); }); - test('basics', async () => { - const storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); + test('basics', () => { + return runWithFakedTimers({}, async function () { + const storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); - await storage.init(); + await storage.init(); - // Empty fallbacks - strictEqual(storage.get('foo', 'bar'), 'bar'); - strictEqual(storage.getNumber('foo', 55), 55); - strictEqual(storage.getBoolean('foo', true), true); + // Empty fallbacks + strictEqual(storage.get('foo', 'bar'), 'bar'); + strictEqual(storage.getNumber('foo', 55), 55); + strictEqual(storage.getBoolean('foo', true), true); - let changes = new Set(); - storage.onDidChangeStorage(key => { - changes.add(key); + let changes = new Set(); + storage.onDidChangeStorage(key => { + changes.add(key); + }); + + await storage.whenFlushed(); // returns immediately when no pending updates + + // Simple updates + const set1Promise = storage.set('bar', 'foo'); + const set2Promise = storage.set('barNumber', 55); + const set3Promise = storage.set('barBoolean', true); + + let flushPromiseResolved = false; + storage.whenFlushed().then(() => flushPromiseResolved = true); + + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.getNumber('barNumber'), 55); + strictEqual(storage.getBoolean('barBoolean'), true); + + strictEqual(changes.size, 3); + ok(changes.has('bar')); + ok(changes.has('barNumber')); + ok(changes.has('barBoolean')); + + let setPromiseResolved = false; + await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true); + strictEqual(setPromiseResolved, true); + strictEqual(flushPromiseResolved, true); + + changes = new Set(); + + // Does not trigger events for same update values + storage.set('bar', 'foo'); + storage.set('barNumber', 55); + storage.set('barBoolean', true); + strictEqual(changes.size, 0); + + // Simple deletes + const delete1Promise = storage.delete('bar'); + const delete2Promise = storage.delete('barNumber'); + const delete3Promise = storage.delete('barBoolean'); + + ok(!storage.get('bar')); + ok(!storage.getNumber('barNumber')); + ok(!storage.getBoolean('barBoolean')); + + strictEqual(changes.size, 3); + ok(changes.has('bar')); + ok(changes.has('barNumber')); + ok(changes.has('barBoolean')); + + changes = new Set(); + + // Does not trigger events for same delete values + storage.delete('bar'); + storage.delete('barNumber'); + storage.delete('barBoolean'); + strictEqual(changes.size, 0); + + let deletePromiseResolved = false; + await Promise.all([delete1Promise, delete2Promise, delete3Promise]).then(() => deletePromiseResolved = true); + strictEqual(deletePromiseResolved, true); + + await storage.close(); + await storage.close(); // it is ok to call this multiple times }); - - await storage.whenFlushed(); // returns immediately when no pending updates - - // Simple updates - const set1Promise = storage.set('bar', 'foo'); - const set2Promise = storage.set('barNumber', 55); - const set3Promise = storage.set('barBoolean', true); - - let flushPromiseResolved = false; - storage.whenFlushed().then(() => flushPromiseResolved = true); - - strictEqual(storage.get('bar'), 'foo'); - strictEqual(storage.getNumber('barNumber'), 55); - strictEqual(storage.getBoolean('barBoolean'), true); - - strictEqual(changes.size, 3); - ok(changes.has('bar')); - ok(changes.has('barNumber')); - ok(changes.has('barBoolean')); - - let setPromiseResolved = false; - await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true); - strictEqual(setPromiseResolved, true); - strictEqual(flushPromiseResolved, true); - - changes = new Set(); - - // Does not trigger events for same update values - storage.set('bar', 'foo'); - storage.set('barNumber', 55); - storage.set('barBoolean', true); - strictEqual(changes.size, 0); - - // Simple deletes - const delete1Promise = storage.delete('bar'); - const delete2Promise = storage.delete('barNumber'); - const delete3Promise = storage.delete('barBoolean'); - - ok(!storage.get('bar')); - ok(!storage.getNumber('barNumber')); - ok(!storage.getBoolean('barBoolean')); - - strictEqual(changes.size, 3); - ok(changes.has('bar')); - ok(changes.has('barNumber')); - ok(changes.has('barBoolean')); - - changes = new Set(); - - // Does not trigger events for same delete values - storage.delete('bar'); - storage.delete('barNumber'); - storage.delete('barBoolean'); - strictEqual(changes.size, 0); - - let deletePromiseResolved = false; - await Promise.all([delete1Promise, delete2Promise, delete3Promise]).then(() => deletePromiseResolved = true); - strictEqual(deletePromiseResolved, true); - - await storage.close(); - await storage.close(); // it is ok to call this multiple times }); - test('external changes', async () => { + test('external changes', () => { + return runWithFakedTimers({}, async function () { + class TestSQLiteStorageDatabase extends SQLiteStorageDatabase { + private readonly _onDidChangeItemsExternal = new Emitter(); + override get onDidChangeItemsExternal(): Event { return this._onDidChangeItemsExternal.event; } - class TestSQLiteStorageDatabase extends SQLiteStorageDatabase { - private readonly _onDidChangeItemsExternal = new Emitter(); - override get onDidChangeItemsExternal(): Event { return this._onDidChangeItemsExternal.event; } - - fireDidChangeItemsExternal(event: IStorageItemsChangeEvent): void { - this._onDidChangeItemsExternal.fire(event); + fireDidChangeItemsExternal(event: IStorageItemsChangeEvent): void { + this._onDidChangeItemsExternal.fire(event); + } } - } - const database = new TestSQLiteStorageDatabase(join(testDir, 'storage.db')); - const storage = new Storage(database); + const database = new TestSQLiteStorageDatabase(join(testDir, 'storage.db')); + const storage = new Storage(database); - let changes = new Set(); - storage.onDidChangeStorage(key => { - changes.add(key); + let changes = new Set(); + storage.onDidChangeStorage(key => { + changes.add(key); + }); + + await storage.init(); + + await storage.set('foo', 'bar'); + ok(changes.has('foo')); + changes.clear(); + + // Nothing happens if changing to same value + const changed = new Map(); + changed.set('foo', 'bar'); + database.fireDidChangeItemsExternal({ changed }); + strictEqual(changes.size, 0); + + // Change is accepted if valid + changed.set('foo', 'bar1'); + database.fireDidChangeItemsExternal({ changed }); + ok(changes.has('foo')); + strictEqual(storage.get('foo'), 'bar1'); + changes.clear(); + + // Delete is accepted + const deleted = new Set(['foo']); + database.fireDidChangeItemsExternal({ deleted }); + ok(changes.has('foo')); + strictEqual(storage.get('foo', undefined), undefined); + changes.clear(); + + // Nothing happens if changing to same value + database.fireDidChangeItemsExternal({ deleted }); + strictEqual(changes.size, 0); + + strictEqual(isStorageItemsChangeEvent({ changed }), true); + strictEqual(isStorageItemsChangeEvent({ deleted }), true); + strictEqual(isStorageItemsChangeEvent({ changed, deleted }), true); + strictEqual(isStorageItemsChangeEvent(undefined), false); + strictEqual(isStorageItemsChangeEvent({ changed: 'yes', deleted: false }), false); + + await storage.close(); }); - - await storage.init(); - - await storage.set('foo', 'bar'); - ok(changes.has('foo')); - changes.clear(); - - // Nothing happens if changing to same value - const changed = new Map(); - changed.set('foo', 'bar'); - database.fireDidChangeItemsExternal({ changed }); - strictEqual(changes.size, 0); - - // Change is accepted if valid - changed.set('foo', 'bar1'); - database.fireDidChangeItemsExternal({ changed }); - ok(changes.has('foo')); - strictEqual(storage.get('foo'), 'bar1'); - changes.clear(); - - // Delete is accepted - const deleted = new Set(['foo']); - database.fireDidChangeItemsExternal({ deleted }); - ok(changes.has('foo')); - strictEqual(storage.get('foo', undefined), undefined); - changes.clear(); - - // Nothing happens if changing to same value - database.fireDidChangeItemsExternal({ deleted }); - strictEqual(changes.size, 0); - - strictEqual(isStorageItemsChangeEvent({ changed }), true); - strictEqual(isStorageItemsChangeEvent({ deleted }), true); - strictEqual(isStorageItemsChangeEvent({ changed, deleted }), true); - strictEqual(isStorageItemsChangeEvent(undefined), false); - strictEqual(isStorageItemsChangeEvent({ changed: 'yes', deleted: false }), false); - - await storage.close(); }); test('close flushes data', async () => { @@ -218,72 +222,76 @@ flakySuite('Storage Library', function () { await storage.close(); }); - test('conflicting updates', async () => { - let storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); - await storage.init(); + test('conflicting updates', () => { + return runWithFakedTimers({}, async function () { + let storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); + await storage.init(); - let changes = new Set(); - storage.onDidChangeStorage(key => { - changes.add(key); + let changes = new Set(); + storage.onDidChangeStorage(key => { + changes.add(key); + }); + + const set1Promise = storage.set('foo', 'bar1'); + const set2Promise = storage.set('foo', 'bar2'); + const set3Promise = storage.set('foo', 'bar3'); + + let flushPromiseResolved = false; + storage.whenFlushed().then(() => flushPromiseResolved = true); + + strictEqual(storage.get('foo'), 'bar3'); + strictEqual(changes.size, 1); + ok(changes.has('foo')); + + let setPromiseResolved = false; + await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true); + ok(setPromiseResolved); + ok(flushPromiseResolved); + + changes = new Set(); + + const set4Promise = storage.set('bar', 'foo'); + const delete1Promise = storage.delete('bar'); + + ok(!storage.get('bar')); + + strictEqual(changes.size, 1); + ok(changes.has('bar')); + + let setAndDeletePromiseResolved = false; + await Promise.all([set4Promise, delete1Promise]).then(() => setAndDeletePromiseResolved = true); + ok(setAndDeletePromiseResolved); + + await storage.close(); }); - - const set1Promise = storage.set('foo', 'bar1'); - const set2Promise = storage.set('foo', 'bar2'); - const set3Promise = storage.set('foo', 'bar3'); - - let flushPromiseResolved = false; - storage.whenFlushed().then(() => flushPromiseResolved = true); - - strictEqual(storage.get('foo'), 'bar3'); - strictEqual(changes.size, 1); - ok(changes.has('foo')); - - let setPromiseResolved = false; - await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true); - ok(setPromiseResolved); - ok(flushPromiseResolved); - - changes = new Set(); - - const set4Promise = storage.set('bar', 'foo'); - const delete1Promise = storage.delete('bar'); - - ok(!storage.get('bar')); - - strictEqual(changes.size, 1); - ok(changes.has('bar')); - - let setAndDeletePromiseResolved = false; - await Promise.all([set4Promise, delete1Promise]).then(() => setAndDeletePromiseResolved = true); - ok(setAndDeletePromiseResolved); - - await storage.close(); }); test('corrupt DB recovers', async () => { - const storageFile = join(testDir, 'storage.db'); + return runWithFakedTimers({}, async function () { + const storageFile = join(testDir, 'storage.db'); - let storage = new Storage(new SQLiteStorageDatabase(storageFile)); - await storage.init(); + let storage = new Storage(new SQLiteStorageDatabase(storageFile)); + await storage.init(); - await storage.set('bar', 'foo'); + await storage.set('bar', 'foo'); - await Promises.writeFile(storageFile, 'This is a broken DB'); + await Promises.writeFile(storageFile, 'This is a broken DB'); - await storage.set('foo', 'bar'); + await storage.set('foo', 'bar'); - strictEqual(storage.get('bar'), 'foo'); - strictEqual(storage.get('foo'), 'bar'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('foo'), 'bar'); - await storage.close(); + await storage.close(); - storage = new Storage(new SQLiteStorageDatabase(storageFile)); - await storage.init(); + storage = new Storage(new SQLiteStorageDatabase(storageFile)); + await storage.init(); - strictEqual(storage.get('bar'), 'foo'); - strictEqual(storage.get('foo'), 'bar'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('foo'), 'bar'); - await storage.close(); + await storage.close(); + }); }); }); @@ -657,27 +665,27 @@ flakySuite('SQLite Storage Library', function () { storage.set('foo', 'bar'); storage.set('some/foo/path', 'some/bar/path'); - await timeout(10); + await timeout(2); storage.set('foo1', 'bar'); storage.set('some/foo1/path', 'some/bar/path'); - await timeout(10); + await timeout(2); storage.set('foo2', 'bar'); storage.set('some/foo2/path', 'some/bar/path'); - await timeout(10); + await timeout(2); storage.delete('foo1'); storage.delete('some/foo1/path'); - await timeout(10); + await timeout(2); storage.delete('foo4'); storage.delete('some/foo4/path'); - await timeout(70); + await timeout(5); storage.set('foo3', 'bar'); await storage.set('some/foo3/path', 'some/bar/path'); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index fc0f1fc0034..633749fba96 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -1189,6 +1189,25 @@ suite('Map', () => { assert.strictEqual(map.get(windowsFile), 'true'); assert.strictEqual(map.get(uncFile), 'true'); }); + + test('ResourceMap - files (ignorecase, BUT preservecase)', function () { + const map = new ResourceMap(uri => extUriIgnorePathCase.getComparisonKey(uri)); + + const fileA = URI.parse('file://some/filea'); + const fileAUpper = URI.parse('file://SOME/FILEA'); + + map.set(fileA, 1); + assert.strictEqual(map.get(fileA), 1); + assert.strictEqual(map.get(fileAUpper), 1); + assert.deepStrictEqual(Array.from(map.keys()).map(String), [fileA].map(String)); + assert.deepStrictEqual(Array.from(map), [[fileA, 1]]); + + map.set(fileAUpper, 1); + assert.strictEqual(map.get(fileA), 1); + assert.strictEqual(map.get(fileAUpper), 1); + assert.deepStrictEqual(Array.from(map.keys()).map(String), [fileAUpper].map(String)); + assert.deepStrictEqual(Array.from(map), [[fileAUpper, 1]]); + }); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index ba7e600b97c..d5faed937bd 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -94,6 +94,7 @@ import { ipcSharedProcessTunnelChannelName, ISharedProcessTunnelService } from ' import { SharedProcessTunnelService } from 'vs/platform/remote/node/sharedProcessTunnelService'; import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService'; +import { IUserConfigurationFileService, UserConfigurationFileServiceId } from 'vs/platform/configuration/common/userConfigurationFileService'; class SharedProcessMain extends Disposable { @@ -218,6 +219,9 @@ class SharedProcessMain extends Disposable { storageService.initialize() ]); + // User Configuration File + services.set(IUserConfigurationFileService, ProxyChannel.toService(mainProcessService.getChannel(UserConfigurationFileServiceId))); + // Request services.set(IRequestService, new SyncDescriptor(RequestService)); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 30ad484337f..3f7aa27b5cd 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -572,8 +572,10 @@ export class CodeApplication extends Disposable { const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService); mainProcessElectronServer.registerChannel('localFilesystem', fileSystemProviderChannel); - // Configuration - mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService))); + // User Configuration File + const userConfigurationFileService = new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService); + mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(userConfigurationFileService)); + sharedProcessClient.then(client => client.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(userConfigurationFileService))); // Update const updateChannel = new UpdateChannel(accessor.get(IUpdateService)); diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index acb389724ac..ebc9516ff5a 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -5,7 +5,7 @@ import 'vs/css!./indentGuides'; import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; -import { editorActiveIndentGuides, editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; +import { editorActiveIndentGuides, editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorBracketPairGuideActiveBackground1, editorBracketPairGuideActiveBackground2, editorBracketPairGuideActiveBackground3, editorBracketPairGuideActiveBackground4, editorBracketPairGuideActiveBackground5, editorBracketPairGuideActiveBackground6, editorBracketPairGuideBackground1, editorBracketPairGuideBackground2, editorBracketPairGuideBackground3, editorBracketPairGuideBackground4, editorBracketPairGuideBackground5, editorBracketPairGuideBackground6, editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; import { RenderingContext } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; @@ -16,6 +16,7 @@ import { HorizontalGuidesState, IndentGuide } from 'vs/editor/common/model'; import { ArrayQueue } from 'vs/base/common/arrays'; import { BracketPairGuidesClassNames } from 'vs/editor/common/model/textModel'; import { Color } from 'vs/base/common/color'; +import { isDefined } from 'vs/base/common/types'; export class IndentGuidesOverlay extends DynamicViewOverlay { @@ -241,6 +242,13 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { } } +function transparentToUndefined(color: Color | undefined): Color | undefined { + if (color && color.isTransparent()) { + return undefined; + } + return color; +} + registerThemingParticipant((theme, collector) => { const editorIndentGuidesColor = theme.getColor(editorIndentGuides); if (editorIndentGuidesColor) { @@ -252,26 +260,48 @@ registerThemingParticipant((theme, collector) => { } const colors = [ - editorBracketHighlightingForeground1, - editorBracketHighlightingForeground2, - editorBracketHighlightingForeground3, - editorBracketHighlightingForeground4, - editorBracketHighlightingForeground5, - editorBracketHighlightingForeground6 + { bracketColor: editorBracketHighlightingForeground1, guideColor: editorBracketPairGuideBackground1, guideColorActive: editorBracketPairGuideActiveBackground1 }, + { bracketColor: editorBracketHighlightingForeground2, guideColor: editorBracketPairGuideBackground2, guideColorActive: editorBracketPairGuideActiveBackground2 }, + { bracketColor: editorBracketHighlightingForeground3, guideColor: editorBracketPairGuideBackground3, guideColorActive: editorBracketPairGuideActiveBackground3 }, + { bracketColor: editorBracketHighlightingForeground4, guideColor: editorBracketPairGuideBackground4, guideColorActive: editorBracketPairGuideActiveBackground4 }, + { bracketColor: editorBracketHighlightingForeground5, guideColor: editorBracketPairGuideBackground5, guideColorActive: editorBracketPairGuideActiveBackground5 }, + { bracketColor: editorBracketHighlightingForeground6, guideColor: editorBracketPairGuideBackground6, guideColorActive: editorBracketPairGuideActiveBackground6 } ]; const colorProvider = new BracketPairGuidesClassNames(); + let colorValues = colors - .map(c => theme.getColor(c)) - .filter((c): c is Color => !!c) - .filter(c => !c.isTransparent()); + .map(c => { + const bracketColor = theme.getColor(c.bracketColor); + const guideColor = theme.getColor(c.guideColor); + const guideColorActive = theme.getColor(c.guideColorActive); - for (let level = 0; level < 30; level++) { - const color = colorValues[level % colorValues.length]; - collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')}.vertical { opacity: 0.3; box-shadow: 1px 0 0 0 ${color} inset; }`); - collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')}.horizontal-top { opacity: 0.3; border-top: 1px solid ${color}; }`); - collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')}.horizontal-bottom { opacity: 0.3; border-bottom: 1px solid ${color}; }`); + const effectiveGuideColor = transparentToUndefined(transparentToUndefined(guideColor) ?? bracketColor?.transparent(0.3)); + const effectiveGuideColorActive = transparentToUndefined(transparentToUndefined(guideColorActive) ?? bracketColor); + + if (!effectiveGuideColor || !effectiveGuideColorActive) { + return undefined; + } + + return { + guideColor: effectiveGuideColor, + guideColorActive: effectiveGuideColorActive, + }; + }) + .filter(isDefined); + + if (colorValues.length > 0) { + for (let level = 0; level < 30; level++) { + const colors = colorValues[level % colorValues.length]; + collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')} { --guide-color: ${colors.guideColor}; --guide-color-active: ${colors.guideColorActive}; }`); + } + + collector.addRule(`.monaco-editor .vertical { box-shadow: 1px 0 0 0 var(--guide-color) inset; }`); + collector.addRule(`.monaco-editor .horizontal-top { border-top: 1px solid var(--guide-color); }`); + collector.addRule(`.monaco-editor .horizontal-bottom { border-bottom: 1px solid var(--guide-color); }`); + + collector.addRule(`.monaco-editor .vertical.${colorProvider.activeClassName} { box-shadow: 1px 0 0 0 var(--guide-color-active) inset; }`); + collector.addRule(`.monaco-editor .horizontal-top.${colorProvider.activeClassName} { border-top: 1px solid var(--guide-color-active); }`); + collector.addRule(`.monaco-editor .horizontal-bottom.${colorProvider.activeClassName} { border-bottom: 1px solid var(--guide-color-active); }`); } - - collector.addRule(`.monaco-editor .${colorProvider.activeClassName} { opacity: 1 !important; }`); }); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 5e3549179d8..51b7208cdb8 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -3098,9 +3098,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati endLineNumber, this.getLineMaxColumn(endLineNumber) ) - ) - // Exclude bracket pairs that are not balanced. - .filter(b => b.closingBracketRange !== undefined); + ); let activeBracketPairRange: Range | undefined = undefined; if (activePosition && bracketPairs.length > 0) { @@ -3136,7 +3134,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati visibleEndColumn: number, bracketPair: BracketPairInfo, renderHorizontalEndLineAtTheBottom: boolean - }>(); + } | null>(); const nextGuides = new Array(); const colorProvider = new BracketPairGuidesClassNames(); @@ -3169,19 +3167,30 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati // TODO: Consider indentation when computing guideVisibleColumn const start = pair.openingBracketRange.getStartPosition(); const end = (pair.closingBracketRange?.getStartPosition() ?? pair.range.getEndPosition()); - activeGuides[pair.nestingLevel] = { - nestingLevel: pair.nestingLevel, - guideVisibleColumn, - start, - visibleStartColumn: this.getVisibleColumnFromPosition(start), - end, - visibleEndColumn: this.getVisibleColumnFromPosition(end), - bracketPair: pair, - renderHorizontalEndLineAtTheBottom - }; + + + if (pair.closingBracketRange === undefined) { + // Don't show guides for bracket pairs that are not balanced. + // See #135125. + activeGuides[pair.nestingLevel] = null; + } else { + activeGuides[pair.nestingLevel] = { + nestingLevel: pair.nestingLevel, + guideVisibleColumn, + start, + visibleStartColumn: this.getVisibleColumnFromPosition(start), + end, + visibleEndColumn: this.getVisibleColumnFromPosition(end), + bracketPair: pair, + renderHorizontalEndLineAtTheBottom + }; + } } for (const line of activeGuides) { + if (!line) { + continue; + } const isActive = activeBracketPairRange && line.bracketPair.range.equalsRange(activeBracketPairRange); const className = @@ -3214,7 +3223,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati // Going backwards, so the last guide potentially replaces others for (let i = activeGuides.length - 1; i >= 0; i--) { const line = activeGuides[i]; - + if (!line) { + continue; + } const isActive = options.highlightActive && activeBracketPairRange && line.bracketPair.range.equalsRange(activeBracketPairRange); @@ -3233,7 +3244,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati continue; } - if (line.guideVisibleColumn > lastVisibleColumnCount) { + if (line.guideVisibleColumn >= lastVisibleColumnCount && !isActive) { + // Don't render a guide on top of an existing guide, unless it is active. continue; } lastVisibleColumnCount = line.guideVisibleColumn; diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index 808a422a1e3..d0950a54331 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -61,6 +61,21 @@ export const editorBracketHighlightingForeground6 = registerColor('editorBracket export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); +export const editorBracketPairGuideBackground1 = registerColor('editorBracketPairGuide.background1', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background1', 'Background color of inactive bracket pair guides (1). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground2 = registerColor('editorBracketPairGuide.background2', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background2', 'Background color of inactive bracket pair guides (2). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground3 = registerColor('editorBracketPairGuide.background3', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background3', 'Background color of inactive bracket pair guides (3). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground4 = registerColor('editorBracketPairGuide.background4', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background4', 'Background color of inactive bracket pair guides (4). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground5 = registerColor('editorBracketPairGuide.background5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background5', 'Background color of inactive bracket pair guides (5). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground6 = registerColor('editorBracketPairGuide.background6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background6', 'Background color of inactive bracket pair guides (6). Requires enabling bracket pair guides.')); + +export const editorBracketPairGuideActiveBackground1 = registerColor('editorBracketPairGuide.activeBackground1', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground1', 'Background color of active bracket pair guides (1). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground2 = registerColor('editorBracketPairGuide.activeBackground2', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground2', 'Background color of active bracket pair guides (2). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground3 = registerColor('editorBracketPairGuide.activeBackground3', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground3', 'Background color of active bracket pair guides (3). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground4 = registerColor('editorBracketPairGuide.activeBackground4', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground4', 'Background color of active bracket pair guides (4). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground5 = registerColor('editorBracketPairGuide.activeBackground5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground5', 'Background color of active bracket pair guides (5). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground6 = registerColor('editorBracketPairGuide.activeBackground6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground6', 'Background color of active bracket pair guides (6). Requires enabling bracket pair guides.')); + + // contains all color rules that used to defined in editor/browser/widget/editor.css registerThemingParticipant((theme, collector) => { const background = theme.getColor(editorBackground); diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index f1c2e6ff2c2..938a5c1e5d9 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -44,6 +44,11 @@ class CodeLensViewZone implements IViewZone { this._onHeight(); } } + + isVisible(): boolean { + return this._lastHeight !== 0 + && this.domNode.hasAttribute('monaco-visible-view-zone'); + } } class CodeLensContentWidget implements IContentWidget { @@ -289,7 +294,7 @@ export class CodeLensWidget { } computeIfNecessary(model: ITextModel): CodeLensItem[] | null { - if (!this._viewZone.domNode.hasAttribute('monaco-visible-view-zone')) { + if (!this._viewZone.isVisible()) { return null; } diff --git a/src/vs/editor/contrib/inlineCompletions/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/suggestWidgetInlineCompletionProvider.ts index 9114ec0a89b..b55501d6f62 100644 --- a/src/vs/editor/contrib/inlineCompletions/suggestWidgetInlineCompletionProvider.ts +++ b/src/vs/editor/contrib/inlineCompletions/suggestWidgetInlineCompletionProvider.ts @@ -94,7 +94,7 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable { return undefined; } const valid = - normalizedSuggestItem.range.equalsRange(normalizedItemToPreselect.range) && + rangeStartsWith(normalizedItemToPreselect.range, normalizedSuggestItem.range) && normalizedItemToPreselect.text.startsWith(normalizedSuggestItem.text); return { index, valid, prefixLength: normalizedSuggestItem.text.length, suggestItem }; }) @@ -190,6 +190,16 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable { } } +function rangeStartsWith(rangeToTest: Range, prefix: Range): boolean { + return ( + rangeToTest.startLineNumber === prefix.startLineNumber && + rangeToTest.startColumn === prefix.startColumn && + (rangeToTest.endLineNumber < prefix.endLineNumber || + (rangeToTest.endLineNumber === prefix.endLineNumber && + rangeToTest.endColumn <= prefix.endColumn)) + ); +} + function suggestionToInlineCompletion(suggestController: SuggestController, position: Position, item: CompletionItem, toggleMode: boolean): NormalizedInlineCompletion | undefined { // additionalTextEdits might not be resolved here, this could be problematic. if (Array.isArray(item.completion.additionalTextEdits) && item.completion.additionalTextEdits.length > 0) { diff --git a/src/vs/platform/configuration/common/userConfigurationFileService.ts b/src/vs/platform/configuration/common/userConfigurationFileService.ts index c826c7fbd87..2507cb3e1f4 100644 --- a/src/vs/platform/configuration/common/userConfigurationFileService.ts +++ b/src/vs/platform/configuration/common/userConfigurationFileService.ts @@ -8,9 +8,8 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { JSONPath, parse, ParseError } from 'vs/base/common/json'; import { setProperty } from 'vs/base/common/jsonEdit'; import { Edit, FormattingOptions } from 'vs/base/common/jsonFormatter'; -import { URI } from 'vs/base/common/uri'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IFileService, IWriteFileOptions } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -31,6 +30,7 @@ export interface IUserConfigurationFileService { readonly _serviceBrand: undefined; updateSettings(value: IJSONValue, formattingOptions: FormattingOptions): Promise; + write(value: VSBuffer, options?: IWriteFileOptions): Promise; } export class UserConfigurationFileService implements IUserConfigurationFileService { @@ -48,12 +48,12 @@ export class UserConfigurationFileService implements IUserConfigurationFileServi } async updateSettings(value: IJSONValue, formattingOptions: FormattingOptions): Promise { - return this.queue.queue(() => this.doWrite(this.environmentService.settingsResource, value, formattingOptions)); // queue up writes to prevent race conditions + return this.queue.queue(() => this.doWrite(value, formattingOptions)); // queue up writes to prevent race conditions } - private async doWrite(resource: URI, jsonValue: IJSONValue, formattingOptions: FormattingOptions): Promise { - this.logService.trace(`${UserConfigurationFileServiceId}#write`, resource.toString(), jsonValue); - const { value, mtime, etag } = await this.fileService.readFile(resource, { atomic: true }); + private async doWrite(jsonValue: IJSONValue, formattingOptions: FormattingOptions): Promise { + this.logService.trace(`${UserConfigurationFileServiceId}#write`, this.environmentService.settingsResource.toString(), jsonValue); + const { value, mtime, etag } = await this.fileService.readFile(this.environmentService.settingsResource, { atomic: true }); let content = value.toString(); const parseErrors: ParseError[] = []; @@ -66,7 +66,7 @@ export class UserConfigurationFileService implements IUserConfigurationFileServi if (edit) { content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length); try { - await this.fileService.writeFile(resource, VSBuffer.fromString(content), { etag, mtime }); + await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(content), { etag, mtime }); } catch (error) { if ((error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { throw new Error(UserConfigurationErrorCode.ERROR_FILE_MODIFIED_SINCE); @@ -75,6 +75,13 @@ export class UserConfigurationFileService implements IUserConfigurationFileServi } } + async write(content: VSBuffer, options?: IWriteFileOptions): Promise { + // queue up writes to prevent race conditions + return this.queue.queue(async () => { + await this.fileService.writeFile(this.environmentService.settingsResource, content, options); + }); + } + private getEdits({ value, path }: IJSONValue, modelContent: string, formattingOptions: FormattingOptions): Edit[] { if (path.length) { return setProperty(modelContent, path, value, formattingOptions); diff --git a/src/vs/platform/storage/test/browser/storageService.test.ts b/src/vs/platform/storage/test/browser/storageService.test.ts index 3f08fdc934a..2b1ed5b22e6 100644 --- a/src/vs/platform/storage/test/browser/storageService.test.ts +++ b/src/vs/platform/storage/test/browser/storageService.test.ts @@ -8,6 +8,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { Storage } from 'vs/base/parts/storage/common/storage'; import { flakySuite } from 'vs/base/test/common/testUtils'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { FileService } from 'vs/platform/files/common/fileService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -66,20 +67,22 @@ flakySuite('StorageService (browser specific)', () => { disposables.clear(); }); - test('clear', async () => { - storageService.store('bar', 'foo', StorageScope.GLOBAL, StorageTarget.MACHINE); - storageService.store('bar', 3, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); - storageService.store('bar', 3, StorageScope.WORKSPACE, StorageTarget.USER); + test('clear', () => { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + storageService.store('bar', 'foo', StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.WORKSPACE, StorageTarget.USER); - await storageService.clear(); + await storageService.clear(); - for (const scope of [StorageScope.GLOBAL, StorageScope.WORKSPACE]) { - for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { - strictEqual(storageService.get('bar', scope), undefined); - strictEqual(storageService.keys(scope, target).length, 0); + for (const scope of [StorageScope.GLOBAL, StorageScope.WORKSPACE]) { + for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { + strictEqual(storageService.get('bar', scope), undefined); + strictEqual(storageService.keys(scope, target).length, 0); + } } - } + }); }); }); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index f0dc7fdef4a..6b15b6c5549 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -760,7 +760,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { try { if (oldContent) { // file exists already - await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent); + await this.writeFileContent(newContent, oldContent, force); } else { // file does not exist await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force }); @@ -775,6 +775,10 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { } } + protected async writeFileContent(newContent: string, oldContent: IFileContent, force: boolean): Promise { + await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent); + } + private onFileChanges(e: FileChangesEvent): void { if (!e.contains(this.file)) { return; diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 31da84131e4..bb7505765f2 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -12,9 +12,10 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; +import { IUserConfigurationFileService } from 'vs/platform/configuration/common/userConfigurationFileService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; @@ -62,6 +63,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IUserConfigurationFileService private readonly userConfigurationFileService: IUserConfigurationFileService, ) { super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } @@ -325,6 +327,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content); } + protected override async writeFileContent(newContent: string, oldContent: IFileContent, force: boolean): Promise { + await this.userConfigurationFileService.write(VSBuffer.fromString(newContent), force ? undefined : { etag: oldContent.etag, mtime: oldContent.mtime }); + } + private validateContent(content: string): void { if (this.hasErrors(content)) { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 99fb6c02c0c..cf3e6333d88 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -16,6 +16,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IUserConfigurationFileService, UserConfigurationFileService } from 'vs/platform/configuration/common/userConfigurationFileService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { DidUninstallExtensionEvent, IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -88,6 +89,7 @@ export class UserDataSyncClient extends Disposable { const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); + this.instantiationService.stub(IUserConfigurationFileService, this.instantiationService.createInstance(UserConfigurationFileService)); this.instantiationService.stub(IRequestService, this.testServer); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index edd273928b2..77abc5089bd 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -12734,8 +12734,9 @@ declare module 'vscode' { * The UI-visible count of {@link SourceControlResourceState resource states} of * this source control. * - * Equals to the total number of {@link SourceControlResourceState resource state} - * of this source control, if undefined. + * If undefined, this source control will + * - display its UI-visible count as zero, and + * - contribute the count of its {@link SourceControlResourceState resource states} to the UI-visible aggregated count for all source controls */ count?: number; diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index 00038bd692d..bacfe4bdd80 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -16,13 +16,12 @@ import { ResourceMap } from 'vs/base/common/map'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { IExtUri } from 'vs/base/common/resources'; -import { SkipList } from 'vs/base/common/skipList'; export class DiagnosticCollection implements vscode.DiagnosticCollection { readonly #proxy: MainThreadDiagnosticsShape | undefined; readonly #onDidChangeDiagnostics: Emitter; - readonly #data: SkipList; + readonly #data: ResourceMap; private _isDisposed = false; @@ -34,7 +33,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { proxy: MainThreadDiagnosticsShape | undefined, onDidChangeDiagnostics: Emitter ) { - this.#data = new SkipList((a, b) => extUri.compare(a, b)); + this.#data = new ResourceMap(uri => extUri.getComparisonKey(uri)); this.#proxy = proxy; this.#onDidChangeDiagnostics = onDidChangeDiagnostics; } diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 2890618f4d2..a6b1368a61d 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -33,7 +33,7 @@ export const OpenFolderWorkspaceSupportContext = new RawContextKey('ope export const EnterMultiRootWorkspaceSupportContext = new RawContextKey('enterMultiRootWorkspaceSupport', true, true); export const EmptyWorkspaceSupportContext = new RawContextKey('emptyWorkspaceSupport', true, true); -export const DirtyWorkingCopiesContext = new RawContextKey('dirtyWorkingCopies', false, localize('dirtyWorkingCopies', "Whether there are any dirty working copies")); +export const DirtyWorkingCopiesContext = new RawContextKey('dirtyWorkingCopies', false, localize('dirtyWorkingCopies', "Whether there are any working copies with unsaved changes")); export const RemoteNameContext = new RawContextKey('remoteName', '', localize('remoteName', "The name of the remote the window is connected to or an empty string if not connected to any remote")); export const VirtualWorkspaceContext = new RawContextKey('virtualWorkspace', '', localize('virtualWorkspace', "The scheme of the current workspace if is from a virtual file system or an empty string.")); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index a5c9bbe69d4..fcfea9b352d 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -62,7 +62,7 @@ class OutlineItem extends BreadcrumbsItem { if (!(other instanceof OutlineItem)) { return false; } - return this.element === other.element && + return this.element.element === other.element.element && this.options.showFileIcons === other.options.showFileIcons && this.options.showSymbolIcons === other.options.showSymbolIcons; } diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index b421e11c532..896407ca8bd 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -149,11 +149,11 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro ariaLabel: (() => { if (mapGroupIdToGroupAriaLabel.size > 1) { return isDirty ? - localize('entryAriaLabelWithGroupDirty', "{0}, dirty, {1}", nameAndDescription, mapGroupIdToGroupAriaLabel.get(groupId)) : + localize('entryAriaLabelWithGroupDirty', "{0}, unsaved changes, {1}", nameAndDescription, mapGroupIdToGroupAriaLabel.get(groupId)) : localize('entryAriaLabelWithGroup', "{0}, {1}", nameAndDescription, mapGroupIdToGroupAriaLabel.get(groupId)); } - return isDirty ? localize('entryAriaLabelDirty', "{0}, dirty", nameAndDescription) : nameAndDescription; + return isDirty ? localize('entryAriaLabelDirty', "{0}, unsaved changes", nameAndDescription) : nameAndDescription; })(), description, iconClasses: getIconClasses(this.modelService, this.modeService, resource).concat(editor.getLabelExtraClasses()), diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index aeb883be2e1..e782029440e 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -457,9 +457,10 @@ export class ViewsService extends Disposable implements IViewsService { private registerFocusViewAction(viewDescriptor: IViewDescriptor, category?: string | ILocalizedString): IDisposable { return registerAction2(class FocusViewAction extends Action2 { constructor() { + const title = localize({ key: 'focus view', comment: ['{0} indicates the name of the view to be focused.'] }, "Focus on {0} View", viewDescriptor.name); super({ id: viewDescriptor.focusCommand ? viewDescriptor.focusCommand.id : `${viewDescriptor.id}.focus`, - title: { original: `Focus on ${viewDescriptor.name} View`, value: localize({ key: 'focus view', comment: ['{0} indicates the name of the view to be focused.'] }, "Focus on {0} View", viewDescriptor.name) }, + title: { original: `Focus on ${viewDescriptor.name} View`, value: title }, category, menu: [{ id: MenuId.CommandPalette, @@ -473,11 +474,29 @@ export class ViewsService extends Disposable implements IViewsService { linux: viewDescriptor.focusCommand?.keybindings?.linux, mac: viewDescriptor.focusCommand?.keybindings?.mac, win: viewDescriptor.focusCommand?.keybindings?.win + }, + description: { + description: title, + args: [ + { + name: 'focusOptions', + description: 'Focus Options', + schema: { + type: 'object', + properties: { + 'preserveFocus': { + type: 'boolean', + default: false + } + }, + } + } + ] } }); } - run(accessor: ServicesAccessor): void { - accessor.get(IViewsService).openView(viewDescriptor.id, true); + run(accessor: ServicesAccessor, options?: { preserveFocus?: boolean }): void { + accessor.get(IViewsService).openView(viewDescriptor.id, !options?.preserveFocus); } }); } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index c592370424c..b8a8c87d94e 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -47,7 +47,7 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.editor.highlightModifiedTabs': { 'type': 'boolean', - 'markdownDescription': localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is disabled."), + 'markdownDescription': localize('highlightModifiedTabs', "Controls whether a top border is drawn on tabs for editors that have unsaved changes. This value is ignored when `#workbench.editor.showTabs#` is disabled."), 'default': false }, 'workbench.editor.decorations.badges': { @@ -169,7 +169,7 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.editor.closeOnFileDelete': { 'type': 'boolean', - 'description': localize('closeOnFileDelete', "Controls whether editors showing a file that was opened during the session should close automatically when getting deleted or renamed by some other process. Disabling this will keep the editor open on such an event. Note that deleting from within the application will always close the editor and that dirty files will never close to preserve your data."), + 'description': localize('closeOnFileDelete', "Controls whether editors showing a file that was opened during the session should close automatically when getting deleted or renamed by some other process. Disabling this will keep the editor open on such an event. Note that deleting from within the application will always close the editor and that editors with unsaved changes will never close to preserve your data."), 'default': false }, 'workbench.editor.openPositioning': { @@ -228,7 +228,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'workbench.editor.limit.enabled': { 'type': 'boolean', 'default': false, - 'description': localize('limitEditorsEnablement', "Controls if the number of opened editors should be limited or not. When enabled, less recently used editors that are not dirty will close to make space for newly opening editors.") + 'description': localize('limitEditorsEnablement', "Controls if the number of opened editors should be limited or not. When enabled, less recently used editors will close to make space for newly opening editors.") }, 'workbench.editor.limit.value': { 'type': 'number', @@ -381,7 +381,7 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('rootPath', "`${rootPath}`: file path of the opened workspace or folder (e.g. /Users/Development/myWorkspace)."), localize('appName', "`${appName}`: e.g. VS Code."), localize('remoteName', "`${remoteName}`: e.g. SSH"), - localize('dirty', "`${dirty}`: a dirty indicator if the active editor is dirty."), + localize('dirty', "`${dirty}`: an indicator for when the active editor has unsaved changes."), localize('separator', "`${separator}`: a conditional separator (\" - \") that only shows when surrounded by variables with values or static text.") ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index a12258f9297..d363466b598 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -38,7 +38,7 @@ export const DEFAULT_EDITOR_ASSOCIATION = { }; // Editor State Context Keys -export const ActiveEditorDirtyContext = new RawContextKey('activeEditorIsDirty', false, localize('activeEditorIsDirty', "Whether the active editor is dirty")); +export const ActiveEditorDirtyContext = new RawContextKey('activeEditorIsDirty', false, localize('activeEditorIsDirty', "Whether the active editor has unsaved changes")); export const ActiveEditorPinnedContext = new RawContextKey('activeEditorIsNotPreview', false, localize('activeEditorIsNotPreview', "Whether the active editor is not in preview mode")); export const ActiveEditorStickyContext = new RawContextKey('activeEditorIsPinned', false, localize('activeEditorIsPinned', "Whether the active editor is pinned")); export const ActiveEditorReadonlyContext = new RawContextKey('activeEditorIsReadonly', false, localize('activeEditorIsReadonly', "Whether the active editor is readonly")); diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index c0ff64417db..0032fb45349 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -165,25 +165,25 @@ export const TAB_ACTIVE_MODIFIED_BORDER = registerColor('tab.activeModifiedBorde dark: '#3399CC', light: '#33AAEE', hc: null -}, localize('tabActiveModifiedBorder', "Border on the top of modified (dirty) active tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +}, localize('tabActiveModifiedBorder', "Border on the top of modified active tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_INACTIVE_MODIFIED_BORDER = registerColor('tab.inactiveModifiedBorder', { dark: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.5), light: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.5), hc: Color.white -}, localize('tabInactiveModifiedBorder', "Border on the top of modified (dirty) inactive tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +}, localize('tabInactiveModifiedBorder', "Border on the top of modified inactive tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER = registerColor('tab.unfocusedActiveModifiedBorder', { dark: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.5), light: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.7), hc: Color.white -}, localize('unfocusedActiveModifiedBorder', "Border on the top of modified (dirty) active tabs in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +}, localize('unfocusedActiveModifiedBorder', "Border on the top of modified active tabs in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER = registerColor('tab.unfocusedInactiveModifiedBorder', { dark: transparent(TAB_INACTIVE_MODIFIED_BORDER, 0.5), light: transparent(TAB_INACTIVE_MODIFIED_BORDER, 0.5), hc: Color.white -}, localize('unfocusedINactiveModifiedBorder', "Border on the top of modified (dirty) inactive tabs in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +}, localize('unfocusedINactiveModifiedBorder', "Border on the top of modified inactive tabs in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 2d393097c0f..39a6a25f2c0 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -1107,11 +1107,18 @@ export class ExtensionEditor extends EditorPane { $('th', undefined, localize('description', "Description")), $('th', undefined, localize('default', "Default")) ), - ...contrib.map(key => $('tr', undefined, - $('td', undefined, $('code', undefined, key)), - $('td', undefined, properties[key].description || (properties[key].markdownDescription && renderMarkdown({ value: properties[key].markdownDescription }, { actionHandler: { callback: (content) => this.openerService.open(content).catch(onUnexpectedError), disposables: this.contentDisposables } }))), - $('td', undefined, $('code', undefined, `${isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : properties[key].default}`)) - )) + ...contrib.map(key => { + let description: (Node | string) = properties[key].description; + if (properties[key].markdownDescription) { + const { element, dispose } = renderMarkdown({ value: properties[key].markdownDescription }, { actionHandler: { callback: (content) => this.openerService.open(content).catch(onUnexpectedError), disposables: this.contentDisposables } }); + description = element; + this.contentDisposables.add(toDisposable(dispose)); + } + return $('tr', undefined, + $('td', undefined, $('code', undefined, key)), + $('td', undefined, description), + $('td', undefined, $('code', undefined, `${isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : properties[key].default}`))); + }) ) ); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 07e83ac0ddc..8c3bef3516c 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -113,7 +113,7 @@ const hotExitConfiguration: IConfigurationPropertySchema = isNative ? 'enum': [HotExitConfiguration.OFF, HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE], 'default': HotExitConfiguration.ON_EXIT, 'markdownEnumDescriptions': [ - nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with dirty files.'), + nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with editors that have unsaved changes.'), nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`'), nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`') ], @@ -124,7 +124,7 @@ const hotExitConfiguration: IConfigurationPropertySchema = isNative ? 'enum': [HotExitConfiguration.OFF, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE], 'default': HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, 'markdownEnumDescriptions': [ - nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with dirty files.'), + nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with editors that have unsaved changes.'), nls.localize('hotExit.onExitAndWindowCloseBrowser', 'Hot exit will be triggered when the browser quits or the window or tab is closed.') ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) @@ -229,18 +229,18 @@ configurationRegistry.registerConfiguration({ 'type': 'string', 'enum': [AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE], 'markdownEnumDescriptions': [ - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.off' }, "A dirty editor is never automatically saved."), - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.afterDelay' }, "A dirty editor is automatically saved after the configured `#files.autoSaveDelay#`."), - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "A dirty editor is automatically saved when the editor loses focus."), - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty editor is automatically saved when the window loses focus.") + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.off' }, "An editor with changes is never automatically saved."), + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.afterDelay' }, "An editor with changes is automatically saved after the configured `#files.autoSaveDelay#`."), + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "An editor with changes is automatically saved when the editor loses focus."), + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "An editor with changes is automatically saved when the window loses focus.") ], 'default': isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty editors. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of editors that have unsaved changes. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) }, 'files.autoSaveDelay': { 'type': 'number', 'default': 1000, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty editor is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY) + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in milliseconds after which an editor with unsaved changes is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY) }, 'files.watcherExclude': { 'type': 'object', diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index a0634775d9d..d7dd132c96b 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -292,7 +292,8 @@ class DirtyDiffWidget extends PeekViewWidget { fixedOverflowWidgets: true, minimap: { enabled: false }, renderSideBySide: false, - readOnly: false + readOnly: false, + renderIndicators: false }; this.diffEditor = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, container, options, this.editor); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 83031ac57b8..b242e3d36ac 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -894,7 +894,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 848903b8a3f..fe945b86aad 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -312,7 +312,7 @@ const terminalConfiguration: IConfigurationNode = { default: 'never' }, [TerminalSettingId.ConfirmOnKill]: { - description: localize('terminal.integrated.confirmOnKill', "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as dirty when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell."), + description: localize('terminal.integrated.confirmOnKill', "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as changed when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell."), type: 'string', enum: ['never', 'editor', 'panel', 'always'], enumDescriptions: [ diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 95624815e8b..a7962686b93 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -214,7 +214,7 @@ abstract class BaseSwitchWindow extends Action2 { return { payload: window.id, label: window.title, - ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, dirty window", window.title) : window.title, + ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, window with unsaved changes", window.title) : window.title, iconClasses: getIconClasses(modelService, modeService, resource, fileKind), description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined, buttons: currentWindowId !== window.id ? window.dirty ? [this.closeDirtyWindowAction] : [this.closeWindowAction] : undefined diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 3c26c0f94ac..ff8f5e35abb 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -387,18 +387,18 @@ export class ConfigurationEditingService { } case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: { if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { - return nls.localize('errorTasksConfigurationFileDirty', "Unable to write into tasks configuration file because the file is dirty. Please save it first and then try again."); + return nls.localize('errorTasksConfigurationFileDirty', "Unable to write into tasks configuration file because the file has unsaved changes. Please save it first and then try again."); } if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) { - return nls.localize('errorLaunchConfigurationFileDirty', "Unable to write into launch configuration file because the file is dirty. Please save it first and then try again."); + return nls.localize('errorLaunchConfigurationFileDirty', "Unable to write into launch configuration file because the file has unsaved changes. Please save it first and then try again."); } switch (target) { case EditableConfigurationTarget.USER_LOCAL: - return nls.localize('errorConfigurationFileDirty', "Unable to write into user settings because the file is dirty. Please save the user settings file first and then try again."); + return nls.localize('errorConfigurationFileDirty', "Unable to write into user settings because the file has unsaved changes. Please save the user settings file first and then try again."); case EditableConfigurationTarget.USER_REMOTE: - return nls.localize('errorRemoteConfigurationFileDirty', "Unable to write into remote user settings because the file is dirty. Please save the remote user settings file first and then try again."); + return nls.localize('errorRemoteConfigurationFileDirty', "Unable to write into remote user settings because the file has unsaved changes. Please save the remote user settings file first and then try again."); case EditableConfigurationTarget.WORKSPACE: - return nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write into workspace settings because the file is dirty. Please save the workspace settings file first and then try again."); + return nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write into workspace settings because the file has unsaved changes. Please save the workspace settings file first and then try again."); case EditableConfigurationTarget.WORKSPACE_FOLDER: let workspaceFolderName: string = '<>'; if (operation.resource) { @@ -407,7 +407,7 @@ export class ConfigurationEditingService { workspaceFolderName = folder.name; } } - return nls.localize('errorConfigurationFileDirtyFolder', "Unable to write into folder settings because the file is dirty. Please save the '{0}' folder settings file first and then try again.", workspaceFolderName); + return nls.localize('errorConfigurationFileDirtyFolder', "Unable to write into folder settings because the file has unsaved changes. Please save the '{0}' folder settings file first and then try again.", workspaceFolderName); default: return ''; } diff --git a/src/vs/workbench/services/configuration/common/jsonEditingService.ts b/src/vs/workbench/services/configuration/common/jsonEditingService.ts index a9686fae2d9..cd00727f8d1 100644 --- a/src/vs/workbench/services/configuration/common/jsonEditingService.ts +++ b/src/vs/workbench/services/configuration/common/jsonEditingService.ts @@ -134,7 +134,7 @@ export class JSONEditingService implements IJSONEditingService { return nls.localize('errorInvalidFile', "Unable to write into the file. Please open the file to correct errors/warnings in the file and try again."); } case JSONEditingErrorCode.ERROR_FILE_DIRTY: { - return nls.localize('errorFileDirty', "Unable to write into the file because the file is dirty. Please save the file and try again."); + return nls.localize('errorFileDirty', "Unable to write into the file because the file has unsaved changes. Please save the file and try again."); } } } diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index a44cc8bf3dc..6dd20722202 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -261,7 +261,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { // Extension is disabled. Enable the extension and reload the window to handle. else if (this.extensionEnablementService.canChangeEnablement(extension)) { - this.telemetryService.publicLog2('uri_invoked/enable_extension/accept', { extensionId: extensionIdentifier.id }); + this.telemetryService.publicLog2('uri_invoked/enable_extension/start', { extensionId: extensionIdentifier.id }); const result = await this.dialogService.confirm({ message: localize('enableAndHandle', "Extension '{0}' is disabled. Would you like to enable the extension and reload the window to open the URL?", extension.manifest.displayName || extension.manifest.name), detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`, diff --git a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts index 3d25314fdcd..9935d106589 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts @@ -256,7 +256,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding // Target cannot be dirty if not writing into buffer if (this.textFileService.isDirty(this.resource)) { - return Promise.reject(new Error(localize('errorKeybindingsFileDirty', "Unable to write because the keybindings configuration file is dirty. Please save it first and then try again."))); + return Promise.reject(new Error(localize('errorKeybindingsFileDirty', "Unable to write because the keybindings configuration file has unsaved changes. Please save it first and then try again."))); } return this.resolveModelReference() diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index cd0d99dc3ba..196876fd1fe 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -152,7 +152,7 @@ suite('KeybindingsEditing', () => { instantiationService.stub(ITextFileService, 'isDirty', true); return testObject.editKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }), 'alt+c', undefined) .then(() => assert.fail('Should fail with dirty error'), - error => assert.strictEqual(error.message, 'Unable to write because the keybindings configuration file is dirty. Please save it first and then try again.')); + error => assert.strictEqual(error.message, 'Unable to write because the keybindings configuration file has unsaved changes. Please save it first and then try again.')); }); test('errors cases - did not find an array', async () => { diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts index 2687aaf4434..fb7cd5adc95 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts @@ -115,7 +115,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp return false; // do not block shutdown during extension development (https://github.com/microsoft/vscode/issues/115028) } - this.showErrorDialog(localize('backupTrackerBackupFailed', "The following dirty editors could not be saved to the back up location."), remainingDirtyWorkingCopies, backupError); + this.showErrorDialog(localize('backupTrackerBackupFailed', "The following editors with unsaved changes could not be saved to the back up location."), remainingDirtyWorkingCopies, backupError); return true; // veto (the backup failed) } @@ -131,7 +131,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp return false; // do not block shutdown during extension development (https://github.com/microsoft/vscode/issues/115028) } - this.showErrorDialog(localize('backupTrackerConfirmFailed', "The following dirty editors could not be saved or reverted."), remainingDirtyWorkingCopies, error); + this.showErrorDialog(localize('backupTrackerConfirmFailed', "The following editors with unsaved changes could not be saved or reverted."), remainingDirtyWorkingCopies, error); return true; // veto (save or revert failed) } @@ -140,7 +140,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp private showErrorDialog(msg: string, workingCopies: readonly IWorkingCopy[], error?: Error): void { const dirtyWorkingCopies = workingCopies.filter(workingCopy => workingCopy.isDirty()); - const advice = localize('backupErrorDetails', "Try saving or reverting the dirty editors first and then try again."); + const advice = localize('backupErrorDetails', "Try saving or reverting the editors with unsaved changes first and then try again."); const detail = dirtyWorkingCopies.length ? getFileNamesMessage(dirtyWorkingCopies.map(x => x.name)) + '\n' + advice : advice; @@ -225,8 +225,8 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp error = backupError; } }, - localize('backupBeforeShutdownMessage', "Backing up dirty editors is taking longer than expected..."), - localize('backupBeforeShutdownDetail', "Click 'Cancel' to stop waiting and to save or revert dirty editors.") + localize('backupBeforeShutdownMessage', "Backing up editors with unsaved changes is taking longer than expected..."), + localize('backupBeforeShutdownDetail', "Click 'Cancel' to stop waiting and to save or revert editors with unsaved changes.") ); return { backups, error }; @@ -296,7 +296,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp if (result !== false) { await Promises.settled(dirtyWorkingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : Promise.resolve(true))); } - }, localize('saveBeforeShutdown', "Saving dirty editors is taking longer than expected...")); + }, localize('saveBeforeShutdown', "Saving editors with unsaved changes is taking longer than expected...")); } private doRevertAllBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[]): Promise { @@ -313,7 +313,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp // If we still have dirty working copies, revert those directly // unless the revert operation was not successful (e.g. cancelled) await Promises.settled(dirtyWorkingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : Promise.resolve())); - }, localize('revertBeforeShutdown', "Reverting dirty editors is taking longer than expected...")); + }, localize('revertBeforeShutdown', "Reverting editors with unsaved changes is taking longer than expected...")); } private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string, detail?: string): Promise { diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index a8f715a7048..5c30da7a14d 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -307,7 +307,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi } private onWorkspaceConfigurationFileDirtyError(): void { - const message = localize('errorWorkspaceConfigurationFileDirty', "Unable to write into workspace configuration file because the file is dirty. Please save it and try again."); + const message = localize('errorWorkspaceConfigurationFileDirty', "Unable to write into workspace configuration file because the file has unsaved changes. Please save it and try again."); this.askToOpenWorkspaceConfigurationFile(message); }