#75079 Use fileservice for listening to keybindings file changes

This commit is contained in:
Sandeep Somavarapu 2019-06-11 16:34:02 +02:00
parent 3c1b39ae6b
commit a0184cc541

View file

@ -8,13 +8,11 @@ import * as nativeKeymap from 'native-keymap';
import { release } from 'os';
import * as dom from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Keybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { KeybindingParser } from 'vs/base/common/keybindingParser';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { ConfigWatcher } from 'vs/base/node/config';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Extensions as ConfigExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
@ -41,6 +39,13 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten
import { MenuRegistry } from 'vs/platform/actions/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { commandsExtensionPoint } from 'vs/workbench/api/common/menusExtensionPoint';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { URI } from 'vs/base/common/uri';
import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { dirname, isEqual } from 'vs/base/common/resources';
import { parse } from 'vs/base/common/json';
import * as objects from 'vs/base/common/objects';
export class KeyboardMapperFactory {
public static readonly INSTANCE = new KeyboardMapperFactory();
@ -267,8 +272,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
private _keyboardMapper: IKeyboardMapper;
private _cachedResolver: KeybindingResolver | null;
private _firstTimeComputingResolver: boolean;
private userKeybindings: ConfigWatcher<IUserFriendlyKeybinding[]>;
private userKeybindings: UserKeybindings;
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@ -278,7 +282,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
@IEnvironmentService environmentService: IEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@IWindowService private readonly windowService: IWindowService,
@IExtensionService extensionService: IExtensionService
@IExtensionService extensionService: IExtensionService,
@IFileService fileService: IFileService
) {
super(contextKeyService, commandService, telemetryService, notificationService);
@ -303,9 +308,27 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
});
this._cachedResolver = null;
this._firstTimeComputingResolver = true;
this.userKeybindings = this._register(new ConfigWatcher(environmentService.keybindingsResource.fsPath, { defaultConfig: [], onError: error => onUnexpectedError(error) }));
this.userKeybindings = this._register(new UserKeybindings(environmentService.keybindingsResource, fileService));
this.userKeybindings.initialize().then(() => {
if (this.userKeybindings.keybindings.length) {
this.updateResolver({ source: KeybindingSource.User });
}
});
this._register(this.userKeybindings.onDidChange(() => {
/* __GDPR__
"customKeybindingsChanged" : {
"keyCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this._telemetryService.publicLog('customKeybindingsChanged', {
keyCount: this.userKeybindings.keybindings.length
});
this.updateResolver({
source: KeybindingSource.User,
keybindings: this.userKeybindings.keybindings
});
}));
keybindingsExtPoint.setHandler((extensions) => {
@ -321,11 +344,6 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
updateSchema();
this._register(extensionService.onDidRegisterExtensions(() => updateSchema()));
this._register(this.userKeybindings.onDidUpdateConfiguration(event => this.updateResolver({
source: KeybindingSource.User,
keybindings: event.config
})));
this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let keyEvent = new StandardKeyboardEvent(e);
let shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
@ -353,18 +371,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
return `Layout info:\n${layoutInfo}\n${mapperInfo}\n\nRaw mapping:\n${rawMapping}`;
}
private _safeGetConfig(): IUserFriendlyKeybinding[] {
let rawConfig = this.userKeybindings.getConfig();
if (Array.isArray(rawConfig)) {
return rawConfig;
}
return [];
}
public customKeybindingsCount(): number {
let userKeybindings = this._safeGetConfig();
return userKeybindings.length;
return this.userKeybindings.keybindings.length;
}
private updateResolver(event: IKeybindingEvent): void {
@ -375,9 +383,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
protected _getResolver(): KeybindingResolver {
if (!this._cachedResolver) {
const defaults = this._resolveKeybindingItems(KeybindingsRegistry.getDefaultKeybindings(), true);
const overrides = this._resolveUserKeybindingItems(this._getExtraKeybindings(this._firstTimeComputingResolver), false);
const overrides = this._resolveUserKeybindingItems(this.userKeybindings.keybindings.map((k) => KeybindingIO.readUserKeybindingItem(k)), false);
this._cachedResolver = new KeybindingResolver(defaults, overrides);
this._firstTimeComputingResolver = false;
}
return this._cachedResolver;
}
@ -427,24 +434,6 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
return result;
}
private _getExtraKeybindings(isFirstTime: boolean): IUserKeybindingItem[] {
let extraUserKeybindings: IUserFriendlyKeybinding[] = this._safeGetConfig();
if (!isFirstTime) {
let cnt = extraUserKeybindings.length;
/* __GDPR__
"customKeybindingsChanged" : {
"keyCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this._telemetryService.publicLog('customKeybindingsChanged', {
keyCount: cnt
});
}
return extraUserKeybindings.map((k) => KeybindingIO.readUserKeybindingItem(k));
}
public resolveKeybinding(kb: Keybinding): ResolvedKeybinding[] {
return this._keyboardMapper.resolveKeybinding(kb);
}
@ -585,6 +574,105 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
}
class UserKeybindings extends Disposable {
private _keybindings: IUserFriendlyKeybinding[] = [];
get keybindings(): IUserFriendlyKeybinding[] { return this._keybindings; }
private readonly reloadConfigurationScheduler: RunOnceScheduler;
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
private fileWatcherDisposable: IDisposable = Disposable.None;
private directoryWatcherDisposable: IDisposable = Disposable.None;
constructor(
private readonly keybindingsResource: URI,
private readonly fileService: IFileService
) {
super();
this._register(fileService.onFileChanges(e => this.handleFileEvents(e)));
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(changed => {
if (changed) {
this._onDidChange.fire();
}
}), 50));
this._register(toDisposable(() => {
this.stopWatchingResource();
this.stopWatchingDirectory();
}));
}
private watchResource(): void {
this.fileWatcherDisposable = this.fileService.watch(this.keybindingsResource);
}
private stopWatchingResource(): void {
this.fileWatcherDisposable.dispose();
this.fileWatcherDisposable = Disposable.None;
}
private watchDirectory(): void {
const directory = dirname(this.keybindingsResource);
this.directoryWatcherDisposable = this.fileService.watch(directory);
}
private stopWatchingDirectory(): void {
this.directoryWatcherDisposable.dispose();
this.directoryWatcherDisposable = Disposable.None;
}
async initialize(): Promise<void> {
const exists = await this.fileService.exists(this.keybindingsResource);
this.onResourceExists(exists);
await this.reload();
}
private async reload(): Promise<boolean> {
const existing = this._keybindings;
try {
const content = await this.fileService.readFile(this.keybindingsResource);
this._keybindings = parse(content.value.toString());
} catch (e) {
this._keybindings = [];
}
return existing ? !objects.equals(existing, this._keybindings) : true;
}
private async handleFileEvents(event: FileChangesEvent): Promise<void> {
const events = event.changes;
let affectedByChanges = false;
// Find changes that affect the resource
for (const event of events) {
affectedByChanges = isEqual(this.keybindingsResource, event.resource);
if (affectedByChanges) {
if (event.type === FileChangeType.ADDED) {
this.onResourceExists(true);
} else if (event.type === FileChangeType.DELETED) {
this.onResourceExists(false);
}
break;
}
}
if (affectedByChanges) {
this.reloadConfigurationScheduler.schedule();
}
}
private onResourceExists(exists: boolean): void {
if (exists) {
this.stopWatchingDirectory();
this.watchResource();
} else {
this.stopWatchingResource();
this.watchDirectory();
}
}
}
let schemaId = 'vscode://schemas/keybindings';
let commandsSchemas: IJSONSchema[] = [];
let commandsEnum: string[] = [];