From 600e0dbc10de722bcc9e74af81d13bca0386f4e3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 21 Jul 2017 18:14:56 +0530 Subject: [PATCH] #30955 Implement virtual editor for workspace settings in MR workspace --- extensions/configuration-editing/package.json | 4 + .../preferences/browser/preferencesEditor.ts | 22 ++- .../preferences/browser/preferencesService.ts | 44 ++++- .../parts/preferences/common/preferences.ts | 1 + .../common/preferencesContentProvider.ts | 9 +- .../preferences/common/preferencesModels.ts | 171 +++++++++++++++++- 6 files changed, 225 insertions(+), 26 deletions(-) diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 54f70a0b137..d7fd6d3d30e 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -35,6 +35,10 @@ "fileMatch": "vscode://defaultsettings/settings.json", "url": "vscode://schemas/settings" }, + { + "fileMatch": "vscode://settings/workspaceSettings.json", + "url": "vscode://schemas/settings" + }, { "fileMatch": "%APP_SETTINGS_HOME%/settings.json", "url": "vscode://schemas/settings" diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index b7570f46216..c0ab452bb1e 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -422,13 +422,13 @@ class SideBySidePreferencesWidget extends Widget { this.defaultPreferencesEditorContainer = DOM.append(parentElement, DOM.$('.default-preferences-editor-container')); this.defaultPreferencesEditorContainer.style.position = 'absolute'; - this.defaultPreferencesEditor = this.instantiationService.createInstance(DefaultPreferencesEditor); + this.defaultPreferencesEditor = this._register(this.instantiationService.createInstance(DefaultPreferencesEditor)); this.defaultPreferencesEditor.create(new Builder(this.defaultPreferencesEditorContainer)); this.defaultPreferencesEditor.setVisible(true); this.editablePreferencesEditorContainer = DOM.append(parentElement, DOM.$('.editable-preferences-editor-container')); this.editablePreferencesEditorContainer.style.position = 'absolute'; - this.editablePreferencesEditor = this.instantiationService.createInstance(EditableSettingsEditor); + this.editablePreferencesEditor = this._register(this.instantiationService.createInstance(EditableSettingsEditor)); this.editablePreferencesEditor.create(new Builder(this.editablePreferencesEditorContainer)); this.editablePreferencesEditor.setVisible(true); @@ -582,14 +582,22 @@ export class EditableSettingsEditor extends BaseTextEditor { .then(editorModel => this.getControl().setModel((editorModel).textEditorModel))); } - private onDidModelChange(): void { + clearInput(): void { this.modelDisposables = dispose(this.modelDisposables); - const model = getCodeEditor(this).getModel(); - this.modelDisposables.push(model.onDidChangeContent(() => this.save(model.uri))); + super.clearInput(); } - private save(resource: URI): void { - this.textFileService.save(resource); + private onDidModelChange(): void { + this.modelDisposables = dispose(this.modelDisposables); + const model = getCodeEditor(this).getModel(); + if (model) { + this.preferencesService.createPreferencesEditorModel(model.uri) + .then(preferencesEditorModel => { + const settingsEditorModel = preferencesEditorModel; + this.modelDisposables.push(settingsEditorModel); + this.modelDisposables.push(model.onDidChangeContent(() => settingsEditorModel.save())); + }); + } } } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index bd493033a18..10f314e0a27 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -13,6 +13,7 @@ import { ResourceMap } from 'vs/base/common/map'; import * as labels from 'vs/base/common/labels'; import * as strings from 'vs/base/common/strings'; import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter } from 'vs/base/common/event'; import { EditorInput } from 'vs/workbench/common/editor'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -28,7 +29,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName } from 'vs/workbench/parts/preferences/common/preferences'; -import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents } from 'vs/workbench/parts/preferences/common/preferencesModels'; +import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, WorkspaceConfigModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; import { KeybindingsEditorInput } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; @@ -57,6 +58,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic private defaultPreferencesEditorModels: ResourceMap>>; private lastOpenedSettingsInput: PreferencesEditorInput = null; + private _onDispose: Emitter = new Emitter(); + constructor( @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IEditorGroupService private editorGroupService: IEditorGroupService, @@ -99,6 +102,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic readonly defaultSettingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/settings.json' }); readonly defaultKeybindingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/keybindings.json' }); + private readonly workspaceConfigSettingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'settings', path: '/workspaceSettings.json' }); get userSettingsResource(): URI { return this.getEditableSettingsURI(ConfigurationTarget.USER); @@ -108,6 +112,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE); } + resolveContent(uri: URI): TPromise { + const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE); + if (workspaceSettingsUri && workspaceSettingsUri.fsPath === uri.fsPath) { + return this.resolveSettingsContentFromWorkspaceConfiguration(); + } + return this.createPreferencesEditorModel(uri) + .then(preferencesEditorModel => preferencesEditorModel ? preferencesEditorModel.content : null); + } + createPreferencesEditorModel(uri: URI): TPromise> { let promise = this.defaultPreferencesEditorModels.get(uri); if (promise) { @@ -132,6 +145,12 @@ export class PreferencesService extends Disposable implements IPreferencesServic return promise; } + if (this.workspaceConfigSettingsResource.fsPath === uri.fsPath) { + promise = this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE); + this.defaultPreferencesEditorModels.set(uri, promise); + return promise; + } + if (this.getEditableSettingsURI(ConfigurationTarget.USER).fsPath === uri.fsPath) { return this.createEditableSettingsEditorModel(ConfigurationTarget.USER); } @@ -243,12 +262,29 @@ export class PreferencesService extends Disposable implements IPreferencesServic private createEditableSettingsEditorModel(configurationTarget: ConfigurationTarget): TPromise { const settingsUri = this.getEditableSettingsURI(configurationTarget); if (settingsUri) { + if (settingsUri.fsPath === this.workspaceConfigSettingsResource.fsPath) { + return TPromise.join([this.textModelResolverService.createModelReference(settingsUri), this.textModelResolverService.createModelReference(this.contextService.getWorkspace().configuration)]) + .then(([reference, workspaceConfigReference]) => this.instantiationService.createInstance(WorkspaceConfigModel, reference, workspaceConfigReference, configurationTarget, this._onDispose.event)); + } return this.textModelResolverService.createModelReference(settingsUri) .then(reference => this.instantiationService.createInstance(SettingsEditorModel, reference, configurationTarget)); } return TPromise.wrap(null); } + private resolveSettingsContentFromWorkspaceConfiguration(): TPromise { + if (this.contextService.hasMultiFolderWorkspace()) { + return this.textModelResolverService.createModelReference(this.contextService.getWorkspace().configuration) + .then(reference => { + const model = reference.object.textEditorModel; + const settingsContent = WorkspaceConfigModel.getSettingsContentFromConfigContent(model.getValue()); + reference.dispose(); + return TPromise.as(settingsContent ? settingsContent : this.getEmptyEditableSettingsContent(ConfigurationTarget.WORKSPACE)); + }); + } + return TPromise.as(null); + } + private getEmptyEditableSettingsContent(target: ConfigurationTarget | URI): string { if (target === ConfigurationTarget.USER) { const emptySettingsHeader = nls.localize('emptySettingsHeader', "Place your settings in this file to overwrite the default settings"); @@ -275,7 +311,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.toResource(paths.join('.vscode', 'settings.json'), workspace.roots[0]); } if (this.contextService.hasMultiFolderWorkspace()) { - return workspace.configuration; + return this.workspaceConfigSettingsResource; } return null; } @@ -289,9 +325,6 @@ export class PreferencesService extends Disposable implements IPreferencesServic private createSettingsIfNotExists(target: ConfigurationTarget | URI): TPromise { const resource = this.getEditableSettingsURI(target); if (this.contextService.hasMultiFolderWorkspace() && target === ConfigurationTarget.WORKSPACE) { - if (!this.configurationService.keys().workspace.length) { - return this.jsonEditingService.write(resource, { key: 'settings', value: {} }, true).then(null, () => { }); - } return TPromise.as(null); } const editableSettingsEmptyContent = this.getEmptyEditableSettingsContent(target); @@ -367,6 +400,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } public dispose(): void { + this._onDispose.fire(); this.defaultPreferencesEditorModels.clear(); super.dispose(); } diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 1c7f1279555..e076d37b2de 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -73,6 +73,7 @@ export interface IPreferencesService { workspaceSettingsResource: URI; defaultKeybindingsResource: URI; + resolveContent(uri: URI): TPromise; createPreferencesEditorModel(uri: URI): TPromise>; openSettings(target: ConfigurationTarget | URI): TPromise; diff --git a/src/vs/workbench/parts/preferences/common/preferencesContentProvider.ts b/src/vs/workbench/parts/preferences/common/preferencesContentProvider.ts index 6fff0beb5de..4b58705c22b 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesContentProvider.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesContentProvider.ts @@ -47,12 +47,11 @@ export class PreferencesContentProvider implements IWorkbenchContribution { return TPromise.as(this.modelService.createModel(modelContent, mode, uri)); } } - return this.preferencesService.createPreferencesEditorModel(uri) - .then(preferencesModel => { - if (preferencesModel) { + return this.preferencesService.resolveContent(uri) + .then(content => { + if (content !== null && content !== void 0) { let mode = this.modeService.getOrCreateMode('json'); - const model = this.modelService.createModel(preferencesModel.content, mode, uri); - preferencesModel.dispose(); + const model = this.modelService.createModel(content, mode, uri); return TPromise.as(model); } return null; diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index aea7dfe8ed8..3b5505a1b7c 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -9,6 +9,7 @@ import { assign } from 'vs/base/common/objects'; import { distinct } from 'vs/base/common/arrays'; import URI from 'vs/base/common/uri'; import { IReference } from 'vs/base/common/lifecycle'; +import Event from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { visit, JSONVisitor } from 'vs/base/common/json'; import { IModel } from 'vs/editor/common/editorCommon'; @@ -19,8 +20,12 @@ import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; import { IMatch, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters'; -import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { IRange } from 'vs/editor/common/core/range'; +import { ITextFileService, StateChange } from "vs/workbench/services/textfile/common/textfiles"; +import { TPromise } from "vs/base/common/winjs.base"; +import { Queue } from "vs/base/common/async"; +import { IFileService } from 'vs/platform/files/common/files'; class SettingMatches { @@ -233,19 +238,21 @@ export abstract class AbstractSettingsModel extends EditorModel { export class SettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel { private _settingsGroups: ISettingsGroup[]; - private model: IModel; + protected settingsModel: IModel; + private queue: Queue; - constructor(reference: IReference, private _configurationTarget: ConfigurationTarget) { + constructor(reference: IReference, private _configurationTarget: ConfigurationTarget, @ITextFileService protected textFileService: ITextFileService) { super(); - this.model = reference.object.textEditorModel; + this.settingsModel = reference.object.textEditorModel; this._register(this.onDispose(() => reference.dispose())); - this._register(this.model.onDidChangeContent(() => { + this._register(this.settingsModel.onDidChangeContent(() => { this._settingsGroups = null; })); + this.queue = new Queue(); } public get uri(): URI { - return this.model.uri; + return this.settingsModel.uri; } public get configurationTarget(): ConfigurationTarget { @@ -260,19 +267,27 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti } public get content(): string { - return this.model.getValue(); + return this.settingsModel.getValue(); } public filterSettings(filter: string): IFilterResult { return this.doFilterSettings(filter, this.settingsGroups); } + public save(): TPromise { + return this.queue.queue(() => this.doSave()); + } + + protected doSave(): TPromise { + return this.textFileService.save(this.uri); + } + protected findValueMatches(filter: string, setting: ISetting): IRange[] { - return this.model.findMatches(filter, setting.valueRange, false, false, null, false).map(match => match.range); + return this.settingsModel.findMatches(filter, setting.valueRange, false, false, null, false).map(match => match.range); } private parse() { - const model = this.model; + const model = this.settingsModel; const settings: ISetting[] = []; let overrideSetting: ISetting = null; @@ -439,6 +454,144 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti } } +export class WorkspaceConfigModel extends SettingsEditorModel implements ISettingsEditorModel { + + private workspaceConfigModel: IModel; + private workspaceConfigEtag: string; + + constructor( + reference: IReference, + workspaceConfigModelReference: IReference, + _configurationTarget: ConfigurationTarget, + onDispose: Event, + @IFileService private fileService: IFileService, + @ITextModelService private textModelResolverService: ITextModelService, + @ITextFileService textFileService: ITextFileService + ) { + super(reference, _configurationTarget, textFileService); + + this._register(workspaceConfigModelReference); + this.workspaceConfigModel = workspaceConfigModelReference.object.textEditorModel; + + // Only listen to state changes. Content changes without saving are not synced. + this._register(this.textFileService.models.get(this.workspaceConfigModel.uri).onDidStateChange(statChange => this._onWorkspaceConfigFileStateChanged(statChange))); + this.onDispose(() => super.dispose()); + } + + protected doSave(): TPromise { + if (this.textFileService.isDirty(this.workspaceConfigModel.uri)) { + // Throw an error? + return TPromise.as(null); + } + + const content = this.createWorkspaceConfigContentFromSettingsModel(); + if (content !== this.workspaceConfigModel.getValue()) { + return this.fileService.updateContent(this.workspaceConfigModel.uri, content) + .then(stat => this.workspaceConfigEtag = stat.etag); + } + + return TPromise.as(null); + } + + private createWorkspaceConfigContentFromSettingsModel(): string { + const workspaceConfigContent = this.workspaceConfigModel.getValue(); + const { settingsPropertyEndsAt, nodeAfterSettingStartsAt } = WorkspaceConfigModel.parseWorkspaceConfigContent(workspaceConfigContent); + const workspaceConfigEndsAt = workspaceConfigContent.lastIndexOf('}'); + + // Settings property exist in Workspace Configuration and has Ending Brace + if (settingsPropertyEndsAt !== -1 && workspaceConfigEndsAt > settingsPropertyEndsAt) { + + // Place settings at the end + let from = workspaceConfigContent.indexOf(':', settingsPropertyEndsAt) + 1; + let to = workspaceConfigEndsAt; + let settingsContent = this.settingsModel.getValue(); + + // There is a node after settings property + // Place settings before that node + if (nodeAfterSettingStartsAt !== -1) { + settingsContent += ','; + to = nodeAfterSettingStartsAt; + } + + return workspaceConfigContent.substring(0, from) + settingsContent + workspaceConfigContent.substring(to); + } + + // Settings property does not exist. Place it at the end + return workspaceConfigContent.substring(0, workspaceConfigEndsAt) + `,\n"settings": ${this.settingsModel.getValue()}\n` + workspaceConfigContent.substring(workspaceConfigEndsAt); + } + + private _onWorkspaceConfigFileStateChanged(stateChange: StateChange): void { + let hasToUpdate = false; + switch (stateChange) { + case StateChange.SAVED: + hasToUpdate = this.workspaceConfigEtag !== this.textFileService.models.get(this.workspaceConfigModel.uri).getETag(); + break; + } + if (hasToUpdate) { + this.onWorkspaceConfigFileContentChanged(); + } + } + + private onWorkspaceConfigFileContentChanged(): void { + this.workspaceConfigEtag = this.textFileService.models.get(this.workspaceConfigModel.uri).getETag(); + const settingsValue = WorkspaceConfigModel.getSettingsContentFromConfigContent(this.workspaceConfigModel.getValue()); + if (settingsValue) { + this.settingsModel.setValue(settingsValue); + } + } + + dispose() { + // Not disposable by default + } + + static getSettingsContentFromConfigContent(workspaceConfigContent: string): string { + const { settingsPropertyEndsAt, nodeAfterSettingStartsAt } = WorkspaceConfigModel.parseWorkspaceConfigContent(workspaceConfigContent); + + const workspaceConfigEndsAt = workspaceConfigContent.lastIndexOf('}'); + + if (settingsPropertyEndsAt !== -1) { + const from = workspaceConfigContent.indexOf(':', settingsPropertyEndsAt) + 1; + const to = nodeAfterSettingStartsAt !== -1 ? nodeAfterSettingStartsAt : workspaceConfigEndsAt; + return workspaceConfigContent.substring(from, to); + } + + return null; + } + + static parseWorkspaceConfigContent(content: string): { settingsPropertyEndsAt: number, nodeAfterSettingStartsAt: number } { + + let settingsPropertyEndsAt = -1; + let nodeAfterSettingStartsAt = -1; + + let rootProperties = []; + let ancestors = []; + let currentProperty = ''; + + visit(content, { + onObjectProperty: (name: string, offset: number, length: number) => { + currentProperty = name; + if (ancestors.length === 1) { + rootProperties.push(name); + if (rootProperties[rootProperties.length - 1] === 'settings') { + settingsPropertyEndsAt = offset + length; + } + if (rootProperties[rootProperties.length - 2] === 'settings') { + nodeAfterSettingStartsAt = offset; + } + } + }, + onObjectBegin: (offset: number, length: number) => { + ancestors.push(currentProperty); + }, + onObjectEnd: (offset: number, length: number) => { + ancestors.pop(); + } + }, { allowTrailingComma: true }); + + return { settingsPropertyEndsAt, nodeAfterSettingStartsAt }; + } +} + export class DefaultSettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel { private _allSettingsGroups: ISettingsGroup[]; -- GitLab