#30955 Implement virtual editor for workspace settings in MR workspace

This commit is contained in:
Sandeep Somavarapu 2017-07-21 18:14:56 +05:30
parent f4208eff49
commit 600e0dbc10
6 changed files with 225 additions and 26 deletions

View file

@ -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"

View file

@ -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((<ResourceEditorModel>editorModel).textEditorModel)));
}
clearInput(): void {
this.modelDisposables = dispose(this.modelDisposables);
super.clearInput();
}
private onDidModelChange(): void {
this.modelDisposables = dispose(this.modelDisposables);
const model = getCodeEditor(this).getModel();
this.modelDisposables.push(model.onDidChangeContent(() => this.save(model.uri)));
}
private save(resource: URI): void {
this.textFileService.save(resource);
if (model) {
this.preferencesService.createPreferencesEditorModel(model.uri)
.then(preferencesEditorModel => {
const settingsEditorModel = <SettingsEditorModel>preferencesEditorModel;
this.modelDisposables.push(settingsEditorModel);
this.modelDisposables.push(model.onDidChangeContent(() => settingsEditorModel.save()));
});
}
}
}

View file

@ -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<TPromise<IPreferencesEditorModel<any>>>;
private lastOpenedSettingsInput: PreferencesEditorInput = null;
private _onDispose: Emitter<void> = new Emitter<void>();
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<string> {
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<IPreferencesEditorModel<any>> {
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<SettingsEditorModel> {
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<SettingsEditorModel>(null);
}
private resolveSettingsContentFromWorkspaceConfiguration(): TPromise<string> {
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<void> {
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();
}

View file

@ -73,6 +73,7 @@ export interface IPreferencesService {
workspaceSettingsResource: URI;
defaultKeybindingsResource: URI;
resolveContent(uri: URI): TPromise<string>;
createPreferencesEditorModel<T>(uri: URI): TPromise<IPreferencesEditorModel<T>>;
openSettings(target: ConfigurationTarget | URI): TPromise<IEditor>;

View file

@ -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;

View file

@ -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<void>;
constructor(reference: IReference<ITextEditorModel>, private _configurationTarget: ConfigurationTarget) {
constructor(reference: IReference<ITextEditorModel>, 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<void>();
}
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<any> {
return this.queue.queue(() => this.doSave());
}
protected doSave(): TPromise<any> {
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<ITextEditorModel>,
workspaceConfigModelReference: IReference<ITextEditorModel>,
_configurationTarget: ConfigurationTarget,
onDispose: Event<void>,
@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<any> {
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, <JSONVisitor>{
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[];