deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/preferences/.eslintrc.js
Normal file
10
packages/preferences/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
82
packages/preferences/README.md
Normal file
82
packages/preferences/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - PREFERENCES EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/preferences` extension implements the preferences API defined in `@theia/core`, including the four preference providers:
|
||||
|
||||
- `Default` Preference, which serves as default values of preferences,
|
||||
- `User` Preference for the user home directory, which has precedence over the default values,
|
||||
- `Workspace` Preference for the workspace, which has precedence over User Preference, and
|
||||
- `Folder` Preference for the root folder, which has precedence over the Workspace Preference
|
||||
|
||||
To set:
|
||||
|
||||
- `User` Preferences: Create or edit a `settings.json` under the `.theia` folder located either in the user home.
|
||||
- `Workspace` Preference: If one folder is opened as the workspace, create or edit a `settings.json` under the root of the workspace. If a multi-root workspace is opened, create or edit the "settings" property in the workspace file.
|
||||
- `Folder` Preferences: Create or edit a `settings.json` under any of the root folders.
|
||||
|
||||
Example of a `settings.json` below:
|
||||
|
||||
```typescript
|
||||
{
|
||||
// Enable/Disable the line numbers in the monaco editor
|
||||
"editor.lineNumbers": "off",
|
||||
// Tab width in the editor
|
||||
"editor.tabSize": 4,
|
||||
"files.watcherExclude": "path/to/file"
|
||||
}
|
||||
```
|
||||
|
||||
Example of a workspace file below:
|
||||
|
||||
```typescript
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "file:///home/username/helloworld"
|
||||
},
|
||||
{
|
||||
"path": "file:///home/username/dev/byeworld"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
// Enable/Disable the line numbers in the monaco editor
|
||||
"editor.lineNumbers": "off",
|
||||
// Tab width in the editor
|
||||
"editor.tabSize": 4,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/preferences`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_preferences.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
|
||||
# Theia - Preferences Extension
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
59
packages/preferences/package.json
Normal file
59
packages/preferences/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@theia/preferences",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Preferences Extension",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/editor": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@theia/monaco": "1.68.0",
|
||||
"@theia/monaco-editor-core": "1.96.302",
|
||||
"@theia/userstorage": "1.68.0",
|
||||
"@theia/workspace": "1.68.0",
|
||||
"async-mutex": "^0.3.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"jsonc-parser": "^2.2.0",
|
||||
"p-debounce": "^2.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/preference-frontend-module",
|
||||
"backend": "lib/node/preference-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { SectionPreferenceProvider } from '../common/section-preference-provider';
|
||||
import { PreferenceScope } from '@theia/core';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
|
||||
export const FolderPreferenceProviderFactory = Symbol('FolderPreferenceProviderFactory');
|
||||
export interface FolderPreferenceProviderFactory {
|
||||
(uri: URI, section: string, folder: FileStat): FolderPreferenceProvider;
|
||||
}
|
||||
|
||||
export const FolderPreferenceProviderFolder = Symbol('FolderPreferenceProviderFolder');
|
||||
export interface FolderPreferenceProviderOptions {
|
||||
readonly configUri: URI;
|
||||
readonly sectionName: string | undefined;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class FolderPreferenceProvider extends SectionPreferenceProvider {
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(FolderPreferenceProviderFolder) protected readonly folder: FileStat;
|
||||
|
||||
private _folderUri: URI;
|
||||
|
||||
get folderUri(): URI {
|
||||
if (!this._folderUri) {
|
||||
this._folderUri = this.folder.resource;
|
||||
}
|
||||
return this._folderUri;
|
||||
}
|
||||
getScope(): PreferenceScope {
|
||||
if (!this.workspaceService.isMultiRootWorkspaceOpened) {
|
||||
// when FolderPreferenceProvider is used as a delegate of WorkspacePreferenceProvider in a one-folder workspace
|
||||
return PreferenceScope.Workspace;
|
||||
}
|
||||
return PreferenceScope.Folder;
|
||||
}
|
||||
|
||||
override getDomain(): string[] {
|
||||
return [this.folderUri.toString()];
|
||||
}
|
||||
}
|
||||
243
packages/preferences/src/browser/folders-preferences-provider.ts
Normal file
243
packages/preferences/src/browser/folders-preferences-provider.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { FolderPreferenceProvider, FolderPreferenceProviderFactory } from './folder-preference-provider';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { PreferenceProviderImpl, PreferenceConfigurations, PreferenceResolveResult, PreferenceScope, PreferenceUtils } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class FoldersPreferencesProvider extends PreferenceProviderImpl {
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(FolderPreferenceProviderFactory)
|
||||
protected readonly folderPreferenceProviderFactory: FolderPreferenceProviderFactory;
|
||||
|
||||
@inject(PreferenceConfigurations)
|
||||
protected readonly configurations: PreferenceConfigurations;
|
||||
|
||||
protected readonly providers = new Map<string, FolderPreferenceProvider>();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
await this.workspaceService.roots;
|
||||
|
||||
this.updateProviders();
|
||||
this.workspaceService.onWorkspaceChanged(() => this.updateProviders());
|
||||
|
||||
const readyPromises: Promise<void>[] = [];
|
||||
for (const provider of this.providers.values()) {
|
||||
readyPromises.push(provider.ready.catch(e => console.error(e)));
|
||||
}
|
||||
Promise.all(readyPromises).then(() => this._ready.resolve());
|
||||
}
|
||||
|
||||
protected updateProviders(): void {
|
||||
const roots = this.workspaceService.tryGetRoots();
|
||||
const toDelete = new Set(this.providers.keys());
|
||||
for (const folder of roots) {
|
||||
for (const configPath of this.configurations.getPaths()) {
|
||||
for (const configName of [...this.configurations.getSectionNames(), this.configurations.getConfigName()]) {
|
||||
const sectionUri = this.configurations.createUri(folder.resource, configPath, configName);
|
||||
const sectionKey = sectionUri.toString();
|
||||
toDelete.delete(sectionKey);
|
||||
if (!this.providers.has(sectionKey)) {
|
||||
const provider = this.createProvider(sectionUri, configName, folder);
|
||||
this.providers.set(sectionKey, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of toDelete) {
|
||||
const provider = this.providers.get(key);
|
||||
if (provider) {
|
||||
this.providers.delete(key);
|
||||
provider.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override getConfigUri(resourceUri?: string, sectionName: string = this.configurations.getConfigName()): URI | undefined {
|
||||
for (const provider of this.getFolderProviders(resourceUri)) {
|
||||
const configUri = provider.getConfigUri(resourceUri);
|
||||
if (configUri && this.configurations.getName(configUri) === sectionName) {
|
||||
return configUri;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override getContainingConfigUri(resourceUri?: string, sectionName: string = this.configurations.getConfigName()): URI | undefined {
|
||||
for (const provider of this.getFolderProviders(resourceUri)) {
|
||||
const configUri = provider.getConfigUri();
|
||||
if (provider.contains(resourceUri) && this.configurations.getName(configUri) === sectionName) {
|
||||
return configUri;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override getDomain(): string[] {
|
||||
return this.workspaceService.tryGetRoots().map(root => root.resource.toString());
|
||||
}
|
||||
|
||||
override resolve<T>(preferenceName: string, resourceUri?: string): PreferenceResolveResult<T> {
|
||||
const result: PreferenceResolveResult<T> = {};
|
||||
const groups = this.groupProvidersByConfigName(resourceUri);
|
||||
for (const group of groups.values()) {
|
||||
for (const provider of group) {
|
||||
const { value, configUri } = provider.resolve<T>(preferenceName, resourceUri);
|
||||
if (configUri && value !== undefined) {
|
||||
result.configUri = configUri;
|
||||
result.value = PreferenceUtils.merge(result.value as any, value as any) as any;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getPreferences(resourceUri?: string): { [p: string]: any } {
|
||||
let result = {};
|
||||
const groups = this.groupProvidersByConfigName(resourceUri);
|
||||
for (const group of groups.values()) {
|
||||
for (const provider of group) {
|
||||
if (provider.getConfigUri(resourceUri)) {
|
||||
const preferences = provider.getPreferences();
|
||||
result = PreferenceUtils.merge(result, preferences) as any;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async setPreference(preferenceName: string, value: any, resourceUri?: string): Promise<boolean> {
|
||||
const firstPathFragment = preferenceName.split('.', 1)[0];
|
||||
const defaultConfigName = this.configurations.getConfigName();
|
||||
const configName = this.configurations.isSectionName(firstPathFragment) ? firstPathFragment : defaultConfigName;
|
||||
|
||||
const providers = this.getFolderProviders(resourceUri);
|
||||
let configPath: string | undefined;
|
||||
const candidates = providers.filter(provider => {
|
||||
// Attempt to figure out the settings folder (.vscode or .theia) we're interested in.
|
||||
const containingConfigUri = provider.getConfigUri(resourceUri);
|
||||
if (configPath === undefined && containingConfigUri) {
|
||||
configPath = this.configurations.getPath(containingConfigUri);
|
||||
}
|
||||
const providerName = this.configurations.getName(containingConfigUri ?? provider.getConfigUri());
|
||||
return providerName === configName || providerName === defaultConfigName;
|
||||
});
|
||||
|
||||
const configNameAndPathMatches = [];
|
||||
const configNameOnlyMatches = [];
|
||||
const configUriMatches = [];
|
||||
const otherMatches = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const domainMatches = candidate.getConfigUri(resourceUri);
|
||||
const configUri = domainMatches ?? candidate.getConfigUri();
|
||||
const nameMatches = this.configurations.getName(configUri) === configName;
|
||||
const pathMatches = this.configurations.getPath(configUri) === configPath;
|
||||
|
||||
// Perfect match, run immediately in case we can bail out early.
|
||||
if (nameMatches && domainMatches) {
|
||||
if (await candidate.setPreference(preferenceName, value, resourceUri)) {
|
||||
return true;
|
||||
}
|
||||
} else if (nameMatches && pathMatches) { // Right file in the right folder.
|
||||
configNameAndPathMatches.push(candidate);
|
||||
} else if (nameMatches) { // Right file.
|
||||
configNameOnlyMatches.push(candidate);
|
||||
} else if (domainMatches) { // Currently valid and governs target URI
|
||||
configUriMatches.push(candidate);
|
||||
} else {
|
||||
otherMatches.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
const candidateSets = [configNameAndPathMatches, configNameOnlyMatches, configUriMatches, otherMatches];
|
||||
|
||||
for (const candidateSet of candidateSets) {
|
||||
for (const candidate of candidateSet) {
|
||||
if (await candidate.setPreference(preferenceName, value, resourceUri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
override canHandleScope(scope: PreferenceScope): boolean {
|
||||
return this.workspaceService.isMultiRootWorkspaceOpened && scope === PreferenceScope.Folder || scope === PreferenceScope.Workspace;
|
||||
}
|
||||
|
||||
protected groupProvidersByConfigName(resourceUri?: string): Map<string, FolderPreferenceProvider[]> {
|
||||
const groups = new Map<string, FolderPreferenceProvider[]>();
|
||||
const providers = this.getFolderProviders(resourceUri);
|
||||
for (const configName of [this.configurations.getConfigName(), ...this.configurations.getSectionNames()]) {
|
||||
const group = [];
|
||||
for (const provider of providers) {
|
||||
if (this.configurations.getName(provider.getConfigUri()) === configName) {
|
||||
group.push(provider);
|
||||
}
|
||||
}
|
||||
groups.set(configName, group);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
protected getFolderProviders(resourceUri?: string): FolderPreferenceProvider[] {
|
||||
if (!resourceUri) {
|
||||
return [];
|
||||
}
|
||||
const resourcePath = new URI(resourceUri).path;
|
||||
let folder: Readonly<{ relativity: number, uri?: string }> = { relativity: Number.MAX_SAFE_INTEGER };
|
||||
const providers = new Map<string, FolderPreferenceProvider[]>();
|
||||
for (const provider of this.providers.values()) {
|
||||
const uri = provider.folderUri.toString();
|
||||
const folderProviders = (providers.get(uri) || []);
|
||||
folderProviders.push(provider);
|
||||
providers.set(uri, folderProviders);
|
||||
|
||||
// in case we have nested folders mounted as workspace roots, select the innermost enclosing folder
|
||||
const relativity = provider.folderUri.path.relativity(resourcePath);
|
||||
if (relativity >= 0 && folder.relativity > relativity) {
|
||||
folder = { relativity, uri };
|
||||
}
|
||||
}
|
||||
return folder.uri && providers.get(folder.uri) || [];
|
||||
}
|
||||
|
||||
protected createProvider(uri: URI, section: string, folder: FileStat): FolderPreferenceProvider {
|
||||
const provider = this.folderPreferenceProviderFactory(uri, section, folder);
|
||||
this.toDispose.push(provider);
|
||||
this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change)));
|
||||
return provider;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ListenerList, DisposableCollection, URI, PreferenceScope, Listener } from '@theia/core';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { FileContentStatus, PreferenceStorage } from '../common/abstract-resource-preference-provider';
|
||||
import { PreferenceTransaction, PreferenceTransactionFactory } from './preference-transaction-manager';
|
||||
|
||||
export class FrontendPreferenceStorage implements PreferenceStorage {
|
||||
|
||||
protected readonly onDidChangeFileContentListeners = new ListenerList<FileContentStatus, Promise<boolean>>();
|
||||
protected transaction: PreferenceTransaction | undefined;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
constructor(
|
||||
protected readonly transactionFactory: PreferenceTransactionFactory,
|
||||
protected readonly fileService: FileService,
|
||||
protected readonly uri: URI,
|
||||
protected readonly scope: PreferenceScope
|
||||
) {
|
||||
|
||||
this.fileService.watch(uri);
|
||||
this.fileService.onDidFilesChange(e => {
|
||||
if (e.contains(uri)) {
|
||||
this.read().then(content => this.onDidChangeFileContentListeners.invoke({ content, fileOK: true }, () => { }))
|
||||
.catch(() => this.onDidChangeFileContentListeners.invoke({ content: '', fileOK: false }, () => { }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
writeValue(key: string, path: string[], value: JSONValue): Promise<boolean> {
|
||||
if (!this.transaction?.open) {
|
||||
const current = this.transaction;
|
||||
this.transaction = this.transactionFactory({
|
||||
getScope: () => this.scope,
|
||||
getConfigUri: () => this.uri
|
||||
}, current?.result);
|
||||
this.transaction.onWillConclude(async status => {
|
||||
if (status) {
|
||||
const content = await this.read();
|
||||
await Listener.awaitAll({ content, fileOK: true }, this.onDidChangeFileContentListeners);
|
||||
}
|
||||
});
|
||||
this.toDispose.push(this.transaction);
|
||||
}
|
||||
return this.transaction.enqueueAction(key, path, value);
|
||||
}
|
||||
|
||||
onDidChangeFileContent: Listener.Registration<FileContentStatus, Promise<boolean>> = this.onDidChangeFileContentListeners.registration;
|
||||
async read(): Promise<string> {
|
||||
return (await this.fileService.read(this.uri)).value;
|
||||
}
|
||||
}
|
||||
20
packages/preferences/src/browser/index.ts
Normal file
20
packages/preferences/src/browser/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from '@theia/core/lib/browser/preferences';
|
||||
export * from './workspace-preference-provider';
|
||||
export * from './folders-preferences-provider';
|
||||
export * from './folder-preference-provider';
|
||||
67
packages/preferences/src/browser/monaco-jsonc-editor.ts
Normal file
67
packages/preferences/src/browser/monaco-jsonc-editor.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as jsoncparser from 'jsonc-parser';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
@injectable()
|
||||
export class MonacoJSONCEditor {
|
||||
@inject(MonacoWorkspace) protected readonly workspace: MonacoWorkspace;
|
||||
|
||||
async setValue(model: MonacoEditorModel, path: jsoncparser.JSONPath, value: unknown, shouldSave = true): Promise<void> {
|
||||
const edits = this.getEditOperations(model, path, value);
|
||||
if (edits.length > 0) {
|
||||
await this.workspace.applyBackgroundEdit(model, edits, shouldSave);
|
||||
}
|
||||
}
|
||||
|
||||
getEditOperations(model: MonacoEditorModel, path: jsoncparser.JSONPath, value: unknown): monaco.editor.IIdentifiedSingleEditOperation[] {
|
||||
const textModel = model.textEditorModel;
|
||||
const content = model.getText();
|
||||
// Everything is already undefined - no need for changes.
|
||||
if (!content.trim() && value === undefined) {
|
||||
return [];
|
||||
}
|
||||
// Delete the entire document.
|
||||
if (!path.length && value === undefined) {
|
||||
return [{
|
||||
range: textModel.getFullModelRange(),
|
||||
text: null, // eslint-disable-line no-null/no-null
|
||||
forceMoveMarkers: false
|
||||
}];
|
||||
}
|
||||
const { insertSpaces, tabSize, defaultEOL } = textModel.getOptions();
|
||||
const jsonCOptions = {
|
||||
formattingOptions: {
|
||||
insertSpaces,
|
||||
tabSize,
|
||||
eol: defaultEOL === monaco.editor.DefaultEndOfLine.LF ? '\n' : '\r\n'
|
||||
}
|
||||
};
|
||||
return jsoncparser.modify(content, path, value, jsonCOptions).map(edit => {
|
||||
const start = textModel.getPositionAt(edit.offset);
|
||||
const end = textModel.getPositionAt(edit.offset + edit.length);
|
||||
return {
|
||||
range: monaco.Range.fromPositions(start, end),
|
||||
text: edit.content || null, // eslint-disable-line no-null/no-null
|
||||
forceMoveMarkers: false
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
28
packages/preferences/src/browser/package.spec.ts
Normal file
28
packages/preferences/src/browser/package.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* note: this bogus test file is required so that
|
||||
we are able to run mocha unit tests on this
|
||||
package, without having any actual unit tests in it.
|
||||
This way a coverage report will be generated,
|
||||
showing 0% coverage, instead of no report.
|
||||
This file can be removed once we have real unit
|
||||
tests in place. */
|
||||
|
||||
describe('preferences package', () => {
|
||||
|
||||
it('should support code coverage statistics', () => true);
|
||||
});
|
||||
46
packages/preferences/src/browser/preference-bindings.ts
Normal file
46
packages/preferences/src/browser/preference-bindings.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Container, interfaces } from '@theia/core/shared/inversify';
|
||||
import { UserPreferenceProvider, UserPreferenceProviderFactory } from '../common/user-preference-provider';
|
||||
import { WorkspacePreferenceProvider } from './workspace-preference-provider';
|
||||
import { WorkspaceFilePreferenceProvider, WorkspaceFilePreferenceProviderFactory, WorkspaceFilePreferenceProviderOptions } from './workspace-file-preference-provider';
|
||||
import { FoldersPreferencesProvider } from './folders-preferences-provider';
|
||||
import { FolderPreferenceProvider, FolderPreferenceProviderFactory, FolderPreferenceProviderFolder } from './folder-preference-provider';
|
||||
import { SectionPreferenceProviderUri, SectionPreferenceProviderSection } from '../common/section-preference-provider';
|
||||
import { bindFactory, PreferenceProvider, PreferenceScope } from '@theia/core';
|
||||
import { UserStorageUri } from '@theia/userstorage/lib/browser';
|
||||
import { UserConfigsPreferenceProvider, UserStorageLocationProvider } from '../common/user-configs-preference-provider';
|
||||
|
||||
export function bindWorkspaceFilePreferenceProvider(bind: interfaces.Bind): void {
|
||||
bind(WorkspaceFilePreferenceProviderFactory).toFactory(ctx => (options: WorkspaceFilePreferenceProviderOptions) => {
|
||||
const child = new Container({ defaultScope: 'Singleton' });
|
||||
child.parent = ctx.container;
|
||||
child.bind(WorkspaceFilePreferenceProvider).toSelf();
|
||||
child.bind(WorkspaceFilePreferenceProviderOptions).toConstantValue(options);
|
||||
return child.get(WorkspaceFilePreferenceProvider);
|
||||
});
|
||||
}
|
||||
|
||||
export function bindPreferenceProviders(bind: interfaces.Bind, unbind: interfaces.Unbind): void {
|
||||
bind(PreferenceProvider).to(UserConfigsPreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.User);
|
||||
bind(PreferenceProvider).to(WorkspacePreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Workspace);
|
||||
bind(PreferenceProvider).to(FoldersPreferencesProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Folder);
|
||||
bindWorkspaceFilePreferenceProvider(bind);
|
||||
bind(UserStorageLocationProvider).toConstantValue(() => UserStorageUri);
|
||||
bindFactory(bind, UserPreferenceProviderFactory, UserPreferenceProvider, SectionPreferenceProviderUri, SectionPreferenceProviderSection);
|
||||
bindFactory(bind, FolderPreferenceProviderFactory, FolderPreferenceProvider, SectionPreferenceProviderUri, SectionPreferenceProviderSection, FolderPreferenceProviderFolder);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { CliPreferences } from '../common/cli-preferences';
|
||||
import { PreferenceService, PreferenceScope } from '@theia/core/lib/common/preferences';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceFrontendContribution implements FrontendApplicationContribution {
|
||||
@inject(CliPreferences)
|
||||
protected readonly CliPreferences: CliPreferences;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
onStart(): void {
|
||||
this.CliPreferences.getPreferences().then(async preferences => {
|
||||
await this.preferenceService.ready;
|
||||
for (const [key, value] of preferences) {
|
||||
this.preferenceService.set(key, value, PreferenceScope.User);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
import './preferences-monaco-contribution';
|
||||
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
|
||||
import { bindViewContribution, FrontendApplicationContribution, noopWidgetStatusBarContribution, OpenHandler, WidgetStatusBarContribution } from '@theia/core/lib/browser';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { PreferenceTreeGenerator } from './util/preference-tree-generator';
|
||||
import { bindPreferenceProviders } from './preference-bindings';
|
||||
import { bindPreferencesWidgets } from './views/preference-widget-bindings';
|
||||
import { PreferencesContribution } from './preferences-contribution';
|
||||
import { PreferenceScopeCommandManager } from './util/preference-scope-command-manager';
|
||||
import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
|
||||
import { PreferencesJsonSchemaContribution } from './preferences-json-schema-contribution';
|
||||
import { MonacoJSONCEditor } from './monaco-jsonc-editor';
|
||||
import { PreferenceTransaction, PreferenceTransactionFactory, preferenceTransactionFactoryCreator } from './preference-transaction-manager';
|
||||
import { PreferenceOpenHandler } from './preference-open-handler';
|
||||
import { CliPreferences, CliPreferencesPath } from '../common/cli-preferences';
|
||||
import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
|
||||
import { PreferenceFrontendContribution } from './preference-frontend-contribution';
|
||||
import { PreferenceLayoutProvider } from './util/preference-layout';
|
||||
import { PreferencesWidget } from './views/preference-widget';
|
||||
import { PreferenceStorageFactory } from '../common/abstract-resource-preference-provider';
|
||||
import { FrontendPreferenceStorage } from './frontend-preference-storage';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { PreferenceScope, URI } from '@theia/core';
|
||||
|
||||
export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind): void {
|
||||
bindPreferenceProviders(bind, unbind);
|
||||
bindPreferencesWidgets(bind);
|
||||
|
||||
bind(PreferenceTreeGenerator).toSelf().inSingletonScope();
|
||||
bind(PreferenceLayoutProvider).toSelf().inSingletonScope();
|
||||
|
||||
bindViewContribution(bind, PreferencesContribution);
|
||||
|
||||
bind(PreferenceOpenHandler).toSelf().inSingletonScope();
|
||||
bind(OpenHandler).toService(PreferenceOpenHandler);
|
||||
|
||||
bind(PreferenceScopeCommandManager).toSelf().inSingletonScope();
|
||||
bind(TabBarToolbarContribution).toService(PreferencesContribution);
|
||||
|
||||
bind(PreferencesJsonSchemaContribution).toSelf().inSingletonScope();
|
||||
bind(JsonSchemaContribution).toService(PreferencesJsonSchemaContribution);
|
||||
|
||||
bind(MonacoJSONCEditor).toSelf().inSingletonScope();
|
||||
bind(PreferenceTransaction).toSelf();
|
||||
bind(PreferenceTransactionFactory).toFactory(preferenceTransactionFactoryCreator);
|
||||
bind(PreferenceStorageFactory).toFactory(({ container }) => (uri: URI, scope: PreferenceScope) => new FrontendPreferenceStorage(
|
||||
container.get(PreferenceTransactionFactory),
|
||||
container.get(FileService),
|
||||
uri,
|
||||
scope
|
||||
));
|
||||
|
||||
bind(CliPreferences).toDynamicValue(ctx => ServiceConnectionProvider.createProxy<CliPreferences>(ctx.container, CliPreferencesPath)).inSingletonScope();
|
||||
bind(PreferenceFrontendContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(PreferenceFrontendContribution);
|
||||
|
||||
bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(PreferencesWidget));
|
||||
}
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bindPreferences(bind, unbind);
|
||||
});
|
||||
53
packages/preferences/src/browser/preference-open-handler.ts
Normal file
53
packages/preferences/src/browser/preference-open-handler.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { animationFrame, OpenHandler } from '@theia/core/lib/browser';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { PreferencesContribution } from './preferences-contribution';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceOpenHandler implements OpenHandler {
|
||||
|
||||
readonly id = 'preference';
|
||||
|
||||
@inject(PreferencesContribution)
|
||||
protected readonly preferencesContribution: PreferencesContribution;
|
||||
|
||||
canHandle(uri: URI): number {
|
||||
return uri.scheme === this.id ? 500 : -1;
|
||||
}
|
||||
|
||||
async open(uri: URI): Promise<boolean> {
|
||||
const preferencesWidget = await this.preferencesContribution.openView();
|
||||
const selector = `li[data-pref-id="${uri.path.toString()}"]:not([data-node-id^="commonly-used@"])`;
|
||||
const element = document.querySelector(selector);
|
||||
if (element instanceof HTMLElement) {
|
||||
if (element.classList.contains('hidden')) {
|
||||
// We clear the search term as we have clicked on a hidden preference
|
||||
await preferencesWidget.setSearchTerm('');
|
||||
await animationFrame();
|
||||
}
|
||||
element.scrollIntoView({
|
||||
block: 'center'
|
||||
});
|
||||
element.focus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { CancellationError, Listener, ListenerList, MaybePromise, MessageService, nls } from '@theia/core';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { Mutex, MutexInterface } from 'async-mutex';
|
||||
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { MonacoJSONCEditor } from './monaco-jsonc-editor';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
|
||||
|
||||
@injectable()
|
||||
/**
|
||||
* Represents a batch of interactions with an underlying resource.
|
||||
*/
|
||||
export abstract class Transaction<Arguments extends unknown[], Result = unknown, Status = unknown> {
|
||||
protected _open = true;
|
||||
/**
|
||||
* Whether the transaction is still accepting new interactions.
|
||||
* Enqueueing an action when the Transaction is no longer open will throw an error.
|
||||
*/
|
||||
get open(): boolean {
|
||||
return this._open;
|
||||
}
|
||||
protected _result = new Deferred<Result | false>();
|
||||
/**
|
||||
* The status of the transaction when complete.
|
||||
*/
|
||||
get result(): Promise<Result | false> {
|
||||
return this._result.promise;
|
||||
}
|
||||
/**
|
||||
* The transaction will self-dispose when the queue is empty, once at least one action has been processed.
|
||||
*/
|
||||
protected readonly queue = new Mutex(new CancellationError());
|
||||
protected readonly onWillConcludeListeners = new ListenerList<(Status | false), Promise<void>>();
|
||||
/**
|
||||
* An event fired when the transaction is wrapping up.
|
||||
* Consumers can call `waitUntil` on the event to delay the resolution of the `result` Promise.
|
||||
*/
|
||||
onWillConclude = this.onWillConcludeListeners.registration;
|
||||
|
||||
protected status = new Deferred<Status>();
|
||||
/**
|
||||
* Whether any actions have been added to the transaction.
|
||||
* The Transaction will not self-dispose until at least one action has been performed.
|
||||
*/
|
||||
protected inUse = false;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
const release = await this.queue.acquire();
|
||||
try {
|
||||
const status = await this.setUp();
|
||||
this.status.resolve(status);
|
||||
} catch {
|
||||
this.dispose();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
async waitFor(delay?: Promise<unknown>, disposeIfRejected?: boolean): Promise<void> {
|
||||
try {
|
||||
await this.queue.runExclusive(() => delay);
|
||||
} catch {
|
||||
if (disposeIfRejected) {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a promise reflecting the result of performing an action. Typically the promise will not resolve until the whole transaction is complete.
|
||||
*/
|
||||
async enqueueAction(...args: Arguments): Promise<Result | false> {
|
||||
if (this._open) {
|
||||
let release: MutexInterface.Releaser | undefined;
|
||||
try {
|
||||
release = await this.queue.acquire();
|
||||
if (!this.inUse) {
|
||||
this.inUse = true;
|
||||
this.disposeWhenDone();
|
||||
}
|
||||
return this.act(...args);
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
release?.();
|
||||
}
|
||||
} else {
|
||||
throw new Error('Transaction used after disposal.');
|
||||
}
|
||||
}
|
||||
|
||||
protected disposeWhenDone(): void {
|
||||
// Due to properties of the micro task system, it's possible for something to have been enqueued between
|
||||
// the resolution of the waitForUnlock() promise and the the time this code runs, so we have to check.
|
||||
this.queue.waitForUnlock().then(() => {
|
||||
if (!this.queue.isLocked()) {
|
||||
this.dispose();
|
||||
} else {
|
||||
this.disposeWhenDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async conclude(): Promise<void> {
|
||||
if (this._open) {
|
||||
try {
|
||||
this._open = false;
|
||||
this.queue.cancel();
|
||||
const result = await this.tearDown();
|
||||
const status: Status | boolean = (this.status.state === 'unresolved' || this.status.state === 'rejected') ? false : await this.status.promise;
|
||||
await Listener.awaitAll(status, this.onWillConcludeListeners);
|
||||
this._result.resolve(result);
|
||||
} catch {
|
||||
this._result.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.conclude();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs any code necessary to initialize the batch of interactions. No interaction will be run until the setup is complete.
|
||||
*
|
||||
* @returns a representation of the success of setup specific to a given transaction implementation.
|
||||
*/
|
||||
protected abstract setUp(): MaybePromise<Status>;
|
||||
/**
|
||||
* Performs a single interaction
|
||||
*
|
||||
* @returns the result of that interaction, specific to a given transaction type.
|
||||
*/
|
||||
protected abstract act(...args: Arguments): MaybePromise<Result>;
|
||||
/**
|
||||
* Runs any code necessary to complete a transaction and release any resources it holds.
|
||||
*
|
||||
* @returns implementation-specific information about the success of the transaction. Will be used as the final status of the transaction.
|
||||
*/
|
||||
protected abstract tearDown(): MaybePromise<Result>;
|
||||
}
|
||||
|
||||
export interface PreferenceContext {
|
||||
getConfigUri(): URI;
|
||||
getScope(): PreferenceScope;
|
||||
}
|
||||
|
||||
export const PreferenceContext = Symbol('PreferenceContext');
|
||||
export const PreferenceTransactionPreludeProvider = Symbol('PreferenceTransactionPreludeProvider');
|
||||
export type PreferenceTransactionPreludeProvider = () => Promise<unknown>;
|
||||
|
||||
@injectable()
|
||||
export class PreferenceTransaction extends Transaction<[string, string[], unknown], boolean, boolean> {
|
||||
reference: IReference<MonacoEditorModel> | undefined;
|
||||
@inject(PreferenceContext) protected readonly context: PreferenceContext;
|
||||
@inject(PreferenceTransactionPreludeProvider) protected readonly prelude?: PreferenceTransactionPreludeProvider;
|
||||
@inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService;
|
||||
@inject(MonacoJSONCEditor) protected readonly jsoncEditor: MonacoJSONCEditor;
|
||||
@inject(MessageService) protected readonly messageService: MessageService;
|
||||
@inject(EditorManager) protected readonly editorManager: EditorManager;
|
||||
|
||||
protected override async doInit(): Promise<void> {
|
||||
this.waitFor(this.prelude?.());
|
||||
await super.doInit();
|
||||
}
|
||||
|
||||
protected async setUp(): Promise<boolean> {
|
||||
const reference = await this.textModelService.createModelReference(this.context.getConfigUri()!);
|
||||
if (this._open) {
|
||||
this.reference = reference;
|
||||
} else {
|
||||
reference.dispose();
|
||||
return false;
|
||||
}
|
||||
if (reference.object.dirty) {
|
||||
const shouldContinue = await this.handleDirtyEditor();
|
||||
if (!shouldContinue) {
|
||||
this.dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the setting operation in progress, and any others started in the meantime, should continue.
|
||||
*/
|
||||
protected async handleDirtyEditor(): Promise<boolean> {
|
||||
const saveAndRetry = nls.localizeByDefault('Save and Retry');
|
||||
const open = nls.localizeByDefault('Open File');
|
||||
const msg = await this.messageService.error(
|
||||
// eslint-disable-next-line @theia/localization-check
|
||||
nls.localizeByDefault('Unable to write into {0} settings because the file has unsaved changes. Please save the {0} settings file first and then try again.',
|
||||
nls.localizeByDefault(PreferenceScope[this.context.getScope()].toLocaleLowerCase())
|
||||
),
|
||||
saveAndRetry, open);
|
||||
|
||||
if (this.reference?.object) {
|
||||
if (msg === open) {
|
||||
this.editorManager.open(new URI(this.reference.object.uri));
|
||||
} else if (msg === saveAndRetry) {
|
||||
await this.reference.object.save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async act(key: string, path: string[], value: unknown): Promise<boolean> {
|
||||
const model = this.reference?.object;
|
||||
try {
|
||||
if (model) {
|
||||
await this.jsoncEditor.setValue(model, path, value);
|
||||
return this.result;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
const message = `Failed to update the value of '${key}' in '${this.context.getConfigUri()}'.`;
|
||||
this.messageService.error(`${message} Please check if it is corrupted.`);
|
||||
console.error(`${message}`, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async tearDown(): Promise<boolean> {
|
||||
try {
|
||||
const model = this.reference?.object;
|
||||
if (model) {
|
||||
if (this.status.state === 'resolved' && await this.status.promise) {
|
||||
await model.save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
this.reference?.dispose();
|
||||
this.reference = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PreferenceTransactionFactory {
|
||||
(context: PreferenceContext, waitFor?: Promise<unknown>): PreferenceTransaction;
|
||||
}
|
||||
export const PreferenceTransactionFactory = Symbol('PreferenceTransactionFactory');
|
||||
|
||||
export const preferenceTransactionFactoryCreator: interfaces.FactoryCreator<PreferenceTransaction> = ({ container }) =>
|
||||
(context: PreferenceContext, waitFor?: Promise<unknown>) => {
|
||||
const child = container.createChild();
|
||||
child.bind(PreferenceContext).toConstantValue(context);
|
||||
child.bind(PreferenceTransactionPreludeProvider).toConstantValue(() => waitFor);
|
||||
return child.get(PreferenceTransaction);
|
||||
};
|
||||
|
||||
258
packages/preferences/src/browser/preference-tree-model.ts
Normal file
258
packages/preferences/src/browser/preference-tree-model.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
TreeModelImpl,
|
||||
TreeWidget,
|
||||
CompositeTreeNode,
|
||||
TopDownTreeIterator,
|
||||
TreeNode,
|
||||
NodeProps,
|
||||
ExpandableTreeNode,
|
||||
SelectableTreeNode,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { Emitter, PreferenceDataProperty, PreferenceSchemaService, PreferenceService } from '@theia/core';
|
||||
import { PreferencesSearchbarWidget } from './views/preference-searchbar-widget';
|
||||
import { PreferenceTreeGenerator } from './util/preference-tree-generator';
|
||||
import * as fuzzy from '@theia/core/shared/fuzzy';
|
||||
import { PreferencesScopeTabBar } from './views/preference-scope-tabbar-widget';
|
||||
import { Preference } from './util/preference-types';
|
||||
import { Event } from '@theia/core/lib/common';
|
||||
import { COMMONLY_USED_SECTION_PREFIX } from './util/preference-layout';
|
||||
|
||||
export interface PreferenceTreeNodeProps extends NodeProps {
|
||||
visibleChildren: number;
|
||||
isExpansible?: boolean;
|
||||
}
|
||||
|
||||
export interface PreferenceTreeNodeRow extends Readonly<TreeWidget.NodeRow>, PreferenceTreeNodeProps {
|
||||
node: Preference.TreeNode;
|
||||
}
|
||||
export enum PreferenceFilterChangeSource {
|
||||
Schema,
|
||||
Search,
|
||||
Scope,
|
||||
}
|
||||
export interface PreferenceFilterChangeEvent {
|
||||
source: PreferenceFilterChangeSource
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceTreeModel extends TreeModelImpl {
|
||||
|
||||
@inject(PreferenceSchemaService) protected readonly schemaProvider: PreferenceSchemaService;
|
||||
@inject(PreferencesSearchbarWidget) protected readonly filterInput: PreferencesSearchbarWidget;
|
||||
@inject(PreferenceTreeGenerator) protected readonly treeGenerator: PreferenceTreeGenerator;
|
||||
@inject(PreferencesScopeTabBar) protected readonly scopeTracker: PreferencesScopeTabBar;
|
||||
@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
|
||||
|
||||
protected readonly onTreeFilterChangedEmitter = new Emitter<PreferenceFilterChangeEvent>();
|
||||
readonly onFilterChanged = this.onTreeFilterChangedEmitter.event;
|
||||
|
||||
protected lastSearchedFuzzy: string = '';
|
||||
protected lastSearchedLiteral: string = '';
|
||||
protected lastSearchedTags: string[] = [];
|
||||
protected _currentScope: number = Number(Preference.DEFAULT_SCOPE.scope);
|
||||
protected _isFiltered: boolean = false;
|
||||
protected _currentRows: Map<string, PreferenceTreeNodeRow> = new Map();
|
||||
protected _totalVisibleLeaves = 0;
|
||||
|
||||
get currentRows(): Readonly<Map<string, PreferenceTreeNodeRow>> {
|
||||
return this._currentRows;
|
||||
}
|
||||
|
||||
get totalVisibleLeaves(): number {
|
||||
return this._totalVisibleLeaves;
|
||||
}
|
||||
|
||||
get isFiltered(): boolean {
|
||||
return this._isFiltered;
|
||||
}
|
||||
|
||||
get propertyList(): ReadonlyMap<string, PreferenceDataProperty> {
|
||||
return this.schemaProvider.getSchemaProperties();
|
||||
}
|
||||
|
||||
get currentScope(): Preference.SelectedScopeDetails {
|
||||
return this.scopeTracker.currentScope;
|
||||
}
|
||||
|
||||
get onSchemaChanged(): Event<CompositeTreeNode> {
|
||||
return this.treeGenerator.onSchemaChanged;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.treeGenerator.onSchemaChanged(newTree => this.handleNewSchema(newTree)),
|
||||
this.scopeTracker.onScopeChanged(scopeDetails => {
|
||||
this._currentScope = scopeDetails.scope;
|
||||
this.updateFilteredRows(PreferenceFilterChangeSource.Scope);
|
||||
}),
|
||||
this.filterInput.onFilterChanged(newSearchTerm => {
|
||||
this.lastSearchedTags = Array.from(newSearchTerm.matchAll(/@tag:([^\s]+)/g)).map(match => match[0].slice(5));
|
||||
const newSearchTermWithoutTags = newSearchTerm.replace(/@tag:[^\s]+/g, '');
|
||||
this.lastSearchedLiteral = newSearchTermWithoutTags;
|
||||
this.lastSearchedFuzzy = newSearchTermWithoutTags.replace(/\s/g, '');
|
||||
this._isFiltered = newSearchTerm.length > 2;
|
||||
if (this.isFiltered) {
|
||||
this.expandAll();
|
||||
} else if (CompositeTreeNode.is(this.root)) {
|
||||
this.collapseAll(this.root);
|
||||
}
|
||||
this.updateFilteredRows(PreferenceFilterChangeSource.Search);
|
||||
}),
|
||||
this.onFilterChanged(() => {
|
||||
this.filterInput.updateResultsCount(this._totalVisibleLeaves);
|
||||
}),
|
||||
this.onTreeFilterChangedEmitter,
|
||||
]);
|
||||
await this.preferenceService.ready;
|
||||
this.handleNewSchema(this.treeGenerator.root);
|
||||
}
|
||||
|
||||
private handleNewSchema(newRoot: CompositeTreeNode): void {
|
||||
this.root = newRoot;
|
||||
if (this.isFiltered) {
|
||||
this.expandAll();
|
||||
}
|
||||
this.updateFilteredRows(PreferenceFilterChangeSource.Schema);
|
||||
}
|
||||
|
||||
protected updateRows(): void {
|
||||
const root = this.root;
|
||||
this._currentRows = new Map();
|
||||
if (root) {
|
||||
this._totalVisibleLeaves = 0;
|
||||
let index = 0;
|
||||
|
||||
for (const node of new TopDownTreeIterator(root, {
|
||||
pruneCollapsed: false,
|
||||
pruneSiblings: true
|
||||
})) {
|
||||
if (TreeNode.isVisible(node) && Preference.TreeNode.is(node)) {
|
||||
const { id } = Preference.TreeNode.getGroupAndIdFromNodeId(node.id);
|
||||
if (CompositeTreeNode.is(node) || this.passesCurrentFilters(node, id)) {
|
||||
this.updateVisibleChildren(node);
|
||||
|
||||
this._currentRows.set(node.id, {
|
||||
index: index++,
|
||||
node,
|
||||
depth: node.depth,
|
||||
visibleChildren: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected updateFilteredRows(source: PreferenceFilterChangeSource): void {
|
||||
this.updateRows();
|
||||
this.onTreeFilterChangedEmitter.fire({ source });
|
||||
}
|
||||
|
||||
protected passesCurrentFilters(node: Preference.LeafNode, prefID: string): boolean {
|
||||
if (!this.schemaProvider.isValidInScope(prefID, this._currentScope)) {
|
||||
return false;
|
||||
}
|
||||
if (!this._isFiltered) {
|
||||
return true;
|
||||
}
|
||||
// When filtering, VSCode will render an item that is present in the commonly used section only once but render both its possible parents in the left-hand tree.
|
||||
// E.g. searching for editor.renderWhitespace will show one item in the main panel, but both 'Commonly Used' and 'Text Editor' in the left tree.
|
||||
// That seems counterintuitive and introduces a number of special cases, so I prefer to remove the commonly used section entirely when the user searches.
|
||||
if (node.id.startsWith(COMMONLY_USED_SECTION_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.lastSearchedTags.every(tag => node.preference.data.tags?.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
return fuzzy.test(this.lastSearchedFuzzy, prefID) // search matches preference name.
|
||||
// search matches description. Fuzzy isn't ideal here because the score depends on the order of discovery.
|
||||
|| (node.preference.data.description ?? '').includes(this.lastSearchedLiteral);
|
||||
}
|
||||
|
||||
protected override isVisibleSelectableNode(node: TreeNode): node is SelectableTreeNode {
|
||||
return CompositeTreeNode.is(node) && !!this._currentRows.get(node.id)?.visibleChildren;
|
||||
}
|
||||
|
||||
protected updateVisibleChildren(node: TreeNode): void {
|
||||
if (!CompositeTreeNode.is(node)) {
|
||||
this._totalVisibleLeaves++;
|
||||
let nextParent = node.parent?.id && this._currentRows.get(node.parent?.id);
|
||||
while (nextParent && nextParent.node !== this.root) {
|
||||
if (nextParent) {
|
||||
nextParent.visibleChildren += 1;
|
||||
}
|
||||
nextParent = nextParent.node.parent?.id && this._currentRows.get(nextParent.node.parent?.id);
|
||||
if (nextParent) {
|
||||
nextParent.isExpansible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collapseAllExcept(openNode: TreeNode | undefined): void {
|
||||
const openNodes: TreeNode[] = [];
|
||||
while (ExpandableTreeNode.is(openNode)) {
|
||||
openNodes.push(openNode);
|
||||
this.expandNode(openNode);
|
||||
openNode = openNode.parent;
|
||||
}
|
||||
if (CompositeTreeNode.is(this.root)) {
|
||||
this.root.children.forEach(child => {
|
||||
if (!openNodes.includes(child) && ExpandableTreeNode.is(child)) {
|
||||
this.collapseNode(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected expandAll(): void {
|
||||
if (CompositeTreeNode.is(this.root)) {
|
||||
this.root.children.forEach(child => {
|
||||
if (ExpandableTreeNode.is(child)) {
|
||||
this.expandNode(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getNodeFromPreferenceId(id: string): Preference.TreeNode | undefined {
|
||||
const node = this.getNode(this.treeGenerator.getNodeId(id));
|
||||
return node && Preference.TreeNode.is(node) ? node : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if selection changed, false otherwise
|
||||
*/
|
||||
selectIfNotSelected(node: SelectableTreeNode): boolean {
|
||||
const currentlySelected = this.selectedNodes[0];
|
||||
if (!node.selected || node !== currentlySelected) {
|
||||
node.selected = true;
|
||||
this.selectNode(node);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
256
packages/preferences/src/browser/preferences-contribution.ts
Normal file
256
packages/preferences/src/browser/preferences-contribution.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, optional } from '@theia/core/shared/inversify';
|
||||
import { MenuModelRegistry, CommandRegistry, nls, PreferenceScope, PreferenceService } from '@theia/core';
|
||||
import {
|
||||
CommonMenus,
|
||||
AbstractViewContribution,
|
||||
CommonCommands,
|
||||
KeybindingRegistry,
|
||||
Widget,
|
||||
QuickInputService,
|
||||
QuickPickItem,
|
||||
isFirefox,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { PreferencesWidget } from './views/preference-widget';
|
||||
import { Preference, PreferencesCommands, PreferenceMenus } from './util/preference-types';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
|
||||
@injectable()
|
||||
export class PreferencesContribution extends AbstractViewContribution<PreferencesWidget> {
|
||||
|
||||
@inject(FileService) protected readonly fileService: FileService;
|
||||
@inject(EditorManager) protected readonly editorManager: EditorManager;
|
||||
@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
|
||||
@inject(ClipboardService) protected readonly clipboardService: ClipboardService;
|
||||
@inject(PreferencesWidget) protected readonly scopeTracker: PreferencesWidget;
|
||||
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
|
||||
@inject(QuickInputService) @optional() protected readonly quickInputService: QuickInputService;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: PreferencesWidget.ID,
|
||||
widgetName: PreferencesWidget.LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'main',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(CommonCommands.OPEN_PREFERENCES, {
|
||||
execute: async (query?: string) => {
|
||||
const widget = await this.openView({ activate: true });
|
||||
if (typeof query === 'string') {
|
||||
widget.setSearchTerm(query);
|
||||
}
|
||||
},
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR, {
|
||||
isEnabled: () => true,
|
||||
isVisible: w => this.withWidget(w, () => true),
|
||||
execute: (preferenceId: string) => {
|
||||
this.openPreferencesJSON(preferenceId);
|
||||
}
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.COPY_JSON_NAME, {
|
||||
isEnabled: Preference.EditorCommandArgs.is,
|
||||
isVisible: Preference.EditorCommandArgs.is,
|
||||
execute: ({ id, value }: Preference.EditorCommandArgs) => {
|
||||
this.clipboardService.writeText(id);
|
||||
}
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.COPY_JSON_VALUE, {
|
||||
isEnabled: Preference.EditorCommandArgs.is,
|
||||
isVisible: Preference.EditorCommandArgs.is,
|
||||
execute: ({ id, value }: { id: string, value: string; }) => {
|
||||
const jsonString = `"${id}": ${JSON.stringify(value)}`;
|
||||
this.clipboardService.writeText(jsonString);
|
||||
}
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.RESET_PREFERENCE, {
|
||||
isEnabled: Preference.EditorCommandArgs.is,
|
||||
isVisible: Preference.EditorCommandArgs.is,
|
||||
execute: ({ id }: Preference.EditorCommandArgs) => {
|
||||
this.preferenceService.set(id, undefined, Number(this.scopeTracker.currentScope.scope), this.scopeTracker.currentScope.uri);
|
||||
}
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.OPEN_USER_PREFERENCES, {
|
||||
execute: async () => {
|
||||
const widget = await this.openView({ activate: true });
|
||||
widget.setScope(PreferenceScope.User);
|
||||
}
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.OPEN_WORKSPACE_PREFERENCES, {
|
||||
isEnabled: () => !!this.workspaceService.workspace,
|
||||
isVisible: () => !!this.workspaceService.workspace,
|
||||
execute: async () => {
|
||||
const widget = await this.openView({ activate: true });
|
||||
widget.setScope(PreferenceScope.Workspace);
|
||||
}
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.OPEN_FOLDER_PREFERENCES, {
|
||||
isEnabled: () => !!this.workspaceService.isMultiRootWorkspaceOpened && this.workspaceService.tryGetRoots().length > 0,
|
||||
isVisible: () => !!this.workspaceService.isMultiRootWorkspaceOpened && this.workspaceService.tryGetRoots().length > 0,
|
||||
execute: () => this.openFolderPreferences(root => {
|
||||
this.openView({ activate: true });
|
||||
this.scopeTracker.setScope(root.resource);
|
||||
})
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.OPEN_USER_PREFERENCES_JSON, {
|
||||
execute: async () => this.openJson(PreferenceScope.User)
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.OPEN_WORKSPACE_PREFERENCES_JSON, {
|
||||
isEnabled: () => !!this.workspaceService.workspace,
|
||||
isVisible: () => !!this.workspaceService.workspace,
|
||||
execute: async () => this.openJson(PreferenceScope.Workspace)
|
||||
});
|
||||
commands.registerCommand(PreferencesCommands.OPEN_FOLDER_PREFERENCES_JSON, {
|
||||
isEnabled: () => !!this.workspaceService.isMultiRootWorkspaceOpened && this.workspaceService.tryGetRoots().length > 0,
|
||||
isVisible: () => !!this.workspaceService.isMultiRootWorkspaceOpened && this.workspaceService.tryGetRoots().length > 0,
|
||||
execute: () => this.openFolderPreferences(root => this.openJson(PreferenceScope.Folder, root.resource.toString()))
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(CommonMenus.FILE_SETTINGS_SUBMENU_OPEN, {
|
||||
commandId: CommonCommands.OPEN_PREFERENCES.id,
|
||||
label: nls.localizeByDefault('Settings'),
|
||||
order: 'a10',
|
||||
});
|
||||
menus.registerMenuAction(CommonMenus.MANAGE_SETTINGS, {
|
||||
commandId: CommonCommands.OPEN_PREFERENCES.id,
|
||||
label: nls.localizeByDefault('Settings'),
|
||||
order: 'a10',
|
||||
});
|
||||
menus.registerMenuAction(PreferenceMenus.PREFERENCE_EDITOR_CONTEXT_MENU, {
|
||||
commandId: PreferencesCommands.RESET_PREFERENCE.id,
|
||||
label: PreferencesCommands.RESET_PREFERENCE.label,
|
||||
order: 'a'
|
||||
});
|
||||
menus.registerMenuAction(PreferenceMenus.PREFERENCE_EDITOR_COPY_ACTIONS, {
|
||||
commandId: PreferencesCommands.COPY_JSON_VALUE.id,
|
||||
label: PreferencesCommands.COPY_JSON_VALUE.label,
|
||||
order: 'b'
|
||||
});
|
||||
menus.registerMenuAction(PreferenceMenus.PREFERENCE_EDITOR_COPY_ACTIONS, {
|
||||
commandId: PreferencesCommands.COPY_JSON_NAME.id,
|
||||
label: PreferencesCommands.COPY_JSON_NAME.label,
|
||||
order: 'c'
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
keybindings.registerKeybinding({
|
||||
command: CommonCommands.OPEN_PREFERENCES.id,
|
||||
keybinding: (isOSX && !isFirefox) ? 'cmd+,' : 'ctrl+,'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(toolbar: TabBarToolbarRegistry): void {
|
||||
toolbar.registerItem({
|
||||
id: PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id,
|
||||
command: PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id,
|
||||
tooltip: PreferencesCommands.OPEN_USER_PREFERENCES_JSON.label,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
|
||||
protected async openPreferencesJSON(opener: string | PreferencesWidget): Promise<void> {
|
||||
const { scope, activeScopeIsFolder, uri } = this.scopeTracker.currentScope;
|
||||
const scopeID = Number(scope);
|
||||
let preferenceId = '';
|
||||
if (typeof opener === 'string') {
|
||||
preferenceId = opener;
|
||||
const currentPreferenceValue = this.preferenceService.inspect(preferenceId, uri);
|
||||
const valueInCurrentScope = Preference.getValueInScope(currentPreferenceValue, scopeID) ?? currentPreferenceValue?.defaultValue;
|
||||
this.preferenceService.set(preferenceId, valueInCurrentScope, scopeID, uri);
|
||||
}
|
||||
|
||||
let jsonEditorWidget: EditorWidget;
|
||||
const jsonUriToOpen = await this.obtainConfigUri(scopeID, activeScopeIsFolder, uri);
|
||||
if (jsonUriToOpen) {
|
||||
jsonEditorWidget = await this.editorManager.open(jsonUriToOpen);
|
||||
|
||||
if (preferenceId) {
|
||||
const text = jsonEditorWidget.editor.document.getText();
|
||||
if (preferenceId) {
|
||||
const { index } = text.match(preferenceId)!;
|
||||
const numReturns = text.slice(0, index).match(new RegExp('\n', 'g'))!.length;
|
||||
jsonEditorWidget.editor.cursor = { line: numReturns, character: 4 + preferenceId.length + 4 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async openJson(scope: PreferenceScope, resource?: string): Promise<void> {
|
||||
const jsonUriToOpen = await this.obtainConfigUri(scope, false, resource);
|
||||
if (jsonUriToOpen) {
|
||||
await this.editorManager.open(jsonUriToOpen);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts which workspace root folder to open the JSON settings.
|
||||
*/
|
||||
protected async openFolderPreferences(callback: (root: FileStat) => unknown): Promise<void> {
|
||||
const roots = this.workspaceService.tryGetRoots();
|
||||
if (roots.length === 1) {
|
||||
callback(roots[0]);
|
||||
} else {
|
||||
const items: QuickPickItem[] = roots.map(root => ({
|
||||
label: root.name,
|
||||
description: root.resource.path.fsPath(),
|
||||
execute: () => callback(root)
|
||||
}));
|
||||
this.quickInputService?.showQuickPick(items, { placeholder: 'Select workspace folder' });
|
||||
}
|
||||
}
|
||||
|
||||
private async obtainConfigUri(serializedScope: number, activeScopeIsFolder: boolean, resource?: string): Promise<URI | undefined> {
|
||||
let scope: PreferenceScope = serializedScope;
|
||||
if (activeScopeIsFolder) {
|
||||
scope = PreferenceScope.Folder;
|
||||
}
|
||||
const resourceUri = !!resource ? resource : undefined;
|
||||
const configUri = this.preferenceService.getConfigUri(scope, resourceUri);
|
||||
if (!configUri) {
|
||||
return undefined;
|
||||
}
|
||||
if (configUri && !await this.fileService.exists(configUri)) {
|
||||
await this.fileService.create(configUri);
|
||||
}
|
||||
return configUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current widget is the PreferencesWidget.
|
||||
*/
|
||||
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: PreferencesWidget) => T): T | false {
|
||||
if (widget instanceof PreferencesWidget && widget.id === PreferencesWidget.ID) {
|
||||
return fn(widget);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { JsonSchemaRegisterContext, JsonSchemaContribution, JsonSchemaDataStore } from '@theia/core/lib/browser/json-schema-store';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { PreferenceSchemaService, PreferenceConfigurations, PreferenceScope } from '@theia/core';
|
||||
import { UserStorageUri } from '@theia/userstorage/lib/browser';
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
|
||||
const PREFERENCE_URI_PREFIX = 'vscode://schemas/settings/';
|
||||
const DEBOUNCED_UPDATE_DELAY = 200;
|
||||
|
||||
@injectable()
|
||||
export class PreferencesJsonSchemaContribution implements JsonSchemaContribution {
|
||||
|
||||
@inject(PreferenceSchemaService)
|
||||
protected readonly schemaProvider: PreferenceSchemaService;
|
||||
|
||||
@inject(PreferenceConfigurations)
|
||||
protected readonly preferenceConfigurations: PreferenceConfigurations;
|
||||
|
||||
@inject(JsonSchemaDataStore)
|
||||
protected readonly jsonSchemaData: JsonSchemaDataStore;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
protected readonly debouncedUpdateInMemoryResources = debounce(() => this.updateInMemoryResources(), DEBOUNCED_UPDATE_DELAY);
|
||||
|
||||
registerSchemas(context: JsonSchemaRegisterContext): void {
|
||||
this.registerSchema(PreferenceScope.Default, context);
|
||||
this.registerSchema(PreferenceScope.User, context);
|
||||
this.registerSchema(PreferenceScope.Workspace, context);
|
||||
this.registerSchema(PreferenceScope.Folder, context);
|
||||
|
||||
context.registerSchema({
|
||||
fileMatch: `file://**/${this.preferenceConfigurations.getConfigName()}.json`,
|
||||
url: this.getSchemaURIForScope(PreferenceScope.Folder).toString()
|
||||
});
|
||||
|
||||
context.registerSchema({
|
||||
fileMatch: UserStorageUri.resolve(this.preferenceConfigurations.getConfigName() + '.json').toString(),
|
||||
url: this.getSchemaURIForScope(PreferenceScope.User).toString()
|
||||
});
|
||||
|
||||
this.workspaceService.updateSchema('settings', { $ref: this.getSchemaURIForScope(PreferenceScope.Workspace).toString() });
|
||||
this.schemaProvider.onDidChangeSchema(() => this.debouncedUpdateInMemoryResources());
|
||||
}
|
||||
|
||||
protected registerSchema(scope: PreferenceScope, context: JsonSchemaRegisterContext): void {
|
||||
const scopeStr = PreferenceScope[scope].toLowerCase();
|
||||
const uri = new URI(PREFERENCE_URI_PREFIX + scopeStr);
|
||||
|
||||
this.jsonSchemaData.setSchema(uri, (this.schemaProvider.getJSONSchema(scope)));
|
||||
}
|
||||
|
||||
protected updateInMemoryResources(): void {
|
||||
this.jsonSchemaData.setSchema(this.getSchemaURIForScope(PreferenceScope.Default),
|
||||
(this.schemaProvider.getJSONSchema(PreferenceScope.Default)));
|
||||
this.jsonSchemaData.setSchema(this.getSchemaURIForScope(PreferenceScope.User),
|
||||
this.schemaProvider.getJSONSchema(PreferenceScope.User));
|
||||
this.jsonSchemaData.setSchema(this.getSchemaURIForScope(PreferenceScope.Workspace),
|
||||
this.schemaProvider.getJSONSchema(PreferenceScope.Workspace));
|
||||
this.jsonSchemaData.setSchema(this.getSchemaURIForScope(PreferenceScope.Folder),
|
||||
this.schemaProvider.getJSONSchema(PreferenceScope.Folder));
|
||||
}
|
||||
|
||||
protected getSchemaURIForScope(scope: PreferenceScope): URI {
|
||||
return new URI(PREFERENCE_URI_PREFIX + PreferenceScope[scope].toLowerCase());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
monaco.languages.register({
|
||||
id: 'jsonc',
|
||||
'aliases': [
|
||||
'JSON with Comments'
|
||||
],
|
||||
'filenames': [
|
||||
'settings.json'
|
||||
]
|
||||
});
|
||||
479
packages/preferences/src/browser/style/index.css
Normal file
479
packages/preferences/src/browser/style/index.css
Normal file
@@ -0,0 +1,479 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2019 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
#preferences_container_widget .lm-SplitPanel-handle {
|
||||
border-right: var(--theia-border-width) solid var(--theia-editorGroup-border);
|
||||
}
|
||||
|
||||
#preferences_container_widget .lm-TabBar-tabIcon {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
line-height: var(--theia-content-line-height) !important;
|
||||
}
|
||||
|
||||
/* UI View */
|
||||
|
||||
@import url("./preference-context-menu.css");
|
||||
@import url("./preference-array.css");
|
||||
@import url("./preference-file.css");
|
||||
@import url("./preference-object.css");
|
||||
@import url("./search-input.css");
|
||||
|
||||
.theia-settings-container {
|
||||
max-width: 1000px;
|
||||
padding-top: 11px;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"tabbar tabbar"
|
||||
"navbar editor";
|
||||
grid-template-columns: minmax(150px, 280px) 1fr;
|
||||
grid-template-rows: 45px 45px 1fr;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main:not(.no-results) .settings-no-results-announcement {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main .hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-no-results-announcement {
|
||||
font-weight: bold;
|
||||
font-size: var(--theia-ui-font-size3);
|
||||
padding-left: var(--theia-ui-padding);
|
||||
margin: calc(2 * var(--theia-ui-padding)) 0px;
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-searchbar-widget {
|
||||
grid-area: header;
|
||||
margin: 3px 24px 0px 24px;
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-tabbar-widget {
|
||||
grid-area: tabbar;
|
||||
margin: 3px 24px 0px 24px;
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-tabbar-widget.with-shadow {
|
||||
box-shadow: 0px 6px 5px -5px var(--theia-widget-shadow);
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-tabbar-widget .preferences-scope-tab .lm-TabBar-tabIcon:not(.preferences-folder-dropdown-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab {
|
||||
background: var(--theia-editor-background);
|
||||
border-right: unset;
|
||||
border-bottom: var(--theia-border-width) solid var(--theia-tab-unfocusedInactiveForeground);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container .tabbar-underline {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
border-top: 1px solid var(--theia-tab-unfocusedInactiveForeground);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab {
|
||||
color: var(--theia-panelTitle-inactiveForeground);
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab:hover {
|
||||
color: var(--theia-panelTitle-activeForeground);
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab.lm-mod-current {
|
||||
color: var(--theia-panelTitle-activeForeground);
|
||||
border-bottom: var(--theia-border-width) solid var(--theia-panelTitle-activeBorder);
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab.lm-mod-current:not(.theia-mod-active) {
|
||||
border-top: unset;
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab.preferences-folder-tab .lm-TabBar-tabLabel::after {
|
||||
content: "Folder";
|
||||
padding-left: 4px;
|
||||
font-size: 0.8em;
|
||||
color: var(--theia-tab-inactiveForeground);
|
||||
}
|
||||
|
||||
#theia-main-content-panel .theia-settings-container #preferences-scope-tab-bar .preferences-scope-tab.preferences-folder-dropdown {
|
||||
position: relative;
|
||||
padding-right: 23px;
|
||||
}
|
||||
|
||||
.preferences-folder-dropdown-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
right: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-editor-widget {
|
||||
grid-area: editor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-editor-widget.full-pane {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-tree-widget {
|
||||
grid-area: navbar;
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-tree-widget .theia-mod-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.theia-settings-container .preferences-tree-widget .theia-TreeNodeSegment {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main-scroll-container {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main-sticky-misc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 0 1 50px;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main-sticky-misc .json-button>i {
|
||||
display: inline-block;
|
||||
background: var(--theia-icon-open-json) no-repeat;
|
||||
background-position-y: 1px;
|
||||
-webkit-filter: invert(1);
|
||||
filter: invert(1);
|
||||
height: var(--theia-icon-size);
|
||||
width: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-scope>label {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-section {
|
||||
padding-left: 0;
|
||||
padding-top: var(--theia-ui-padding);
|
||||
margin-top: calc(var(--theia-ui-padding) * -1);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-section a {
|
||||
border: none;
|
||||
color: var(--theia-foreground);
|
||||
font-weight: 500;
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.theia-settings-container .command-link {
|
||||
color: var(--theia-textLink-foreground);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-section a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-section-category-title {
|
||||
font-weight: bold;
|
||||
font-size: var(--theia-ui-font-size3);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-section-subcategory-title {
|
||||
font-weight: bold;
|
||||
font-size: var(--theia-ui-font-size2);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-section>li {
|
||||
list-style-type: none;
|
||||
margin: var(--theia-ui-padding) 0px;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-box-align: start;
|
||||
-ms-flex-align: start;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.theia-settings-container li.single-pref {
|
||||
list-style-type: none;
|
||||
padding: 12px 14px 18px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theia-settings-container li.single-pref p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.theia-settings-container li.single-pref:hover:not(:focus) {
|
||||
background-color: var(--theia-settings-rowHoverBackground);
|
||||
}
|
||||
|
||||
.theia-settings-container li.single-pref:focus {
|
||||
background-color: var(--theia-settings-focusedRowBackground);
|
||||
outline: 1px solid var(--theia-settings-focusedRowBorder);
|
||||
}
|
||||
|
||||
.theia-settings-container li.single-pref .pref-context-gutter {
|
||||
position: absolute;
|
||||
height: calc(100% - 36px);
|
||||
left: -22px;
|
||||
padding-right: 8px;
|
||||
border-right: 2px hidden;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-context-menu-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.theia-settings-container .single-pref:focus-within .pref-context-gutter .settings-context-menu-btn,
|
||||
.theia-settings-container .pref-name:hover+.pref-context-gutter .settings-context-menu-btn,
|
||||
.theia-settings-container .pref-context-gutter:hover .settings-context-menu-btn,
|
||||
.theia-settings-container .pref-context-gutter.show-cog .settings-context-menu-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theia-settings-container li.single-pref .pref-context-gutter.theia-mod-item-modified {
|
||||
border-right: 2px solid var(--theia-settings-modifiedItemIndicator);
|
||||
}
|
||||
|
||||
.theia-settings-container li.single-pref input[type="text"] {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-main-sticky {
|
||||
top: 0;
|
||||
padding-top: calc(var(--theia-ui-padding));
|
||||
margin-top: calc(var(--theia-ui-padding) * -1);
|
||||
background-color: var(--theia-editor-background);
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-name {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-leaf-headline-prefix {
|
||||
color: var(--theia-descriptionForeground);
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-leaf-headline-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(var(--theia-ui-padding) / 2);
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-tag {
|
||||
display: inline-block;
|
||||
padding: calc(var(--theia-ui-padding) / 8) calc(var(--theia-ui-padding) / 2);
|
||||
font-size: 0.85em;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
color: var(--theia-descriptionForeground);
|
||||
background-color: var(--theia-badge-background);
|
||||
border-radius: calc(var(--theia-ui-padding) / 4);
|
||||
border: 1px solid var(--theia-contrastBorder, transparent);
|
||||
}
|
||||
|
||||
.preferences-tree-spacer {
|
||||
padding-left: calc(var(--theia-ui-padding) / 2);
|
||||
padding-right: calc(var(--theia-ui-padding) / 2);
|
||||
min-width: var(--theia-icon-size);
|
||||
min-height: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-description {
|
||||
padding: var(--theia-ui-padding) 0;
|
||||
color: var(--theia-descriptionForeground);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-description a {
|
||||
text-decoration-line: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theia-settings-container .theia-select:focus {
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
outline-offset: -1px;
|
||||
opacity: 1 !important;
|
||||
outline-color: var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.theia-settings-container .theia-input[type="text"] {
|
||||
border: 1px solid var(--theia-dropdown-border);
|
||||
}
|
||||
|
||||
.theia-settings-container .theia-input[type="checkbox"]:focus,
|
||||
.theia-settings-container .theia-input[type="number"]:focus {
|
||||
outline-width: 1px;
|
||||
}
|
||||
|
||||
.theia-settings-container .theia-input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Remove the spinners from input[type = number] on Firefox. */
|
||||
.theia-settings-container .theia-input[type="number"] {
|
||||
-webkit-appearance: textfield;
|
||||
border: 1px solid var(--theia-dropdown-border);
|
||||
}
|
||||
|
||||
/* Remove the webkit spinners from input[type = number] on all browsers except Firefox. */
|
||||
.theia-settings-container input::-webkit-outer-spin-button,
|
||||
.theia-settings-container input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dialogContent .error:not(:empty),
|
||||
.theia-settings-container .pref-content-container .pref-input .pref-input-container .pref-error-notification {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--theia-inputValidation-errorBorder);
|
||||
background-color: var(--theia-inputValidation-errorBackground);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container .pref-input .pref-input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container a.theia-json-input {
|
||||
text-decoration: underline;
|
||||
color: var(--theia-titleBar-activeForeground);
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container a.theia-json-input:hover {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container .pref-input {
|
||||
padding: var(--theia-ui-padding) 0;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container .pref-input>select,
|
||||
.theia-settings-container .pref-content-container .pref-input>input:not([type="checkbox"]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* These specifications for the boolean class ensure that the
|
||||
checkbox is rendered to the left of the description.
|
||||
*/
|
||||
.theia-settings-container .pref-content-container.boolean {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container.boolean .pref-description {
|
||||
grid-column-start: 2;
|
||||
grid-row-start: 1;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-content-container.boolean .pref-input {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-section>li:last-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-leaf-headline-suffix {
|
||||
font-weight: normal;
|
||||
color: var(--theia-descriptionForeground);
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-leaf-headline-suffix::before {
|
||||
content: " (";
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-leaf-headline-suffix::after {
|
||||
content: ")";
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-scope-underlined {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-modified-scope-wrapper:not(:last-child)::after {
|
||||
content: ", ";
|
||||
}
|
||||
|
||||
/** Select component */
|
||||
|
||||
.theia-settings-container .theia-select-component {
|
||||
height: 28px;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
94
packages/preferences/src/browser/style/preference-array.css
Normal file
94
packages/preferences/src/browser/style/preference-array.css
Normal file
@@ -0,0 +1,94 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2020 Ericsson and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.theia-settings-container .preference-array {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-array-element {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: calc(var(--theia-ui-padding) / 2) var(--theia-ui-padding);
|
||||
border-bottom: var(--theia-panel-border) 2px solid;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-input li:nth-last-child(2) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-input li:last-child {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-array-element:hover {
|
||||
background-color: rgba(50%, 50%, 50%, 0.1);
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-array-element-btn {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theia-settings-container
|
||||
.preference-array-element
|
||||
.preference-array-element-btn {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.theia-settings-container
|
||||
.preference-array-element:hover
|
||||
.preference-array-element-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-array-element-btn:hover {
|
||||
background-color: rgba(50%, 50%, 50%, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-array .codicon.codicon-add {
|
||||
margin-left: calc((var(--theia-icon-size) + 4px) * -1);
|
||||
margin-right: 4px;
|
||||
width: var(--theia-icon-size);
|
||||
height: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-array-input {
|
||||
padding-right: calc(var(--theia-icon-size) + var(--theia-ui-padding));
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2020 Ericsson and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.theia-settings-container .settings-context-menu-container {
|
||||
position: relative;
|
||||
padding-left: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-context-menu-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-context-menu {
|
||||
position: absolute;
|
||||
width: var(--theia-settingsSidebar-width);
|
||||
list-style: none;
|
||||
padding: var(--theia-ui-padding);
|
||||
bottom: calc(100% + 10px);
|
||||
left: -10px;
|
||||
z-index: 9999;
|
||||
background-color: var(--theia-menu-background);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-context-menu:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: -10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid var(--theia-menu-background);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-context-menu li {
|
||||
padding: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-context-menu li:hover {
|
||||
background-color: var(--theia-menu-selectionBackground);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-context-menu i {
|
||||
padding-right: var(--theia-ui-padding);
|
||||
width: var(--theia-icon-size);
|
||||
display: -webkit-inline-box;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-context-menu-btn {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.theia-settings-container .pref-context-menu-btn:hover {
|
||||
background-color: rgba(50%, 50%, 50%, 0.1);
|
||||
}
|
||||
30
packages/preferences/src/browser/style/preference-file.css
Normal file
30
packages/preferences/src/browser/style/preference-file.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2022 EclipseSource and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.theia-settings-container .preference-file-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-file-input {
|
||||
flex: 1;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.theia-settings-container .preference-file-button {
|
||||
margin-left: var(--theia-ui-padding);
|
||||
}
|
||||
49
packages/preferences/src/browser/style/preference-object.css
Normal file
49
packages/preferences/src/browser/style/preference-object.css
Normal file
@@ -0,0 +1,49 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2020 Ericsson and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.theia-settings-container .object-preference-input-container {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.theia-settings-container .object-preference-input {
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
resize: none;
|
||||
color: var(--theia-settings-textInputForeground);
|
||||
background-color: var(--theia-settings-textInputBackground);
|
||||
border-color: var(--theia-panel-border);
|
||||
font-size: var(--theia-code-font-size);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.theia-settings-container .object-preference-input-btn-toggle {
|
||||
padding: 0 calc(var(--theia-ui-padding) / 2);
|
||||
}
|
||||
|
||||
.theia-settings-container .object-preference-input-btn-toggle-icon {
|
||||
display: inline-block;
|
||||
background: var(--theia-icon-open-json) no-repeat;
|
||||
background-position-y: 1px;
|
||||
height: var(--theia-icon-size);
|
||||
width: var(--theia-icon-size);
|
||||
}
|
||||
66
packages/preferences/src/browser/style/search-input.css
Normal file
66
packages/preferences/src/browser/style/search-input.css
Normal file
@@ -0,0 +1,66 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2020 Ericsson and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.theia-settings-container .settings-search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-search-container .settings-search-input {
|
||||
flex: 1;
|
||||
text-indent: 8px;
|
||||
padding: calc(var(--theia-ui-padding) / 2) 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--theia-dropdown-border);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-search-container .option-buttons {
|
||||
height: 23px;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
right: 5px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-search-container .clear-all {
|
||||
background: var(--theia-icon-clear);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-search-container .results-found {
|
||||
background-color: var(--theia-badge-background);
|
||||
border-radius: 2px;
|
||||
color: var(--theia-badge-foreground);
|
||||
padding: calc(var(--theia-ui-padding) / 5) calc(var(--theia-ui-padding) / 2);
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-search-container .option {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
margin: 0 1px;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border: var(--theia-border-width) solid transparent;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theia-settings-container .settings-search-container .enabled {
|
||||
opacity: 1;
|
||||
}
|
||||
454
packages/preferences/src/browser/util/preference-layout.ts
Normal file
454
packages/preferences/src/browser/util/preference-layout.ts
Normal file
@@ -0,0 +1,454 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { nls } from '@theia/core';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
|
||||
export interface PreferenceLayout {
|
||||
id: string;
|
||||
label: string;
|
||||
children?: PreferenceLayout[];
|
||||
settings?: string[];
|
||||
}
|
||||
|
||||
export const COMMONLY_USED_SECTION_PREFIX = 'commonly-used';
|
||||
|
||||
export const COMMONLY_USED_LAYOUT = {
|
||||
id: COMMONLY_USED_SECTION_PREFIX,
|
||||
label: nls.localizeByDefault('Commonly Used'),
|
||||
settings: [
|
||||
'files.autoSave',
|
||||
'editor.fontSize',
|
||||
'editor.fontFamily',
|
||||
'editor.tabSize',
|
||||
'editor.renderWhitespace',
|
||||
'editor.cursorStyle',
|
||||
'editor.multiCursorModifier',
|
||||
'editor.insertSpaces',
|
||||
'editor.wordWrap',
|
||||
'files.exclude',
|
||||
'files.associations'
|
||||
]
|
||||
};
|
||||
|
||||
export const DEFAULT_LAYOUT: PreferenceLayout[] = [
|
||||
{
|
||||
id: 'editor',
|
||||
label: nls.localizeByDefault('Text Editor'),
|
||||
settings: ['editor.*'],
|
||||
children: [
|
||||
{
|
||||
id: 'editor.cursor',
|
||||
label: nls.localizeByDefault('Cursor'),
|
||||
settings: ['editor.cursor*']
|
||||
},
|
||||
{
|
||||
id: 'editor.find',
|
||||
label: nls.localizeByDefault('Find'),
|
||||
settings: ['editor.find.*']
|
||||
},
|
||||
{
|
||||
id: 'editor.font',
|
||||
label: nls.localizeByDefault('Font'),
|
||||
settings: ['editor.font*']
|
||||
},
|
||||
{
|
||||
id: 'editor.format',
|
||||
label: nls.localizeByDefault('Formatting'),
|
||||
settings: ['editor.format*']
|
||||
},
|
||||
{
|
||||
id: 'editor.diffEditor',
|
||||
label: nls.localizeByDefault('Diff Editor'),
|
||||
settings: ['diffEditor.*']
|
||||
},
|
||||
{
|
||||
id: 'editor.multiDiffEditor',
|
||||
label: nls.localizeByDefault('Multi-File Diff Editor'),
|
||||
settings: ['multiDiffEditor.*']
|
||||
},
|
||||
{
|
||||
id: 'editor.minimap',
|
||||
label: nls.localizeByDefault('Minimap'),
|
||||
settings: ['editor.minimap.*']
|
||||
},
|
||||
{
|
||||
id: 'editor.suggestions',
|
||||
label: nls.localizeByDefault('Suggestions'),
|
||||
settings: ['editor.*suggest*']
|
||||
},
|
||||
{
|
||||
id: 'editor.files',
|
||||
label: nls.localizeByDefault('Files'),
|
||||
settings: ['files.*']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'workbench',
|
||||
label: nls.localizeByDefault('Workbench'),
|
||||
settings: ['workbench.*', 'workspace.*'],
|
||||
children: [
|
||||
{
|
||||
id: 'workbench.appearance',
|
||||
label: nls.localizeByDefault('Appearance'),
|
||||
settings: [
|
||||
'workbench.activityBar.*', 'workbench.*color*', 'workbench.fontAliasing', 'workbench.iconTheme', 'workbench.sidebar.location',
|
||||
'workbench.*.visible', 'workbench.tips.enabled', 'workbench.tree.*', 'workbench.view.*'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'workbench.breadcrumbs',
|
||||
label: nls.localizeByDefault('Breadcrumbs'),
|
||||
settings: ['breadcrumbs.*']
|
||||
},
|
||||
{
|
||||
id: 'workbench.editor',
|
||||
label: nls.localizeByDefault('Editor Management'),
|
||||
settings: ['workbench.editor.*']
|
||||
},
|
||||
{
|
||||
id: 'workbench.settings',
|
||||
label: nls.localizeByDefault('Settings Editor'),
|
||||
settings: ['workbench.settings.*']
|
||||
},
|
||||
{
|
||||
id: 'workbench.zenmode',
|
||||
label: nls.localizeByDefault('Zen Mode'),
|
||||
settings: ['zenmode.*']
|
||||
},
|
||||
{
|
||||
id: 'workbench.screencastmode',
|
||||
label: nls.localizeByDefault('Screencast Mode'),
|
||||
settings: ['screencastMode.*']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'window',
|
||||
label: nls.localizeByDefault('Window'),
|
||||
settings: ['window.*'],
|
||||
children: [
|
||||
{
|
||||
id: 'window.newWindow',
|
||||
label: nls.localizeByDefault('New Window'),
|
||||
settings: ['window.*newwindow*']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'features',
|
||||
label: nls.localizeByDefault('Features'),
|
||||
children: [
|
||||
{
|
||||
id: 'features.accessibilitySignals',
|
||||
label: nls.localizeByDefault('Accessibility Signals'),
|
||||
settings: ['accessibility.signal*']
|
||||
},
|
||||
{
|
||||
id: 'features.accessibility',
|
||||
label: nls.localizeByDefault('Accessibility'),
|
||||
settings: ['accessibility.*']
|
||||
},
|
||||
{
|
||||
id: 'features.explorer',
|
||||
label: nls.localizeByDefault('Explorer'),
|
||||
settings: ['explorer.*', 'outline.*']
|
||||
},
|
||||
{
|
||||
id: 'features.search',
|
||||
label: nls.localizeByDefault('Search'),
|
||||
settings: ['search.*']
|
||||
},
|
||||
{
|
||||
id: 'features.debug',
|
||||
label: nls.localizeByDefault('Debug'),
|
||||
settings: ['debug.*', 'launch']
|
||||
},
|
||||
{
|
||||
id: 'features.testing',
|
||||
label: nls.localizeByDefault('Testing'),
|
||||
settings: ['testing.*']
|
||||
},
|
||||
{
|
||||
id: 'features.scm',
|
||||
label: nls.localizeByDefault('Source Control'),
|
||||
settings: ['scm.*']
|
||||
},
|
||||
{
|
||||
id: 'features.extensions',
|
||||
label: nls.localizeByDefault('Extensions'),
|
||||
settings: ['extensions.*']
|
||||
},
|
||||
{
|
||||
id: 'features.terminal',
|
||||
label: nls.localizeByDefault('Terminal'),
|
||||
settings: ['terminal.*']
|
||||
},
|
||||
{
|
||||
id: 'features.task',
|
||||
label: nls.localizeByDefault('Task'),
|
||||
settings: ['task.*']
|
||||
},
|
||||
{
|
||||
id: 'features.problems',
|
||||
label: nls.localizeByDefault('Problems'),
|
||||
settings: ['problems.*']
|
||||
},
|
||||
{
|
||||
id: 'features.output',
|
||||
label: nls.localizeByDefault('Output'),
|
||||
settings: ['output.*']
|
||||
},
|
||||
{
|
||||
id: 'features.comments',
|
||||
label: nls.localizeByDefault('Comments'),
|
||||
settings: ['comments.*']
|
||||
},
|
||||
{
|
||||
id: 'features.remote',
|
||||
label: nls.localizeByDefault('Remote'),
|
||||
settings: ['remote.*']
|
||||
},
|
||||
{
|
||||
id: 'features.timeline',
|
||||
label: nls.localizeByDefault('Timeline'),
|
||||
settings: ['timeline.*']
|
||||
},
|
||||
{
|
||||
id: 'features.toolbar',
|
||||
label: nls.localize('theia/preferences/toolbar', 'Toolbar'),
|
||||
settings: ['toolbar.*']
|
||||
},
|
||||
{
|
||||
id: 'features.notebook',
|
||||
label: nls.localizeByDefault('Notebook'),
|
||||
settings: ['notebook.*', 'interactiveWindow.*']
|
||||
},
|
||||
{
|
||||
id: 'features.mergeEditor',
|
||||
label: nls.localizeByDefault('Merge Editor'),
|
||||
settings: ['mergeEditor.*']
|
||||
},
|
||||
{
|
||||
id: 'features.chat',
|
||||
label: nls.localizeByDefault('Chat'),
|
||||
settings: ['chat.*', 'inlineChat.*']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'application',
|
||||
label: nls.localizeByDefault('Application'),
|
||||
children: [
|
||||
{
|
||||
id: 'application.http',
|
||||
label: nls.localizeByDefault('HTTP'),
|
||||
settings: ['http.*']
|
||||
},
|
||||
{
|
||||
id: 'application.keyboard',
|
||||
label: nls.localizeByDefault('Keyboard'),
|
||||
settings: ['keyboard.*']
|
||||
},
|
||||
{
|
||||
id: 'application.update',
|
||||
label: nls.localizeByDefault('Update'),
|
||||
settings: ['update.*']
|
||||
},
|
||||
{
|
||||
id: 'application.telemetry',
|
||||
label: nls.localizeByDefault('Telemetry'),
|
||||
settings: ['telemetry.*']
|
||||
},
|
||||
{
|
||||
id: 'application.settingsSync',
|
||||
label: nls.localizeByDefault('Settings Sync'),
|
||||
settings: ['settingsSync.*']
|
||||
},
|
||||
{
|
||||
id: 'application.experimental',
|
||||
label: nls.localizeByDefault('Experimental'),
|
||||
settings: ['application.experimental.*']
|
||||
},
|
||||
{
|
||||
id: 'application.other',
|
||||
label: nls.localizeByDefault('Other'),
|
||||
settings: ['application.*']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: nls.localizeByDefault('Security'),
|
||||
settings: ['security.*'],
|
||||
children: [
|
||||
{
|
||||
id: 'security.workspace',
|
||||
label: nls.localizeByDefault('Workspace'),
|
||||
settings: ['security.workspace.*']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'ai-features',
|
||||
label: nls.localize('theia/preferences/ai-features', 'AI Features'),
|
||||
children: [
|
||||
{
|
||||
id: 'ai-features.aiEnablement',
|
||||
label: nls.localize('theia/preferences/ai-features/ai-enable', 'AI Enablement'),
|
||||
settings: ['ai-features.AiEnable.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.anthropic',
|
||||
label: 'Anthropic',
|
||||
settings: ['ai-features.anthropic.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.chat',
|
||||
label: nls.localizeByDefault('Chat'),
|
||||
settings: ['ai-features.chat.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.codeCompletion',
|
||||
label: nls.localize('theia/preferences/ai-features/code-completion', 'Code Completion'),
|
||||
settings: ['ai-features.codeCompletion.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.huggingFace',
|
||||
label: 'Hugging Face',
|
||||
settings: ['ai-features.huggingFace.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.mcp',
|
||||
label: nls.localize('theia/preferences/ai-features/MCP', 'MCP'),
|
||||
settings: ['ai-features.mcp.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.modelSettings',
|
||||
label: nls.localize('theia/preferences/ai-features/model-settings', 'Model Settings'),
|
||||
settings: ['ai-features.modelSettings.*', 'ai-features.languageModelAliases']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.ollama',
|
||||
label: 'Ollama',
|
||||
settings: ['ai-features.ollama']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.llamafile',
|
||||
label: 'Llamafile',
|
||||
settings: ['ai-features.llamafile.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.openAiCustom',
|
||||
label: nls.localize('theia/preferences/ai-features/open-ai-custom', '{0} Custom Models', 'Open AI'),
|
||||
settings: ['ai-features.openAiCustom.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.openAiOfficial',
|
||||
label: nls.localize('theia/preferences/ai-features/open-ai-official', '{0} Official Models', 'Open AI'),
|
||||
settings: ['ai-features.openAiOfficial.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.promptTemplates',
|
||||
label: nls.localize('theia/preferences/ai-features/promptTemplates', 'Prompt Templates'),
|
||||
settings: ['ai-features.promptTemplates.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.SCANOSS',
|
||||
label: 'SCANOSS',
|
||||
settings: ['ai-features.SCANOSS.*']
|
||||
},
|
||||
{
|
||||
id: 'ai-features.workspaceFunctions',
|
||||
label: nls.localize('theia/preferences/ai-features/workspace-functions', 'Workspace Functions'),
|
||||
settings: ['ai-features.workspaceFunctions.*']
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
id: 'extensions',
|
||||
label: nls.localizeByDefault('Extensions'),
|
||||
children: [
|
||||
{
|
||||
id: 'extensions.hosted-plugin',
|
||||
label: nls.localize('theia/preferences/hostedPlugin', 'Hosted Plugin'),
|
||||
settings: ['hosted-plugin.*']
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@injectable()
|
||||
export class PreferenceLayoutProvider {
|
||||
|
||||
getLayout(): PreferenceLayout[] {
|
||||
return DEFAULT_LAYOUT;
|
||||
}
|
||||
|
||||
getCommonlyUsedLayout(): PreferenceLayout {
|
||||
return COMMONLY_USED_LAYOUT;
|
||||
}
|
||||
|
||||
hasCategory(id: string): boolean {
|
||||
return [...this.getLayout(), this.getCommonlyUsedLayout()].some(e => e.id === id);
|
||||
}
|
||||
|
||||
getLayoutForPreference(preferenceId: string): PreferenceLayout | undefined {
|
||||
const layout = this.getLayout();
|
||||
for (const section of layout) {
|
||||
const item = this.findItemInSection(section, preferenceId);
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected findItemInSection(section: PreferenceLayout, preferenceId: string): PreferenceLayout | undefined {
|
||||
// First check whether any of its children match the preferenceId.
|
||||
if (section.children) {
|
||||
for (const child of section.children) {
|
||||
const item = this.findItemInSection(child, preferenceId);
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Then check whether the section itself matches the preferenceId.
|
||||
if (section.settings) {
|
||||
for (const setting of section.settings) {
|
||||
if (this.matchesSetting(preferenceId, setting)) {
|
||||
return section;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected matchesSetting(preferenceId: string, setting: string): boolean {
|
||||
if (setting.includes('*')) {
|
||||
return this.createRegExp(setting).test(preferenceId);
|
||||
}
|
||||
return preferenceId === setting;
|
||||
}
|
||||
|
||||
protected createRegExp(setting: string): RegExp {
|
||||
return new RegExp(`^${setting.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { LabelProvider, codicon } from '@theia/core/lib/browser';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { CommandRegistry, MenuModelRegistry, Command, PreferenceScope } from '@theia/core/lib/common';
|
||||
import { Preference, PreferenceMenus } from './preference-types';
|
||||
|
||||
/**
|
||||
* @deprecated since 1.17.0 moved to PreferenceMenus namespace.
|
||||
*/
|
||||
export const FOLDER_SCOPE_MENU_PATH = PreferenceMenus.FOLDER_SCOPE_MENU_PATH;
|
||||
|
||||
/**
|
||||
* @deprecated since 1.17.0. This work is now done in the PreferenceScopeTabbarWidget.
|
||||
*/
|
||||
@injectable()
|
||||
export class PreferenceScopeCommandManager {
|
||||
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry) protected readonly menuModelRegistry: MenuModelRegistry;
|
||||
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
|
||||
|
||||
protected foldersAsCommands: Command[] = [];
|
||||
|
||||
createFolderWorkspacesMenu(
|
||||
folderWorkspaces: FileStat[],
|
||||
currentFolderURI?: string,
|
||||
): void {
|
||||
this.foldersAsCommands.forEach(folderCommand => {
|
||||
this.menuModelRegistry.unregisterMenuAction(folderCommand, FOLDER_SCOPE_MENU_PATH);
|
||||
this.commandRegistry.unregisterCommand(folderCommand);
|
||||
});
|
||||
this.foldersAsCommands.length = 0;
|
||||
|
||||
folderWorkspaces.forEach(folderWorkspace => {
|
||||
const folderLabel = this.labelProvider.getName(folderWorkspace.resource);
|
||||
|
||||
const iconClass = currentFolderURI === folderWorkspace.resource.toString() ? codicon('pass') : '';
|
||||
const newFolderAsCommand = {
|
||||
id: `preferenceScopeCommand:${folderWorkspace.resource.toString()}`,
|
||||
label: folderLabel,
|
||||
iconClass: iconClass
|
||||
};
|
||||
|
||||
this.foldersAsCommands.push(newFolderAsCommand);
|
||||
|
||||
this.commandRegistry.registerCommand(newFolderAsCommand, {
|
||||
isVisible: (callback, check) => check === 'from-tabbar',
|
||||
isEnabled: (callback, check) => check === 'from-tabbar',
|
||||
execute: (callback: (scopeDetails: Preference.SelectedScopeDetails) => void) => {
|
||||
callback({ scope: PreferenceScope.Folder, uri: folderWorkspace.resource.toString(), activeScopeIsFolder: true });
|
||||
}
|
||||
});
|
||||
|
||||
this.menuModelRegistry.registerMenuAction(FOLDER_SCOPE_MENU_PATH, {
|
||||
commandId: newFolderAsCommand.id,
|
||||
label: newFolderAsCommand.label
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser';
|
||||
import { Emitter, OVERRIDE_PROPERTY_PATTERN, PreferenceConfigurations, PreferenceDataProperty, PreferenceSchemaService } from '@theia/core';
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
import { Preference } from './preference-types';
|
||||
import { COMMONLY_USED_SECTION_PREFIX, PreferenceLayoutProvider } from './preference-layout';
|
||||
import { PreferenceTreeLabelProvider } from './preference-tree-label-provider';
|
||||
|
||||
export interface CreatePreferencesGroupOptions {
|
||||
id: string,
|
||||
group: string,
|
||||
root: CompositeTreeNode,
|
||||
expanded?: boolean,
|
||||
depth?: number,
|
||||
label?: string
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceTreeGenerator {
|
||||
|
||||
@inject(PreferenceSchemaService) protected readonly schemaProvider: PreferenceSchemaService;
|
||||
@inject(PreferenceConfigurations) protected readonly preferenceConfigs: PreferenceConfigurations;
|
||||
@inject(PreferenceLayoutProvider) protected readonly layoutProvider: PreferenceLayoutProvider;
|
||||
@inject(PreferenceTreeLabelProvider) protected readonly labelProvider: PreferenceTreeLabelProvider;
|
||||
|
||||
protected _root: CompositeTreeNode;
|
||||
protected _idCache = new Map<string, string>();
|
||||
|
||||
protected readonly onSchemaChangedEmitter = new Emitter<CompositeTreeNode>();
|
||||
readonly onSchemaChanged = this.onSchemaChangedEmitter.event;
|
||||
protected readonly defaultTopLevelCategory = 'extensions';
|
||||
|
||||
get root(): CompositeTreeNode {
|
||||
return this._root ?? this.generateTree();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
this.schemaProvider.onDidChangeSchema(() => this.handleChangedSchema());
|
||||
this.handleChangedSchema();
|
||||
}
|
||||
|
||||
generateTree(): CompositeTreeNode {
|
||||
this._idCache.clear();
|
||||
const properties = this.schemaProvider.getSchemaProperties();
|
||||
const groups = new Map<string, Preference.CompositeTreeNode>();
|
||||
const root = this.createRootNode();
|
||||
|
||||
const commonlyUsedLayout = this.layoutProvider.getCommonlyUsedLayout();
|
||||
const commonlyUsed = this.getOrCreatePreferencesGroup({
|
||||
id: commonlyUsedLayout.id,
|
||||
group: commonlyUsedLayout.id,
|
||||
root,
|
||||
groups,
|
||||
label: commonlyUsedLayout.label
|
||||
});
|
||||
|
||||
for (const layout of this.layoutProvider.getLayout()) {
|
||||
this.getOrCreatePreferencesGroup({
|
||||
id: layout.id,
|
||||
group: layout.id,
|
||||
root,
|
||||
groups,
|
||||
label: layout.label
|
||||
});
|
||||
}
|
||||
for (const preference of commonlyUsedLayout.settings ?? []) {
|
||||
if (properties.has(preference)) {
|
||||
this.createLeafNode(preference, commonlyUsed, properties.get(preference)!);
|
||||
}
|
||||
}
|
||||
for (const [propertyName, property] of properties.entries()) {
|
||||
if (!property.hidden && !property.deprecationMessage && !this.preferenceConfigs.isSectionName(propertyName) && !OVERRIDE_PROPERTY_PATTERN.test(propertyName)) {
|
||||
if (property.owner) {
|
||||
this.createPluginLeafNode(propertyName, property, root, groups);
|
||||
} else {
|
||||
this.createBuiltinLeafNode(propertyName, property, root, groups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of groups.values()) {
|
||||
if (group.id !== `${COMMONLY_USED_SECTION_PREFIX}@${COMMONLY_USED_SECTION_PREFIX}`) {
|
||||
(group.children as Preference.TreeNode[]).sort((a, b) => {
|
||||
const aIsComposite = CompositeTreeNode.is(a);
|
||||
const bIsComposite = CompositeTreeNode.is(b);
|
||||
if (aIsComposite && !bIsComposite) {
|
||||
return 1;
|
||||
}
|
||||
if (bIsComposite && !aIsComposite) {
|
||||
return -1;
|
||||
}
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._root = root;
|
||||
return root;
|
||||
};
|
||||
|
||||
protected createBuiltinLeafNode(name: string, property: PreferenceDataProperty, root: CompositeTreeNode, groups: Map<string, Preference.CompositeTreeNode>): void {
|
||||
const { immediateParent, topLevelParent } = this.getParents(name, root, groups);
|
||||
this.createLeafNode(name, immediateParent || topLevelParent, property);
|
||||
}
|
||||
|
||||
protected createPluginLeafNode(name: string, property: PreferenceDataProperty, root: CompositeTreeNode, groups: Map<string, Preference.CompositeTreeNode>): void {
|
||||
if (!property.owner) {
|
||||
return;
|
||||
}
|
||||
const groupID = this.defaultTopLevelCategory;
|
||||
const subgroupName = property.owner;
|
||||
const subsubgroupName = property.group;
|
||||
const hasGroup = Boolean(subsubgroupName);
|
||||
const toplevelParent = this.getOrCreatePreferencesGroup({
|
||||
id: groupID,
|
||||
group: groupID,
|
||||
root,
|
||||
groups
|
||||
});
|
||||
const subgroupID = [groupID, subgroupName].join('.');
|
||||
const subgroupParent = this.getOrCreatePreferencesGroup({
|
||||
id: subgroupID,
|
||||
group: groupID,
|
||||
root: toplevelParent,
|
||||
groups,
|
||||
expanded: hasGroup,
|
||||
label: subgroupName
|
||||
});
|
||||
const subsubgroupID = [groupID, subgroupName, subsubgroupName].join('.');
|
||||
const subsubgroupParent = hasGroup ? this.getOrCreatePreferencesGroup({
|
||||
id: subsubgroupID,
|
||||
group: subgroupID,
|
||||
root: subgroupParent,
|
||||
groups,
|
||||
depth: 2,
|
||||
label: subsubgroupName
|
||||
}) : undefined;
|
||||
this.createLeafNode(name, subsubgroupParent || subgroupParent, property);
|
||||
}
|
||||
|
||||
getNodeId(preferenceId: string): string {
|
||||
return this._idCache.get(preferenceId) ?? '';
|
||||
}
|
||||
|
||||
protected getParents(
|
||||
name: string, root: CompositeTreeNode, groups: Map<string, Preference.CompositeTreeNode>
|
||||
): {
|
||||
topLevelParent: Preference.CompositeTreeNode, immediateParent: Preference.CompositeTreeNode | undefined
|
||||
} {
|
||||
const layoutItem = this.layoutProvider.getLayoutForPreference(name);
|
||||
const labels = (layoutItem?.id ?? name).split('.');
|
||||
const groupID = this.getGroupName(labels);
|
||||
const subgroupName = groupID !== labels[0]
|
||||
? labels[0]
|
||||
// If a layout item is present, any additional segments are sections
|
||||
// If not, then the name describes a leaf node and only non-final segments are sections.
|
||||
: layoutItem || labels.length > 2
|
||||
? labels.at(1)
|
||||
: undefined;
|
||||
const topLevelParent = this.getOrCreatePreferencesGroup({
|
||||
id: groupID,
|
||||
group: groupID,
|
||||
root,
|
||||
groups,
|
||||
label: this.generateName(groupID)
|
||||
});
|
||||
const immediateParent = subgroupName ? this.getOrCreatePreferencesGroup({
|
||||
id: [groupID, subgroupName].join('.'),
|
||||
group: groupID,
|
||||
root: topLevelParent,
|
||||
groups,
|
||||
label: layoutItem?.label ?? this.generateName(subgroupName)
|
||||
}) : undefined;
|
||||
return { immediateParent, topLevelParent };
|
||||
}
|
||||
|
||||
protected getGroupName(labels: string[]): string {
|
||||
const defaultGroup = labels[0];
|
||||
if (this.layoutProvider.hasCategory(defaultGroup)) {
|
||||
return defaultGroup;
|
||||
}
|
||||
return this.defaultTopLevelCategory;
|
||||
}
|
||||
|
||||
protected getSubgroupName(labels: string[], computedGroupName: string): string | undefined {
|
||||
if (computedGroupName !== labels[0]) {
|
||||
return labels[0];
|
||||
} else if (labels.length > 1) {
|
||||
return labels[1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected generateName(id: string): string {
|
||||
return this.labelProvider.formatString(id);
|
||||
}
|
||||
|
||||
doHandleChangedSchema(): void {
|
||||
const newTree = this.generateTree();
|
||||
this.onSchemaChangedEmitter.fire(newTree);
|
||||
}
|
||||
|
||||
handleChangedSchema = debounce(this.doHandleChangedSchema, 200);
|
||||
|
||||
protected createRootNode(): CompositeTreeNode {
|
||||
return {
|
||||
id: 'root-node-id',
|
||||
name: '',
|
||||
parent: undefined,
|
||||
visible: true,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
protected createLeafNode(property: string, preferencesGroup: Preference.CompositeTreeNode, data: PreferenceDataProperty): Preference.LeafNode {
|
||||
const { group } = Preference.TreeNode.getGroupAndIdFromNodeId(preferencesGroup.id);
|
||||
const newNode: Preference.LeafNode = {
|
||||
id: `${group}@${property}`,
|
||||
preferenceId: property,
|
||||
parent: preferencesGroup,
|
||||
preference: { data },
|
||||
depth: Preference.TreeNode.isTopLevel(preferencesGroup) ? 1 : 2
|
||||
};
|
||||
this._idCache.set(property, newNode.id);
|
||||
CompositeTreeNode.addChild(preferencesGroup, newNode);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
protected createPreferencesGroup(options: CreatePreferencesGroupOptions): Preference.CompositeTreeNode {
|
||||
const newNode: Preference.CompositeTreeNode = {
|
||||
id: `${options.group}@${options.id}`,
|
||||
visible: true,
|
||||
parent: options.root,
|
||||
children: [],
|
||||
expanded: false,
|
||||
selected: false,
|
||||
depth: 0,
|
||||
label: options.label
|
||||
};
|
||||
const isTopLevel = Preference.TreeNode.isTopLevel(newNode);
|
||||
if (!(options.expanded ?? isTopLevel)) {
|
||||
delete newNode.expanded;
|
||||
}
|
||||
newNode.depth = options.depth ?? (isTopLevel ? 0 : 1);
|
||||
CompositeTreeNode.addChild(options.root, newNode);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
protected getOrCreatePreferencesGroup(options: CreatePreferencesGroupOptions & { groups: Map<string, Preference.CompositeTreeNode> }): Preference.CompositeTreeNode {
|
||||
const existingGroup = options.groups.get(options.id);
|
||||
if (existingGroup) { return existingGroup; }
|
||||
const newNode = this.createPreferencesGroup(options);
|
||||
options.groups.set(options.id, newNode);
|
||||
return newNode;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
const disableJSDOM = enableJSDOM();
|
||||
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
FrontendApplicationConfigProvider.set({});
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { Container } from '@theia/core/shared/inversify';
|
||||
import { PreferenceTreeGenerator } from './preference-tree-generator';
|
||||
import { PreferenceTreeLabelProvider } from './preference-tree-label-provider';
|
||||
import { Preference } from './preference-types';
|
||||
import { SelectableTreeNode } from '@theia/core/lib/browser';
|
||||
import { PreferenceLayoutProvider } from './preference-layout';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
describe('preference-tree-label-provider', () => {
|
||||
|
||||
let preferenceTreeLabelProvider: PreferenceTreeLabelProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
const container = new Container();
|
||||
container.bind(PreferenceLayoutProvider).toSelf().inSingletonScope();
|
||||
container.bind<any>(PreferenceTreeGenerator).toConstantValue({ getCustomLabelFor: () => { } });
|
||||
preferenceTreeLabelProvider = container.resolve(PreferenceTreeLabelProvider);
|
||||
});
|
||||
|
||||
it('PreferenceTreeLabelProvider.format', () => {
|
||||
const testString = 'aaaBbbCcc Dddd eee';
|
||||
expect(preferenceTreeLabelProvider['formatString'](testString)).eq('Aaa Bbb Ccc Dddd eee');
|
||||
});
|
||||
|
||||
it('PreferenceTreeLabelProvider.format.Chinese', () => {
|
||||
const testString = '某個設定/某个设定';
|
||||
expect(preferenceTreeLabelProvider['formatString'](testString)).eq('某個設定/某个设定');
|
||||
});
|
||||
|
||||
it('PreferenceTreeLabelProvider.format.Danish', () => {
|
||||
const testString = 'indstillingPåEnØ';
|
||||
expect(preferenceTreeLabelProvider['formatString'](testString)).eq('Indstilling På En Ø');
|
||||
});
|
||||
|
||||
it('PreferenceTreeLabelProvider.format.Greek', () => {
|
||||
const testString = 'κάποιαΡύθμιση';
|
||||
expect(preferenceTreeLabelProvider['formatString'](testString)).eq('Κάποια Ρύθμιση');
|
||||
});
|
||||
|
||||
it('PreferenceTreeLabelProvider.format.Russian', () => {
|
||||
const testString = 'некоторыеНастройки';
|
||||
expect(preferenceTreeLabelProvider['formatString'](testString)).eq('Некоторые Настройки');
|
||||
});
|
||||
|
||||
it('PreferenceTreeLabelProvider.format.Armenian', () => {
|
||||
const testString = 'ինչ-որՊարամետր';
|
||||
expect(preferenceTreeLabelProvider['formatString'](testString)).eq('Ինչ-որ Պարամետր');
|
||||
});
|
||||
|
||||
it('PreferenceTreeLabelProvider.format.specialCharacters', () => {
|
||||
const testString = 'hyphenated-wordC++Setting';
|
||||
expect(preferenceTreeLabelProvider['formatString'](testString)).eq('Hyphenated-word C++ Setting');
|
||||
});
|
||||
|
||||
describe('PreferenceTreeLabelProvider.createLeafNode', () => {
|
||||
it('when property constructs of three parts the third part is the leaf', () => {
|
||||
const property = 'category-name.subcategory.leaf';
|
||||
const expectedName = 'Leaf';
|
||||
testLeafName(property, expectedName);
|
||||
});
|
||||
|
||||
it('when property constructs of two parts the second part is the leaf', () => {
|
||||
const property = 'category-name.leaf';
|
||||
const expectedName = 'Leaf';
|
||||
testLeafName(property, expectedName);
|
||||
});
|
||||
|
||||
function testLeafName(property: string, expectedName: string): void {
|
||||
|
||||
const expectedSelectableTreeNode: Preference.LeafNode & SelectableTreeNode = {
|
||||
id: `group@${property}`,
|
||||
parent: undefined,
|
||||
visible: true,
|
||||
selected: false,
|
||||
depth: 2,
|
||||
preferenceId: property,
|
||||
preference: { data: {} }
|
||||
};
|
||||
|
||||
expect(preferenceTreeLabelProvider['getName'](expectedSelectableTreeNode)).deep.eq(expectedName);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { LabelProviderContribution, TreeNode } from '@theia/core/lib/browser';
|
||||
import { Preference } from './preference-types';
|
||||
import { PreferenceLayoutProvider } from './preference-layout';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceTreeLabelProvider implements LabelProviderContribution {
|
||||
|
||||
@inject(PreferenceLayoutProvider)
|
||||
protected readonly layoutProvider: PreferenceLayoutProvider;
|
||||
|
||||
canHandle(element: object): number {
|
||||
return TreeNode.is(element) && Preference.TreeNode.is(element) ? 150 : 0;
|
||||
}
|
||||
|
||||
getName(node: Preference.TreeNode): string {
|
||||
if (Preference.TreeNode.is(node) && node.label) {
|
||||
return node.label;
|
||||
}
|
||||
const { id } = Preference.TreeNode.getGroupAndIdFromNodeId(node.id);
|
||||
const labels = id.split('.');
|
||||
const groupName = labels[labels.length - 1];
|
||||
return this.formatString(groupName);
|
||||
}
|
||||
|
||||
getPrefix(node: Preference.TreeNode, fullPath = false): string | undefined {
|
||||
const { depth } = node;
|
||||
const { id, group } = Preference.TreeNode.getGroupAndIdFromNodeId(node.id);
|
||||
const segments = id.split('.');
|
||||
const segmentsHandled = group === segments[0] ? depth : depth - 1;
|
||||
segments.pop(); // Ignore the leaf name.
|
||||
const prefixSegments = fullPath ? segments : segments.slice(segmentsHandled);
|
||||
if (prefixSegments.length) {
|
||||
let output = prefixSegments.length > 1 ? `${this.formatString(prefixSegments[0])} › ` : `${this.formatString(prefixSegments[0])}: `;
|
||||
for (const segment of prefixSegments.slice(1)) {
|
||||
output += `${this.formatString(segment)}: `;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
formatString(string: string): string {
|
||||
let formattedString = string[0].toLocaleUpperCase();
|
||||
for (let i = 1; i < string.length; i++) {
|
||||
if (this.isUpperCase(string[i]) && !/\s/.test(string[i - 1]) && !this.isUpperCase(string[i - 1])) {
|
||||
formattedString += ' ';
|
||||
}
|
||||
formattedString += string[i];
|
||||
}
|
||||
return formattedString.trim();
|
||||
}
|
||||
|
||||
protected isUpperCase(char: string): boolean {
|
||||
return char === char.toLocaleUpperCase() && char.toLocaleLowerCase() !== char.toLocaleUpperCase();
|
||||
}
|
||||
}
|
||||
174
packages/preferences/src/browser/util/preference-types.ts
Normal file
174
packages/preferences/src/browser/util/preference-types.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import {
|
||||
TreeNode as BaseTreeNode,
|
||||
CompositeTreeNode as BaseCompositeTreeNode,
|
||||
SelectableTreeNode,
|
||||
CommonCommands,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { Command, MenuPath, PreferenceDataProperty, PreferenceInspection, PreferenceScope } from '@theia/core';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { JsonType } from '@theia/core/lib/common/json-schema';
|
||||
|
||||
export namespace Preference {
|
||||
|
||||
export interface EditorCommandArgs {
|
||||
id: string;
|
||||
value: string | undefined;
|
||||
}
|
||||
|
||||
export namespace EditorCommandArgs {
|
||||
export function is(prefObject: EditorCommandArgs): prefObject is EditorCommandArgs {
|
||||
return !!prefObject && 'id' in prefObject && 'value' in prefObject;
|
||||
}
|
||||
}
|
||||
|
||||
export const Node = Symbol('Preference.Node');
|
||||
export type Node = TreeNode;
|
||||
|
||||
export type TreeNode = CompositeTreeNode | LeafNode;
|
||||
|
||||
export namespace TreeNode {
|
||||
export const is = (node: BaseTreeNode | TreeNode): node is TreeNode => 'depth' in node;
|
||||
export const isTopLevel = (node: BaseTreeNode): boolean => {
|
||||
const { group, id } = getGroupAndIdFromNodeId(node.id);
|
||||
return group === id;
|
||||
};
|
||||
export const getGroupAndIdFromNodeId = (nodeId: string): { group: string; id: string } => {
|
||||
const separator = nodeId.indexOf('@');
|
||||
const group = nodeId.substring(0, separator);
|
||||
const id = nodeId.substring(separator + 1, nodeId.length);
|
||||
return { group, id };
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompositeTreeNode extends BaseCompositeTreeNode, SelectableTreeNode {
|
||||
expanded?: boolean;
|
||||
depth: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export namespace CompositeTreeNode {
|
||||
export const is = (node: TreeNode): node is CompositeTreeNode => !LeafNode.is(node);
|
||||
}
|
||||
|
||||
export interface LeafNode extends BaseTreeNode {
|
||||
label?: string;
|
||||
depth: number;
|
||||
preference: { data: PreferenceDataProperty };
|
||||
preferenceId: string;
|
||||
}
|
||||
|
||||
export namespace LeafNode {
|
||||
export const is = (node: BaseTreeNode | LeafNode): node is LeafNode => 'preference' in node && !!node.preference.data;
|
||||
|
||||
export const getType = (node: BaseTreeNode | LeafNode): JsonType | undefined => is(node)
|
||||
? Array.isArray(node.preference.data.type) ? node.preference.data.type[0] : node.preference.data.type
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export const getValueInScope = <T extends JSONValue>(preferenceInfo: PreferenceInspection<T> | undefined, scope: number): T | undefined => {
|
||||
if (!preferenceInfo) {
|
||||
return undefined;
|
||||
}
|
||||
switch (scope) {
|
||||
case PreferenceScope.User:
|
||||
return preferenceInfo.globalValue;
|
||||
case PreferenceScope.Workspace:
|
||||
return preferenceInfo.workspaceValue;
|
||||
case PreferenceScope.Folder:
|
||||
return preferenceInfo.workspaceFolderValue;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export interface SelectedScopeDetails {
|
||||
scope: number;
|
||||
uri: string | undefined;
|
||||
activeScopeIsFolder: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_SCOPE: SelectedScopeDetails = {
|
||||
scope: PreferenceScope.User,
|
||||
uri: undefined,
|
||||
activeScopeIsFolder: false
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export namespace PreferencesCommands {
|
||||
export const OPEN_PREFERENCES_JSON_TOOLBAR: Command = {
|
||||
id: 'preferences:openJson.toolbar',
|
||||
iconClass: 'codicon codicon-json'
|
||||
};
|
||||
export const COPY_JSON_NAME = Command.toDefaultLocalizedCommand({
|
||||
id: 'preferences:copyJson.name',
|
||||
label: 'Copy Setting ID'
|
||||
});
|
||||
export const RESET_PREFERENCE = Command.toDefaultLocalizedCommand({
|
||||
id: 'preferences:reset',
|
||||
label: 'Reset Setting'
|
||||
});
|
||||
|
||||
export const COPY_JSON_VALUE = Command.toDefaultLocalizedCommand({
|
||||
id: 'preferences:copyJson.value',
|
||||
label: 'Copy Setting as JSON',
|
||||
});
|
||||
|
||||
export const OPEN_USER_PREFERENCES = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.openGlobalSettings',
|
||||
category: CommonCommands.PREFERENCES_CATEGORY,
|
||||
label: 'Open User Settings',
|
||||
});
|
||||
|
||||
export const OPEN_WORKSPACE_PREFERENCES = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.openWorkspaceSettings',
|
||||
category: CommonCommands.PREFERENCES_CATEGORY,
|
||||
label: 'Open Workspace Settings',
|
||||
});
|
||||
|
||||
export const OPEN_FOLDER_PREFERENCES = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.openFolderSettings',
|
||||
category: CommonCommands.PREFERENCES_CATEGORY,
|
||||
label: 'Open Folder Settings'
|
||||
});
|
||||
|
||||
export const OPEN_USER_PREFERENCES_JSON = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.openSettingsJson',
|
||||
category: CommonCommands.PREFERENCES_CATEGORY,
|
||||
label: 'Open Settings (JSON)'
|
||||
});
|
||||
|
||||
export const OPEN_WORKSPACE_PREFERENCES_JSON = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.openWorkspaceSettingsFile',
|
||||
category: CommonCommands.PREFERENCES_CATEGORY,
|
||||
label: 'Open Workspace Settings (JSON)',
|
||||
});
|
||||
|
||||
export const OPEN_FOLDER_PREFERENCES_JSON = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.openFolderSettingsFile',
|
||||
category: CommonCommands.PREFERENCES_CATEGORY,
|
||||
label: 'Open Folder Settings (JSON)',
|
||||
});
|
||||
}
|
||||
|
||||
export namespace PreferenceMenus {
|
||||
export const PREFERENCE_EDITOR_CONTEXT_MENU: MenuPath = ['preferences:editor.contextMenu'];
|
||||
export const PREFERENCE_EDITOR_COPY_ACTIONS: MenuPath = [...PREFERENCE_EDITOR_CONTEXT_MENU, 'preferences:editor.contextMenu.copy'];
|
||||
export const FOLDER_SCOPE_MENU_PATH = ['preferences:scope.menu'];
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { codiconArray } from '@theia/core/lib/browser';
|
||||
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceArrayInputRenderer extends PreferenceLeafNodeRenderer<string[], HTMLInputElement> {
|
||||
existingValues = new Map<string, { node: HTMLElement, index: number }>();
|
||||
wrapper: HTMLElement;
|
||||
inputWrapper: HTMLElement;
|
||||
|
||||
protected createInteractable(parent: HTMLElement): void {
|
||||
const wrapper = document.createElement('ul');
|
||||
wrapper.classList.add('preference-array');
|
||||
this.wrapper = wrapper;
|
||||
const currentValue = this.getValue();
|
||||
if (Array.isArray(currentValue)) {
|
||||
for (const [index, value] of currentValue.entries()) {
|
||||
const node = this.createExistingValue(value);
|
||||
wrapper.appendChild(node);
|
||||
this.existingValues.set(value, { node, index });
|
||||
}
|
||||
}
|
||||
const inputWrapper = this.createInput();
|
||||
wrapper.appendChild(inputWrapper);
|
||||
parent.appendChild(wrapper);
|
||||
}
|
||||
|
||||
protected getFallbackValue(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected createExistingValue(value: string): HTMLElement {
|
||||
const existingValue = document.createElement('li');
|
||||
existingValue.classList.add('preference-array-element');
|
||||
const valueWrapper = document.createElement('span');
|
||||
valueWrapper.classList.add('preference-array-element-val');
|
||||
valueWrapper.textContent = value;
|
||||
existingValue.appendChild(valueWrapper);
|
||||
const iconWrapper = document.createElement('span');
|
||||
iconWrapper.classList.add('preference-array-element-btn', 'remove-btn');
|
||||
const handler = this.removeItem.bind(this, value);
|
||||
iconWrapper.onclick = handler;
|
||||
iconWrapper.onkeydown = handler;
|
||||
iconWrapper.setAttribute('role', 'button');
|
||||
iconWrapper.tabIndex = 0;
|
||||
existingValue.appendChild(iconWrapper);
|
||||
const icon = document.createElement('i');
|
||||
icon.classList.add(...codiconArray('close'));
|
||||
iconWrapper.appendChild(icon);
|
||||
return existingValue;
|
||||
}
|
||||
|
||||
protected createInput(): HTMLElement {
|
||||
const inputWrapper = document.createElement('li');
|
||||
this.inputWrapper = inputWrapper;
|
||||
const input = document.createElement('input');
|
||||
inputWrapper.appendChild(input);
|
||||
this.interactable = input;
|
||||
input.classList.add('preference-array-input', 'theia-input');
|
||||
input.type = 'text';
|
||||
input.placeholder = 'Add Value...';
|
||||
input.spellcheck = false;
|
||||
input.onkeydown = this.handleEnter.bind(this);
|
||||
input.setAttribute('aria-label', 'Preference String Input');
|
||||
const iconWrapper = document.createElement('span');
|
||||
inputWrapper.appendChild(iconWrapper);
|
||||
iconWrapper.classList.add('preference-array-element-btn', ...codiconArray('add'));
|
||||
iconWrapper.setAttribute('role', 'button');
|
||||
const handler = this.addItem.bind(this);
|
||||
iconWrapper.onclick = handler;
|
||||
iconWrapper.onkeydown = handler;
|
||||
iconWrapper.tabIndex = 0;
|
||||
iconWrapper.setAttribute('aria-label', 'Submit Preference Input');
|
||||
return inputWrapper;
|
||||
}
|
||||
|
||||
protected doHandleValueChange(): void {
|
||||
this.updateInspection();
|
||||
const values = this.getValue() ?? this.getDefaultValue();
|
||||
const newValues = new Set(...values);
|
||||
for (const [value, row] of this.existingValues.entries()) {
|
||||
if (!newValues.has(value)) {
|
||||
row.node.remove();
|
||||
this.existingValues.delete(value);
|
||||
}
|
||||
}
|
||||
for (const [index, value] of values.entries()) {
|
||||
let row = this.existingValues.get(value);
|
||||
if (row) {
|
||||
row.index = index;
|
||||
} else {
|
||||
row = { node: this.createExistingValue(value), index };
|
||||
this.existingValues.set(value, row);
|
||||
}
|
||||
|
||||
if (this.wrapper.children[index] !== row.node) {
|
||||
this.wrapper.children[index].insertAdjacentElement('beforebegin', row.node);
|
||||
}
|
||||
}
|
||||
this.updateModificationStatus();
|
||||
}
|
||||
|
||||
protected removeItem(value: string): void {
|
||||
const row = this.existingValues.get(value);
|
||||
if (row) {
|
||||
row.node.remove();
|
||||
this.existingValues.delete(value);
|
||||
this.setPreferenceImmediately(this.getOrderedValues());
|
||||
}
|
||||
}
|
||||
|
||||
protected handleEnter(e: KeyboardEvent): void {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.addItem();
|
||||
}
|
||||
}
|
||||
|
||||
protected addItem(): void {
|
||||
const newItem = this.interactable.value;
|
||||
if (newItem && !this.existingValues.has(newItem)) {
|
||||
const node = this.createExistingValue(newItem);
|
||||
this.inputWrapper.insertAdjacentElement('beforebegin', node);
|
||||
this.existingValues.set(newItem, { node, index: this.existingValues.size });
|
||||
this.setPreferenceImmediately(this.getOrderedValues());
|
||||
}
|
||||
this.interactable.value = '';
|
||||
}
|
||||
|
||||
protected getOrderedValues(): string[] {
|
||||
return Array.from(this.existingValues.entries())
|
||||
.sort(([, a], [, b]) => a.index - b.index)
|
||||
.map(([value]) => value);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.existingValues.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceArrayInputRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-array-input-renderer';
|
||||
id = PreferenceArrayInputRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(node: Preference.LeafNode): number {
|
||||
const type = Preference.LeafNode.getType(node);
|
||||
return type === 'array' && (node.preference.data.items as IJSONSchema)?.type === 'string' ? 2 : 0;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceArrayInputRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceBooleanInputRenderer extends PreferenceLeafNodeRenderer<boolean, HTMLInputElement> {
|
||||
protected createInteractable(parent: HTMLElement): void {
|
||||
const interactable = document.createElement('input');
|
||||
this.interactable = interactable;
|
||||
interactable.type = 'checkbox';
|
||||
interactable.classList.add('theia-input');
|
||||
interactable.defaultChecked = Boolean(this.getValue());
|
||||
interactable.onchange = this.handleUserInteraction.bind(this);
|
||||
parent.appendChild(interactable);
|
||||
}
|
||||
|
||||
protected override getAdditionalNodeClassnames(): Iterable<string> {
|
||||
return ['boolean'];
|
||||
}
|
||||
|
||||
protected getFallbackValue(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected handleUserInteraction(): Promise<void> {
|
||||
return this.setPreferenceImmediately(this.interactable.checked);
|
||||
}
|
||||
|
||||
protected doHandleValueChange(): void {
|
||||
const currentValue = this.interactable.checked;
|
||||
this.updateInspection();
|
||||
const newValue = Boolean(this.getValue());
|
||||
this.updateModificationStatus();
|
||||
if (newValue !== currentValue && document.activeElement !== this.interactable) {
|
||||
this.interactable.checked = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceBooleanInputRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-boolean-input-renderer';
|
||||
id = PreferenceBooleanInputRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(node: Preference.LeafNode): number {
|
||||
return Preference.LeafNode.getType(node) === 'boolean' ? 2 : 0;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceBooleanInputRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 EclipseSource and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { isObject } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { OpenFileDialogProps } from '@theia/filesystem/lib/browser';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
import { PreferenceStringInputRenderer } from './preference-string-input';
|
||||
|
||||
export interface FileNodeTypeDetails {
|
||||
isFilepath: true;
|
||||
selectionProps?: Partial<OpenFileDialogProps>;
|
||||
}
|
||||
|
||||
export namespace FileNodeTypeDetails {
|
||||
export function is(typeDetails: unknown): typeDetails is FileNodeTypeDetails {
|
||||
return isObject<FileNodeTypeDetails>(typeDetails) && !!typeDetails.isFilepath;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceSingleFilePathInputRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-single-file-path-input-renderer';
|
||||
id = PreferenceSingleFilePathInputRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(node: Preference.LeafNode): number {
|
||||
const typeDetails = node.preference.data.typeDetails;
|
||||
return FileNodeTypeDetails.is(typeDetails) && !typeDetails.selectionProps?.canSelectMany ? 5 : 0;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceSingleFilePathInputRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceSingleFilePathInputRenderer extends PreferenceStringInputRenderer {
|
||||
@inject(FileDialogService) fileDialogService: FileDialogService;
|
||||
|
||||
get typeDetails(): FileNodeTypeDetails {
|
||||
return this.preferenceNode.preference.data.typeDetails as FileNodeTypeDetails;
|
||||
}
|
||||
|
||||
protected createInputWrapper(): HTMLElement {
|
||||
const inputWrapper = document.createElement('div');
|
||||
inputWrapper.classList.add('preference-file-container');
|
||||
return inputWrapper;
|
||||
}
|
||||
|
||||
protected override createInteractable(parent: HTMLElement): void {
|
||||
const inputWrapper = this.createInputWrapper();
|
||||
|
||||
super.createInteractable(inputWrapper);
|
||||
this.interactable.classList.add('preference-file-input');
|
||||
|
||||
this.createBrowseButton(inputWrapper);
|
||||
|
||||
parent.appendChild(inputWrapper);
|
||||
}
|
||||
|
||||
protected createBrowseButton(parent: HTMLElement): void {
|
||||
const button = document.createElement('button');
|
||||
button.classList.add('theia-button', 'main', 'preference-file-button');
|
||||
button.textContent = nls.localize('theia/core/file/browse', 'Browse');
|
||||
const handler = this.browse.bind(this);
|
||||
button.onclick = handler;
|
||||
button.onkeydown = handler;
|
||||
button.tabIndex = 0;
|
||||
button.setAttribute('aria-label', 'Submit Preference Input');
|
||||
parent.appendChild(button);
|
||||
}
|
||||
|
||||
protected async browse(): Promise<void> {
|
||||
const selectionProps = this.typeDetails.selectionProps;
|
||||
const title = selectionProps?.title ?? selectionProps?.canSelectFolders ? WorkspaceCommands.OPEN_FOLDER.dialogLabel : WorkspaceCommands.OPEN_FILE.dialogLabel;
|
||||
const selection = await this.fileDialogService.showOpenDialog({ title, ...selectionProps });
|
||||
if (selection) {
|
||||
this.setPreferenceImmediately(selection.path.fsPath());
|
||||
}
|
||||
}
|
||||
|
||||
protected override setPreferenceImmediately(value: string): Promise<void> {
|
||||
this.interactable.value = value;
|
||||
return super.setPreferenceImmediately(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { injectable, inject, interfaces } from '@theia/core/shared/inversify';
|
||||
import { CommandService, nls } from '@theia/core/lib/common';
|
||||
import { Preference, PreferencesCommands } from '../../util/preference-types';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceJSONLinkRenderer extends PreferenceLeafNodeRenderer<JSONValue, HTMLAnchorElement> {
|
||||
@inject(CommandService) protected readonly commandService: CommandService;
|
||||
|
||||
protected createInteractable(parent: HTMLElement): void {
|
||||
const message = nls.localizeByDefault('Edit in settings.json');
|
||||
const interactable = document.createElement('a');
|
||||
this.interactable = interactable;
|
||||
interactable.classList.add('theia-json-input');
|
||||
interactable.setAttribute('role', 'button');
|
||||
interactable.title = message;
|
||||
interactable.textContent = message;
|
||||
interactable.onclick = this.handleUserInteraction.bind(this);
|
||||
interactable.onkeydown = this.handleUserInteraction.bind(this);
|
||||
parent.appendChild(interactable);
|
||||
}
|
||||
|
||||
protected getFallbackValue(): JSONValue {
|
||||
const node = this.preferenceNode;
|
||||
const type = Array.isArray(node.preference.data.type) ? node.preference.data.type[0] : node.preference.data.type;
|
||||
switch (type) {
|
||||
case 'object':
|
||||
return {};
|
||||
case 'array':
|
||||
return [];
|
||||
case 'null':
|
||||
return null; // eslint-disable-line no-null/no-null
|
||||
default: // Should all be handled by other input types.
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
protected doHandleValueChange(): void {
|
||||
this.updateInspection();
|
||||
this.updateModificationStatus();
|
||||
}
|
||||
|
||||
protected handleUserInteraction(): void {
|
||||
this.commandService.executeCommand(PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id, this.id);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceJSONLinkRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-json-link-renderer';
|
||||
id = PreferenceJSONLinkRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(_node: Preference.LeafNode): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceJSONLinkRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { PreferenceTreeModel } from '../../preference-tree-model';
|
||||
import { PreferenceTreeLabelProvider } from '../../util/preference-tree-label-provider';
|
||||
import * as markdownit from '@theia/core/shared/markdown-it';
|
||||
import * as markdownitemoji from '@theia/core/shared/markdown-it-emoji';
|
||||
import { CommandRegistry } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceMarkdownRenderer {
|
||||
|
||||
@inject(PreferenceTreeModel)
|
||||
protected readonly model: PreferenceTreeModel;
|
||||
@inject(PreferenceTreeLabelProvider)
|
||||
protected readonly labelProvider: PreferenceTreeLabelProvider;
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
protected _renderer?: markdownit;
|
||||
|
||||
render(text: string): string {
|
||||
return this.getRenderer().render(text);
|
||||
}
|
||||
|
||||
renderInline(text: string): string {
|
||||
return this.getRenderer().renderInline(text);
|
||||
}
|
||||
|
||||
protected getRenderer(): markdownit {
|
||||
this._renderer ??= this.buildMarkdownRenderer();
|
||||
return this._renderer;
|
||||
}
|
||||
|
||||
protected buildMarkdownRenderer(): markdownit {
|
||||
const engine = markdownit().use(markdownitemoji.full);
|
||||
const inlineCode = engine.renderer.rules.code_inline;
|
||||
|
||||
engine.renderer.rules.code_inline = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
const content = token.content;
|
||||
if (content.length > 2 && content.startsWith('#') && content.endsWith('#')) {
|
||||
const id = content.substring(1, content.length - 1);
|
||||
// First check whether there's a preference with the given ID
|
||||
const preferenceNode = this.model.getNodeFromPreferenceId(id);
|
||||
if (preferenceNode) {
|
||||
let name = this.labelProvider.getName(preferenceNode);
|
||||
const prefix = this.labelProvider.getPrefix(preferenceNode, true);
|
||||
if (prefix) {
|
||||
name = prefix + name;
|
||||
}
|
||||
return `<a title="${id}" href="preference:${id}">${name}</a>`;
|
||||
}
|
||||
// If no preference was found, check whether there's a command with the given ID
|
||||
const command = this.commandRegistry.getCommand(id);
|
||||
if (command) {
|
||||
const name = `${command.category ? `${command.category}: ` : ''}${command.label}`;
|
||||
return `<span class="command-link" title="${id}">${name}</span>`;
|
||||
}
|
||||
// If nothing was found, print a warning
|
||||
console.warn(`Linked preference "${id}" not found.`);
|
||||
}
|
||||
return inlineCode ? inlineCode(tokens, idx, options, env, self) : '';
|
||||
};
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 EclipseSource and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContributionProvider, Disposable, Emitter, Event, Prioritizeable } from '@theia/core';
|
||||
import { inject, injectable, interfaces, named } from '@theia/core/shared/inversify';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceHeaderRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
|
||||
export const PreferenceNodeRendererCreatorRegistry = Symbol('PreferenceNodeRendererCreatorRegistry');
|
||||
export interface PreferenceNodeRendererCreatorRegistry {
|
||||
registerPreferenceNodeRendererCreator(creator: PreferenceNodeRendererCreator): Disposable;
|
||||
unregisterPreferenceNodeRendererCreator(creator: PreferenceNodeRendererCreator): void;
|
||||
getPreferenceNodeRendererCreator(node: Preference.TreeNode): PreferenceNodeRendererCreator;
|
||||
onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
export const PreferenceNodeRendererContribution = Symbol('PreferenceNodeRendererContribution');
|
||||
export interface PreferenceNodeRendererContribution {
|
||||
registerPreferenceNodeRendererCreator(registry: PreferenceNodeRendererCreatorRegistry): void;
|
||||
}
|
||||
|
||||
export const PreferenceNodeRendererCreator = Symbol('PreferenceNodeRendererCreator');
|
||||
export interface PreferenceNodeRendererCreator {
|
||||
id: string;
|
||||
canHandle(node: Preference.TreeNode): number;
|
||||
createRenderer(node: Preference.TreeNode, container: interfaces.Container): PreferenceNodeRenderer;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DefaultPreferenceNodeRendererCreatorRegistry implements PreferenceNodeRendererCreatorRegistry {
|
||||
protected readonly _creators: Map<string, PreferenceNodeRendererCreator> = new Map<string, PreferenceNodeRendererCreator>();
|
||||
protected readonly onDidChangeEmitter = new Emitter<void>();
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
|
||||
constructor(
|
||||
@inject(ContributionProvider) @named(PreferenceNodeRendererContribution)
|
||||
protected readonly contributionProvider: ContributionProvider<PreferenceNodeRendererContribution>
|
||||
) {
|
||||
const contributions = this.contributionProvider.getContributions();
|
||||
for (const contrib of contributions) {
|
||||
contrib.registerPreferenceNodeRendererCreator(this);
|
||||
}
|
||||
}
|
||||
|
||||
registerPreferenceNodeRendererCreator(creator: PreferenceNodeRendererCreator): Disposable {
|
||||
if (this._creators.has(creator.id)) {
|
||||
console.warn(`A preference node renderer creator ${creator.id} is already registered.`);
|
||||
return Disposable.NULL;
|
||||
}
|
||||
this._creators.set(creator.id, creator);
|
||||
this.fireDidChange();
|
||||
return Disposable.create(() => this._creators.delete(creator.id));
|
||||
}
|
||||
|
||||
unregisterPreferenceNodeRendererCreator(creator: PreferenceNodeRendererCreator | string): void {
|
||||
const id = typeof creator === 'string' ? creator : creator.id;
|
||||
if (this._creators.delete(id)) {
|
||||
this.fireDidChange();
|
||||
}
|
||||
}
|
||||
|
||||
getPreferenceNodeRendererCreator(node: Preference.TreeNode): PreferenceNodeRendererCreator {
|
||||
const contributions = this.prioritize(node);
|
||||
if (contributions.length >= 1) {
|
||||
return contributions[0];
|
||||
}
|
||||
// we already bind a default creator contribution so if that happens it was deliberate
|
||||
throw new Error(`There is no contribution for ${node.id}.`);
|
||||
}
|
||||
|
||||
protected fireDidChange(): void {
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
}
|
||||
|
||||
protected prioritize(node: Preference.TreeNode): PreferenceNodeRendererCreator[] {
|
||||
const prioritized = Prioritizeable.prioritizeAllSync(Array.from(this._creators.values()), creator => {
|
||||
try {
|
||||
return creator.canHandle(node);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return prioritized.map(p => p.value);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class PreferenceLeafNodeRendererContribution implements PreferenceNodeRendererCreator, PreferenceNodeRendererContribution {
|
||||
abstract id: string;
|
||||
|
||||
canHandle(node: Preference.TreeNode): number {
|
||||
return Preference.LeafNode.is(node) ? this.canHandleLeafNode(node) : 0;
|
||||
}
|
||||
|
||||
registerPreferenceNodeRendererCreator(registry: PreferenceNodeRendererCreatorRegistry): void {
|
||||
registry.registerPreferenceNodeRendererCreator(this);
|
||||
}
|
||||
|
||||
abstract canHandleLeafNode(node: Preference.LeafNode): number;
|
||||
|
||||
createRenderer(node: Preference.TreeNode, container: interfaces.Container): PreferenceNodeRenderer {
|
||||
const child = container.createChild();
|
||||
child.bind(Preference.Node).toConstantValue(node);
|
||||
return this.createLeafNodeRenderer(child);
|
||||
}
|
||||
|
||||
abstract createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceHeaderRendererContribution implements PreferenceNodeRendererCreator, PreferenceNodeRendererContribution {
|
||||
static ID = 'preference-header-renderer';
|
||||
id = PreferenceHeaderRendererContribution.ID;
|
||||
|
||||
registerPreferenceNodeRendererCreator(registry: PreferenceNodeRendererCreatorRegistry): void {
|
||||
registry.registerPreferenceNodeRendererCreator(this);
|
||||
}
|
||||
|
||||
canHandle(node: Preference.TreeNode): number {
|
||||
return !Preference.LeafNode.is(node) ? 1 : 0;
|
||||
}
|
||||
|
||||
createRenderer(node: Preference.TreeNode, container: interfaces.Container): PreferenceNodeRenderer {
|
||||
const grandchild = container.createChild();
|
||||
grandchild.bind(Preference.Node).toConstantValue(node);
|
||||
return grandchild.get(PreferenceHeaderRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
ContextMenuRenderer, codicon, OpenerService, open
|
||||
} from '@theia/core/lib/browser';
|
||||
import { Preference, PreferenceMenus } from '../../util/preference-types';
|
||||
import { PreferenceTreeLabelProvider } from '../../util/preference-tree-label-provider';
|
||||
import { PreferencesScopeTabBar } from '../preference-scope-tabbar-widget';
|
||||
import { Disposable, nls, PreferenceDataProperty, PreferenceInspection, PreferenceScope, PreferenceService } from '@theia/core/lib/common';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
import { PreferenceTreeModel } from '../../preference-tree-model';
|
||||
import { PreferencesSearchbarWidget } from '../preference-searchbar-widget';
|
||||
import * as DOMPurify from '@theia/core/shared/dompurify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { PreferenceMarkdownRenderer } from './preference-markdown-renderer';
|
||||
|
||||
export const PreferenceNodeRendererFactory = Symbol('PreferenceNodeRendererFactory');
|
||||
export type PreferenceNodeRendererFactory = (node: Preference.TreeNode) => PreferenceNodeRenderer;
|
||||
export const HEADER_CLASS = 'settings-section-category-title';
|
||||
export const SUBHEADER_CLASS = 'settings-section-subcategory-title';
|
||||
|
||||
export interface GeneralPreferenceNodeRenderer extends Disposable {
|
||||
node: HTMLElement;
|
||||
id: string;
|
||||
schema?: PreferenceDataProperty;
|
||||
group: string;
|
||||
nodeId: string;
|
||||
visible: boolean;
|
||||
insertBefore(nextSibling: HTMLElement): void;
|
||||
insertAfter(previousSibling: HTMLElement): void;
|
||||
appendTo(parent: HTMLElement): void;
|
||||
prependTo(parent: HTMLElement): void;
|
||||
handleValueChange?(): void;
|
||||
handleSearchChange?(isFiltered?: boolean): void;
|
||||
handleScopeChange?(isFiltered?: boolean): void;
|
||||
hide(): void;
|
||||
show(): void;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class PreferenceNodeRenderer implements Disposable, GeneralPreferenceNodeRenderer {
|
||||
@inject(Preference.Node) protected readonly preferenceNode: Preference.Node;
|
||||
@inject(PreferenceTreeLabelProvider) protected readonly labelProvider: PreferenceTreeLabelProvider;
|
||||
|
||||
protected attached = false;
|
||||
|
||||
_id: string;
|
||||
_group: string;
|
||||
_subgroup: string;
|
||||
protected domNode: HTMLElement;
|
||||
|
||||
get node(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
get nodeId(): string {
|
||||
return this.preferenceNode.id;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get group(): string {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
get visible(): boolean {
|
||||
return !this.node.classList.contains('hidden');
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.setId();
|
||||
this.domNode = this.createDomNode();
|
||||
}
|
||||
|
||||
protected setId(): void {
|
||||
const { id, group } = Preference.TreeNode.getGroupAndIdFromNodeId(this.preferenceNode.id);
|
||||
const segments = id.split('.');
|
||||
this._id = id;
|
||||
this._group = group;
|
||||
this._subgroup = (group === segments[0] ? segments[1] : segments[0]) ?? '';
|
||||
}
|
||||
|
||||
protected abstract createDomNode(): HTMLElement;
|
||||
|
||||
protected getAdditionalNodeClassnames(): Iterable<string> {
|
||||
return [];
|
||||
}
|
||||
|
||||
insertBefore(nextSibling: HTMLElement): void {
|
||||
nextSibling.insertAdjacentElement('beforebegin', this.domNode);
|
||||
this.attached = true;
|
||||
}
|
||||
|
||||
insertAfter(previousSibling: HTMLElement): void {
|
||||
previousSibling.insertAdjacentElement('afterend', this.domNode);
|
||||
}
|
||||
|
||||
appendTo(parent: HTMLElement): void {
|
||||
parent.appendChild(this.domNode);
|
||||
}
|
||||
|
||||
prependTo(parent: HTMLElement): void {
|
||||
parent.prepend(this.domNode);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.domNode.classList.add('hidden');
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.domNode.classList.remove('hidden');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.domNode.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class PreferenceHeaderRenderer extends PreferenceNodeRenderer {
|
||||
protected createDomNode(): HTMLElement {
|
||||
const wrapper = document.createElement('ul');
|
||||
wrapper.className = 'settings-section';
|
||||
wrapper.id = `${this.preferenceNode.id}-editor`;
|
||||
const isCategory = Preference.TreeNode.isTopLevel(this.preferenceNode);
|
||||
const hierarchyClassName = isCategory ? HEADER_CLASS : SUBHEADER_CLASS;
|
||||
const name = this.labelProvider.getName(this.preferenceNode);
|
||||
const label = document.createElement('li');
|
||||
label.classList.add('settings-section-title', hierarchyClassName);
|
||||
label.textContent = name;
|
||||
wrapper.appendChild(label);
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class PreferenceLeafNodeRenderer<ValueType extends JSONValue, InteractableType extends HTMLElement>
|
||||
extends PreferenceNodeRenderer
|
||||
implements Required<GeneralPreferenceNodeRenderer> {
|
||||
@inject(Preference.Node) protected override readonly preferenceNode: Preference.LeafNode;
|
||||
@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
|
||||
@inject(ContextMenuRenderer) protected readonly menuRenderer: ContextMenuRenderer;
|
||||
@inject(PreferencesScopeTabBar) protected readonly scopeTracker: PreferencesScopeTabBar;
|
||||
@inject(PreferenceTreeModel) protected readonly model: PreferenceTreeModel;
|
||||
@inject(PreferencesSearchbarWidget) protected readonly searchbar: PreferencesSearchbarWidget;
|
||||
@inject(OpenerService) protected readonly openerService: OpenerService;
|
||||
@inject(PreferenceMarkdownRenderer) protected readonly markdownRenderer: PreferenceMarkdownRenderer;
|
||||
|
||||
protected headlineWrapper: HTMLDivElement;
|
||||
protected gutter: HTMLDivElement;
|
||||
protected interactable: InteractableType;
|
||||
protected inspection: PreferenceInspection<ValueType> | undefined;
|
||||
protected isSet = false;
|
||||
|
||||
get schema(): PreferenceDataProperty {
|
||||
return this.preferenceNode.preference.data;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
this.setId();
|
||||
this.updateInspection();
|
||||
this.domNode = this.createDomNode();
|
||||
this.updateModificationStatus();
|
||||
}
|
||||
|
||||
protected updateInspection(): void {
|
||||
this.inspection = this.preferenceService.inspect<ValueType>(this.id, this.scopeTracker.currentScope.uri);
|
||||
}
|
||||
|
||||
protected openLink(event: MouseEvent): void {
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// Exclude right click
|
||||
if (event.button < 2) {
|
||||
const uri = new URI(event.target.href);
|
||||
open(this.openerService, uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected createDomNode(): HTMLLIElement {
|
||||
const wrapper = document.createElement('li');
|
||||
wrapper.classList.add('single-pref');
|
||||
wrapper.id = `${this.id}-editor`;
|
||||
wrapper.tabIndex = 0;
|
||||
wrapper.setAttribute('data-pref-id', this.id);
|
||||
wrapper.setAttribute('data-node-id', this.preferenceNode.id);
|
||||
|
||||
const headlineWrapper = document.createElement('div');
|
||||
headlineWrapper.classList.add('pref-name');
|
||||
headlineWrapper.title = this.id;
|
||||
this.headlineWrapper = headlineWrapper;
|
||||
wrapper.appendChild(headlineWrapper);
|
||||
|
||||
this.updateHeadline();
|
||||
|
||||
const gutter = document.createElement('div');
|
||||
gutter.classList.add('pref-context-gutter');
|
||||
this.gutter = gutter;
|
||||
wrapper.appendChild(gutter);
|
||||
|
||||
const cog = document.createElement('i');
|
||||
cog.className = `${codicon('settings-gear', true)} settings-context-menu-btn`;
|
||||
cog.setAttribute('aria-label', 'Open Context Menu');
|
||||
cog.setAttribute('role', 'button');
|
||||
cog.onclick = this.handleCogAction.bind(this);
|
||||
cog.onkeydown = this.handleCogAction.bind(this);
|
||||
cog.title = nls.localizeByDefault('More Actions...');
|
||||
gutter.appendChild(cog);
|
||||
|
||||
const contentWrapper = document.createElement('div');
|
||||
contentWrapper.classList.add('pref-content-container', ...this.getAdditionalNodeClassnames());
|
||||
wrapper.appendChild(contentWrapper);
|
||||
|
||||
const { description, markdownDescription } = this.preferenceNode.preference.data;
|
||||
if (markdownDescription || description) {
|
||||
const descriptionWrapper = document.createElement('div');
|
||||
descriptionWrapper.classList.add('pref-description');
|
||||
if (markdownDescription) {
|
||||
const renderedDescription = this.markdownRenderer.render(markdownDescription);
|
||||
descriptionWrapper.onauxclick = this.openLink.bind(this);
|
||||
descriptionWrapper.onclick = this.openLink.bind(this);
|
||||
descriptionWrapper.oncontextmenu = () => false;
|
||||
descriptionWrapper.innerHTML = DOMPurify.sanitize(renderedDescription, {
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true
|
||||
});
|
||||
} else if (description) {
|
||||
descriptionWrapper.textContent = description;
|
||||
}
|
||||
contentWrapper.appendChild(descriptionWrapper);
|
||||
}
|
||||
|
||||
const interactableWrapper = document.createElement('div');
|
||||
interactableWrapper.classList.add('pref-input');
|
||||
contentWrapper.appendChild(interactableWrapper);
|
||||
this.createInteractable(interactableWrapper);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
protected handleCogAction({ currentTarget }: KeyboardEvent | MouseEvent): void {
|
||||
const value = Preference.getValueInScope(this.inspection, this.scopeTracker.currentScope.scope) ?? this.getDefaultValue();
|
||||
const target = currentTarget as HTMLElement | undefined;
|
||||
if (target && value !== undefined) {
|
||||
this.showCog();
|
||||
const domRect = target.getBoundingClientRect();
|
||||
this.menuRenderer.render({
|
||||
menuPath: PreferenceMenus.PREFERENCE_EDITOR_CONTEXT_MENU,
|
||||
anchor: { x: domRect.left, y: domRect.bottom },
|
||||
args: [{ id: this.id, value }],
|
||||
context: target,
|
||||
onHide: () => this.hideCog()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected addModifiedMarking(): void {
|
||||
this.gutter.classList.add('theia-mod-item-modified');
|
||||
}
|
||||
|
||||
protected removeModifiedMarking(): void {
|
||||
this.gutter.classList.remove('theia-mod-item-modified');
|
||||
}
|
||||
|
||||
protected showCog(): void {
|
||||
this.gutter.classList.add('show-cog');
|
||||
}
|
||||
|
||||
protected hideCog(): void {
|
||||
this.gutter.classList.remove('show-cog');
|
||||
}
|
||||
|
||||
protected updateModificationStatus(): void {
|
||||
const wasSet = this.isSet;
|
||||
const { inspection } = this;
|
||||
const valueInCurrentScope = Preference.getValueInScope(inspection, this.scopeTracker.currentScope.scope);
|
||||
this.isSet = valueInCurrentScope !== undefined;
|
||||
if (wasSet !== this.isSet) {
|
||||
this.gutter.classList.toggle('theia-mod-item-modified', this.isSet);
|
||||
}
|
||||
}
|
||||
|
||||
protected updateHeadline(filtered = this.model.isFiltered): void {
|
||||
const { headlineWrapper } = this;
|
||||
if (this.headlineWrapper.childElementCount === 0) {
|
||||
const name = this.labelProvider.getName(this.preferenceNode);
|
||||
const nameWrapper = document.createElement('span');
|
||||
nameWrapper.classList.add('preference-leaf-headline-name');
|
||||
nameWrapper.textContent = name;
|
||||
headlineWrapper.appendChild(nameWrapper);
|
||||
|
||||
const tags = this.schema.tags;
|
||||
if (tags && tags.length > 0) {
|
||||
const tagsWrapper = document.createElement('span');
|
||||
tagsWrapper.classList.add('preference-leaf-headline-tags');
|
||||
const PREVIEW_INDICATOR_DESCRIPTION = nls.localizeByDefault(
|
||||
'Preview setting: this setting controls a new feature that is still under refinement yet ready to use. Feedback is welcome.');
|
||||
const EXPERIMENTAL_INDICATOR_DESCRIPTION = nls.localizeByDefault(
|
||||
'Experimental setting: this setting controls a new feature that is actively being developed and may be unstable. It is subject to change or removal.');
|
||||
|
||||
tags.forEach(tag => {
|
||||
const tagElement = document.createElement('span');
|
||||
const isExperimentalSetting = tag === 'experimental';
|
||||
const isPreviewSetting = tag === 'preview';
|
||||
tagElement.classList.add('preference-tag');
|
||||
tagElement.textContent = isExperimentalSetting ? nls.localizeByDefault('Experimental') :
|
||||
isPreviewSetting ? nls.localizeByDefault('Preview') : tag;
|
||||
tagElement.title = isExperimentalSetting ? EXPERIMENTAL_INDICATOR_DESCRIPTION :
|
||||
isPreviewSetting ? PREVIEW_INDICATOR_DESCRIPTION : tag;
|
||||
tagsWrapper.appendChild(tagElement);
|
||||
});
|
||||
headlineWrapper.appendChild(tagsWrapper);
|
||||
}
|
||||
}
|
||||
const prefix = this.labelProvider.getPrefix(this.preferenceNode, filtered);
|
||||
const currentFirstChild = headlineWrapper.children[0];
|
||||
const currentFirstChildIsPrefix = currentFirstChild.classList.contains('preference-leaf-headline-prefix');
|
||||
if (prefix) {
|
||||
let prefixWrapper;
|
||||
if (currentFirstChildIsPrefix) {
|
||||
prefixWrapper = currentFirstChild;
|
||||
} else {
|
||||
prefixWrapper = document.createElement('span');
|
||||
prefixWrapper.classList.add('preference-leaf-headline-prefix');
|
||||
headlineWrapper.insertBefore(prefixWrapper, currentFirstChild);
|
||||
}
|
||||
prefixWrapper.textContent = prefix;
|
||||
} else if (currentFirstChildIsPrefix) {
|
||||
headlineWrapper.removeChild(currentFirstChild);
|
||||
}
|
||||
|
||||
const currentLastChild = headlineWrapper.lastChild as HTMLElement;
|
||||
if (currentLastChild.classList.contains('preference-leaf-headline-suffix')) {
|
||||
this.compareOtherModifiedScopes(headlineWrapper, currentLastChild);
|
||||
} else {
|
||||
this.createOtherModifiedScopes(headlineWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
protected compareOtherModifiedScopes(headlineWrapper: HTMLDivElement, currentSuffix: HTMLElement): void {
|
||||
const modifiedScopes = this.getModifiedScopesAsStrings();
|
||||
if (modifiedScopes.length === 0) {
|
||||
headlineWrapper.removeChild(currentSuffix);
|
||||
} else {
|
||||
|
||||
const modifiedMessagePrefix = currentSuffix.children[0] as HTMLElement;
|
||||
const newMessagePrefix = this.getModifiedMessagePrefix();
|
||||
if (modifiedMessagePrefix.textContent !== newMessagePrefix) {
|
||||
modifiedMessagePrefix.textContent = newMessagePrefix;
|
||||
}
|
||||
|
||||
const [firstModifiedScope, secondModifiedScope] = modifiedScopes;
|
||||
|
||||
const firstScopeMessage = currentSuffix.children[1] as HTMLElement;
|
||||
const secondScopeMessage = currentSuffix.children[2] as HTMLElement;
|
||||
firstScopeMessage.children[0].textContent = PreferenceScope[firstModifiedScope];
|
||||
this.addEventHandlerToModifiedScope(firstModifiedScope, firstScopeMessage.children[0] as HTMLElement);
|
||||
if (modifiedScopes.length === 1 && secondScopeMessage) {
|
||||
currentSuffix.removeChild(secondScopeMessage);
|
||||
} else if (modifiedScopes.length === 2 && !secondScopeMessage) {
|
||||
const newSecondMessage = this.createModifiedScopeMessage(secondModifiedScope);
|
||||
currentSuffix.appendChild(newSecondMessage);
|
||||
}
|
||||
// If both scopes are modified and both messages are present, do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
protected createOtherModifiedScopes(headlineWrapper: HTMLDivElement): void {
|
||||
const modifiedScopes = this.getModifiedScopesAsStrings();
|
||||
if (modifiedScopes.length !== 0) {
|
||||
const wrapper = document.createElement('i');
|
||||
wrapper.classList.add('preference-leaf-headline-suffix');
|
||||
headlineWrapper.appendChild(wrapper);
|
||||
|
||||
const messagePrefix = this.getModifiedMessagePrefix();
|
||||
const messageWrapper = document.createElement('span');
|
||||
messageWrapper.classList.add('preference-other-modified-scope-alert');
|
||||
messageWrapper.textContent = messagePrefix;
|
||||
wrapper.appendChild(messageWrapper);
|
||||
modifiedScopes.forEach((scopeName, i) => {
|
||||
const scopeWrapper = this.createModifiedScopeMessage(scopeName);
|
||||
wrapper.appendChild(scopeWrapper);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected createModifiedScopeMessage(scope: PreferenceScope): HTMLSpanElement {
|
||||
const scopeWrapper = document.createElement('span');
|
||||
scopeWrapper.classList.add('preference-modified-scope-wrapper');
|
||||
const scopeInteractable = document.createElement('span');
|
||||
scopeInteractable.classList.add('preference-scope-underlined');
|
||||
const scopeName = PreferenceScope[scope];
|
||||
this.addEventHandlerToModifiedScope(scope, scopeInteractable);
|
||||
scopeInteractable.textContent = scopeName;
|
||||
scopeWrapper.appendChild(scopeInteractable);
|
||||
return scopeWrapper;
|
||||
}
|
||||
|
||||
protected getModifiedMessagePrefix(): string {
|
||||
return (this.isSet ? nls.localizeByDefault('Also modified in') : nls.localizeByDefault('Modified in')) + ': ';
|
||||
}
|
||||
|
||||
protected addEventHandlerToModifiedScope(scope: PreferenceScope, scopeWrapper: HTMLElement): void {
|
||||
if (scope === PreferenceScope.User || scope === PreferenceScope.Workspace) {
|
||||
const eventHandler = () => {
|
||||
this.scopeTracker.setScope(scope);
|
||||
this.searchbar.updateSearchTerm(this.id);
|
||||
};
|
||||
scopeWrapper.onclick = eventHandler;
|
||||
scopeWrapper.onkeydown = eventHandler;
|
||||
scopeWrapper.tabIndex = 0;
|
||||
} else {
|
||||
scopeWrapper.onclick = null; // eslint-disable-line no-null/no-null
|
||||
scopeWrapper.onkeydown = null; // eslint-disable-line no-null/no-null
|
||||
scopeWrapper.tabIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected getModifiedScopesAsStrings(): PreferenceScope[] {
|
||||
const currentScopeInView = this.scopeTracker.currentScope.scope;
|
||||
const { inspection } = this;
|
||||
const modifiedScopes = [];
|
||||
if (inspection) {
|
||||
for (const otherScope of [PreferenceScope.User, PreferenceScope.Workspace]) {
|
||||
if (otherScope !== currentScopeInView) {
|
||||
const valueInOtherScope = Preference.getValueInScope(inspection, otherScope);
|
||||
if (valueInOtherScope !== undefined) {
|
||||
modifiedScopes.push(otherScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return modifiedScopes;
|
||||
}
|
||||
|
||||
// Many preferences allow `null` and even use it as a default regardless of the declared type.
|
||||
protected getValue(): ValueType | null {
|
||||
let currentValue = Preference.getValueInScope(this.inspection, this.scopeTracker.currentScope.scope);
|
||||
if (currentValue === undefined) {
|
||||
currentValue = this.getDefaultValue();
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
protected setPreferenceWithDebounce = debounce(this.setPreferenceImmediately.bind(this), 500, { leading: false, trailing: true });
|
||||
|
||||
protected setPreferenceImmediately(value: ValueType | undefined): Promise<void> {
|
||||
return this.preferenceService.set(this.id, value, this.scopeTracker.currentScope.scope, this.scopeTracker.currentScope.uri)
|
||||
.catch(() => this.handleValueChange());
|
||||
}
|
||||
|
||||
handleSearchChange(isFiltered = this.model.isFiltered): void {
|
||||
this.updateHeadline(isFiltered);
|
||||
}
|
||||
|
||||
handleScopeChange(isFiltered = this.model.isFiltered): void {
|
||||
this.handleValueChange();
|
||||
this.updateHeadline(isFiltered);
|
||||
}
|
||||
|
||||
handleValueChange(): void {
|
||||
this.doHandleValueChange();
|
||||
this.updateHeadline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default value for this preference.
|
||||
* @returns The default value from the inspection or the fallback value if no default is specified.
|
||||
*/
|
||||
protected getDefaultValue(): ValueType {
|
||||
return this.inspection?.defaultValue ?? this.getFallbackValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should create an HTML element that the user can interact with to change the value of the preference.
|
||||
* @param container the parent element for the interactable. This method is responsible for adding the new element to its parent.
|
||||
*/
|
||||
protected abstract createInteractable(container: HTMLElement): void;
|
||||
/**
|
||||
* @returns a fallback default value for a preference of the type implemented by a concrete leaf renderer
|
||||
* This function is only called if the default value for a given preference is not specified in its schema.
|
||||
*/
|
||||
protected abstract getFallbackValue(): ValueType;
|
||||
/**
|
||||
* This function is responsible for reconciling the display of the preference value with the value reported by the PreferenceService.
|
||||
*/
|
||||
protected abstract doHandleValueChange(): void;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource GmbH and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
|
||||
@injectable()
|
||||
/** For rendering preference items for which the only interesting feature is the description */
|
||||
export class PreferenceNullInputRenderer extends PreferenceLeafNodeRenderer<null, HTMLElement> {
|
||||
protected override createInteractable(container: HTMLElement): void {
|
||||
const span = document.createElement('span');
|
||||
this.interactable = span;
|
||||
container.appendChild(span);
|
||||
}
|
||||
|
||||
protected override getFallbackValue(): null {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override doHandleValueChange(): void { }
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceNullRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-null-renderer';
|
||||
id = PreferenceNullRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(node: Preference.LeafNode): number {
|
||||
const isOnlyNull = node.preference.data.type === 'null' || Array.isArray(node.preference.data.type) && node.preference.data.type.every(candidate => candidate === 'null');
|
||||
return isOnlyNull ? 5 : 0;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceNullInputRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { nls, isBoolean, isNumber } from '@theia/core';
|
||||
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
|
||||
interface PreferenceNumberInputValidation {
|
||||
/**
|
||||
* the numeric value of the input. `NaN` if there is an error.
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* the error message to display.
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceNumberInputRenderer extends PreferenceLeafNodeRenderer<number, HTMLInputElement> {
|
||||
|
||||
protected _errorMessage: HTMLElement | undefined;
|
||||
protected interactableWrapper: HTMLElement;
|
||||
|
||||
get errorMessage(): HTMLElement {
|
||||
if (!this._errorMessage) {
|
||||
const errorMessage = document.createElement('div');
|
||||
errorMessage.classList.add('pref-error-notification');
|
||||
this._errorMessage = errorMessage;
|
||||
}
|
||||
return this._errorMessage;
|
||||
}
|
||||
|
||||
protected createInteractable(parent: HTMLElement): void {
|
||||
const interactableWrapper = document.createElement('div');
|
||||
this.interactableWrapper = interactableWrapper;
|
||||
interactableWrapper.classList.add('pref-input-container');
|
||||
const interactable = document.createElement('input');
|
||||
this.interactable = interactable;
|
||||
interactable.type = 'number';
|
||||
interactable.step = this.preferenceNode.preference.data.type === 'integer' ? '1' : 'any';
|
||||
interactable.classList.add('theia-input');
|
||||
interactable.defaultValue = this.getValue()?.toString() ?? '';
|
||||
interactable.oninput = this.handleUserInteraction.bind(this);
|
||||
interactable.onblur = this.handleBlur.bind(this);
|
||||
interactableWrapper.appendChild(interactable);
|
||||
parent.appendChild(interactableWrapper);
|
||||
}
|
||||
|
||||
protected getFallbackValue(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected handleUserInteraction(): void {
|
||||
const { value, message } = this.getInputValidation(this.interactable.value);
|
||||
if (isNaN(value)) {
|
||||
this.showErrorMessage(message);
|
||||
} else {
|
||||
this.hideErrorMessage();
|
||||
this.setPreferenceWithDebounce(value);
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleBlur(): Promise<void> {
|
||||
this.hideErrorMessage();
|
||||
await this.setPreferenceWithDebounce.flush();
|
||||
this.handleValueChange();
|
||||
}
|
||||
|
||||
protected doHandleValueChange(): void {
|
||||
const { value } = this.interactable;
|
||||
const currentValue = value.length ? Number(value) : NaN;
|
||||
this.updateInspection();
|
||||
const newValue = this.getValue();
|
||||
this.updateModificationStatus();
|
||||
if (newValue !== currentValue) {
|
||||
if (document.activeElement !== this.interactable) {
|
||||
this.interactable.value = (newValue ?? this.getDefaultValue()).toString();
|
||||
} else {
|
||||
this.handleUserInteraction(); // give priority to the value of the input if it is focused.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getInputValidation(input: string): PreferenceNumberInputValidation {
|
||||
const { preference: { data } } = this.preferenceNode;
|
||||
const inputValue = Number(input);
|
||||
const errorMessages: string[] = [];
|
||||
|
||||
if (input === '' || isNaN(inputValue)) {
|
||||
return { value: NaN, message: nls.localizeByDefault('Value must be a number.') };
|
||||
}
|
||||
if (data.type === 'integer' && !Number.isInteger(inputValue)) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be an integer.'));
|
||||
}
|
||||
if (data.minimum !== undefined && isFinite(data.minimum)) {
|
||||
// https://json-schema.org/understanding-json-schema/reference/numeric
|
||||
// "In JSON Schema Draft 4, exclusiveMinimum and exclusiveMaximum work differently.
|
||||
// There they are boolean values, that indicate whether minimum and maximum are exclusive of the value"
|
||||
if (isBoolean(data.exclusiveMinimum) && data.exclusiveMinimum) {
|
||||
if (inputValue <= data.minimum) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be strictly greater than {0}.', data.minimum));
|
||||
}
|
||||
} else {
|
||||
if (inputValue < data.minimum) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be greater than or equal to {0}.', data.minimum));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.maximum !== undefined && isFinite(data.maximum)) {
|
||||
// https://json-schema.org/understanding-json-schema/reference/numeric
|
||||
// "In JSON Schema Draft 4, exclusiveMinimum and exclusiveMaximum work differently.
|
||||
// There they are boolean values, that indicate whether minimum and maximum are exclusive of the value"
|
||||
if (isBoolean(data.exclusiveMaximum) && data.exclusiveMaximum) {
|
||||
if (inputValue >= data.maximum) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be strictly less than {0}.', data.maximum));
|
||||
}
|
||||
} else {
|
||||
if (inputValue > data.maximum) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be less than or equal to {0}.', data.maximum));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Using JSON Schema before Draft 4 both exclusive and non-exclusive variants can be set
|
||||
if (isNumber(data.exclusiveMinimum) && isFinite(data.exclusiveMinimum)) {
|
||||
if (inputValue <= data.exclusiveMinimum) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be strictly greater than {0}.', data.exclusiveMinimum));
|
||||
}
|
||||
}
|
||||
if (isNumber(data.exclusiveMaximum) && isFinite(data.exclusiveMaximum)) {
|
||||
if (inputValue >= data.exclusiveMaximum) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be strictly less than {0}.', data.exclusiveMaximum));
|
||||
}
|
||||
}
|
||||
if (isNumber(data.multipleOf) && data.multipleOf !== 0 && !Number.isInteger(inputValue / data.multipleOf)) {
|
||||
errorMessages.push(nls.localizeByDefault('Value must be a multiple of {0}.', data.multipleOf));
|
||||
}
|
||||
|
||||
return {
|
||||
value: errorMessages.length ? NaN : inputValue,
|
||||
message: errorMessages.join(' ')
|
||||
};
|
||||
}
|
||||
|
||||
protected showErrorMessage(message: string): void {
|
||||
this.errorMessage.textContent = message;
|
||||
this.interactableWrapper.appendChild(this.errorMessage);
|
||||
}
|
||||
|
||||
protected hideErrorMessage(): void {
|
||||
this.errorMessage.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceNumberInputRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-number-input-renderer';
|
||||
id = PreferenceNumberInputRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(node: Preference.LeafNode): number {
|
||||
const type = Preference.LeafNode.getType(node);
|
||||
return type === 'integer' || type === 'number' ? 2 : 0;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceNumberInputRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { createRoot } from '@theia/core/shared/react-dom/client';
|
||||
import { escapeInvisibleChars } from '@theia/core/lib/common/strings';
|
||||
import { PreferenceUtils } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer<JSONValue, HTMLDivElement> {
|
||||
|
||||
protected readonly selectComponent = React.createRef<SelectComponent>();
|
||||
|
||||
protected selectOptions: SelectOption[] = [];
|
||||
|
||||
protected get enumValues(): JSONValue[] {
|
||||
return this.preferenceNode.preference.data.enum!;
|
||||
}
|
||||
|
||||
protected updateSelectOptions(): void {
|
||||
const updatedSelectOptions: SelectOption[] = [];
|
||||
const values = this.enumValues;
|
||||
const preferenceData = this.preferenceNode.preference.data;
|
||||
const defaultValue = preferenceData.default;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i];
|
||||
const stringValue = `${value}`;
|
||||
const label = escapeInvisibleChars(preferenceData.enumItemLabels?.[i] ?? stringValue);
|
||||
const detail = PreferenceUtils.deepEqual(defaultValue, value) ? 'default' : undefined;
|
||||
let enumDescription = preferenceData.enumDescriptions?.[i];
|
||||
let markdown = false;
|
||||
const markdownEnumDescription = preferenceData.markdownEnumDescriptions?.[i];
|
||||
if (markdownEnumDescription) {
|
||||
enumDescription = this.markdownRenderer.renderInline(markdownEnumDescription);
|
||||
markdown = true;
|
||||
}
|
||||
updatedSelectOptions.push({
|
||||
label,
|
||||
value: stringValue,
|
||||
detail,
|
||||
description: enumDescription,
|
||||
markdown
|
||||
});
|
||||
}
|
||||
this.selectOptions = updatedSelectOptions;
|
||||
}
|
||||
|
||||
protected createInteractable(parent: HTMLElement): void {
|
||||
this.updateSelectOptions();
|
||||
const interactable = document.createElement('div');
|
||||
const selectComponent = React.createElement(SelectComponent, {
|
||||
options: this.selectOptions,
|
||||
defaultValue: this.getDataValue(),
|
||||
onChange: (_, index) => this.handleUserInteraction(index),
|
||||
ref: this.selectComponent
|
||||
});
|
||||
this.interactable = interactable;
|
||||
const root = createRoot(interactable);
|
||||
root.render(selectComponent);
|
||||
parent.appendChild(interactable);
|
||||
}
|
||||
|
||||
protected getFallbackValue(): JSONValue {
|
||||
const { default: schemaDefault, enum: enumValues } = this.preferenceNode.preference.data;
|
||||
return schemaDefault !== undefined ? schemaDefault : enumValues![0];
|
||||
}
|
||||
|
||||
protected doHandleValueChange(): void {
|
||||
this.updateInspection();
|
||||
this.updateSelectOptions();
|
||||
const newValue = this.getDataValue();
|
||||
this.updateModificationStatus();
|
||||
if (document.activeElement !== this.interactable && this.selectComponent.current) {
|
||||
this.selectComponent.current.value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stringified index corresponding to the currently selected value.
|
||||
*/
|
||||
protected getDataValue(): number {
|
||||
const currentValue = this.getValue();
|
||||
let selected = this.enumValues.findIndex(value => PreferenceUtils.deepEqual(value, currentValue));
|
||||
if (selected === -1) {
|
||||
const fallback = this.getFallbackValue();
|
||||
selected = this.enumValues.findIndex(value => PreferenceUtils.deepEqual(value, fallback));
|
||||
}
|
||||
return Math.max(selected, 0);
|
||||
}
|
||||
|
||||
protected handleUserInteraction(selected: number): void {
|
||||
const value = this.enumValues[selected];
|
||||
this.setPreferenceImmediately(value);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceSelectInputRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-select-input-renderer';
|
||||
id = PreferenceSelectInputRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(node: Preference.LeafNode): number {
|
||||
return node.preference.data.enum ? 3 : 0;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceSelectInputRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { Preference } from '../../util/preference-types';
|
||||
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
|
||||
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceStringInputRenderer extends PreferenceLeafNodeRenderer<string, HTMLInputElement> {
|
||||
protected createInteractable(parent: HTMLElement): void {
|
||||
const interactable = document.createElement('input');
|
||||
this.interactable = interactable;
|
||||
interactable.type = 'text';
|
||||
interactable.spellcheck = false;
|
||||
interactable.classList.add('theia-input');
|
||||
interactable.defaultValue = this.getValue() ?? '';
|
||||
interactable.oninput = this.handleUserInteraction.bind(this);
|
||||
interactable.onblur = this.handleBlur.bind(this);
|
||||
parent.appendChild(interactable);
|
||||
}
|
||||
|
||||
protected getFallbackValue(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
protected doHandleValueChange(): void {
|
||||
const currentValue = this.interactable.value;
|
||||
this.updateInspection();
|
||||
const newValue = this.getValue();
|
||||
this.updateModificationStatus();
|
||||
if (newValue !== currentValue) {
|
||||
if (document.activeElement !== this.interactable) {
|
||||
this.interactable.value = newValue ?? this.getDefaultValue();
|
||||
} else {
|
||||
this.handleUserInteraction(); // give priority to the value of the input if it is focused.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected handleUserInteraction(): void {
|
||||
this.setPreferenceWithDebounce(this.interactable.value);
|
||||
}
|
||||
|
||||
protected async handleBlur(): Promise<void> {
|
||||
await this.setPreferenceWithDebounce.flush();
|
||||
this.handleValueChange();
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferenceStringInputRendererContribution extends PreferenceLeafNodeRendererContribution {
|
||||
static ID = 'preference-string-input-renderer';
|
||||
id = PreferenceStringInputRendererContribution.ID;
|
||||
|
||||
canHandleLeafNode(node: Preference.LeafNode): number {
|
||||
return Preference.LeafNode.getType(node) === 'string' ? 2 : 0;
|
||||
}
|
||||
|
||||
createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
|
||||
return container.get(PreferenceStringInputRenderer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { postConstruct, injectable, inject } from '@theia/core/shared/inversify';
|
||||
import throttle = require('@theia/core/shared/lodash.throttle');
|
||||
import * as deepEqual from 'fast-deep-equal';
|
||||
import {
|
||||
CompositeTreeNode,
|
||||
SelectableTreeNode,
|
||||
StatefulWidget,
|
||||
TopDownTreeIterator,
|
||||
ExpandableTreeNode,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { Disposable, DisposableCollection, PreferenceProviderDataChanges, PreferenceProviderProvider, PreferenceSchemaService, PreferenceService, unreachable } from '@theia/core';
|
||||
import { BaseWidget, DEFAULT_SCROLL_OPTIONS } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { PreferenceTreeModel, PreferenceFilterChangeEvent, PreferenceFilterChangeSource } from '../preference-tree-model';
|
||||
import { PreferenceNodeRendererFactory, GeneralPreferenceNodeRenderer } from './components/preference-node-renderer';
|
||||
import { Preference } from '../util/preference-types';
|
||||
import { PreferencesScopeTabBar } from './preference-scope-tabbar-widget';
|
||||
import { PreferenceNodeRendererCreatorRegistry } from './components/preference-node-renderer-creator';
|
||||
import { COMMONLY_USED_SECTION_PREFIX } from '../util/preference-layout';
|
||||
|
||||
export interface PreferencesEditorState {
|
||||
firstVisibleChildID: string,
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferencesEditorWidget extends BaseWidget implements StatefulWidget {
|
||||
static readonly ID = 'settings.editor';
|
||||
static readonly LABEL = 'Settings Editor';
|
||||
|
||||
override scrollOptions = DEFAULT_SCROLL_OPTIONS;
|
||||
|
||||
protected scrollContainer: HTMLDivElement;
|
||||
/**
|
||||
* Guards against scroll events and selection events looping into each other. Set before this widget initiates a selection.
|
||||
*/
|
||||
protected currentModelSelectionId = '';
|
||||
/**
|
||||
* Permits the user to expand multiple nodes without each one being collapsed on a new selection.
|
||||
*/
|
||||
protected lastUserSelection = '';
|
||||
protected isAtScrollTop = true;
|
||||
protected firstVisibleChildID = '';
|
||||
protected renderers = new Map<string, GeneralPreferenceNodeRenderer>();
|
||||
protected preferenceDataKeys = new Map<string, string>();
|
||||
// The commonly used section will duplicate preference ID's, so we'll keep a separate list of them.
|
||||
protected commonlyUsedRenderers = new Map<string, GeneralPreferenceNodeRenderer>();
|
||||
|
||||
@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
|
||||
@inject(PreferenceTreeModel) protected readonly model: PreferenceTreeModel;
|
||||
@inject(PreferenceNodeRendererFactory) protected readonly rendererFactory: PreferenceNodeRendererFactory;
|
||||
@inject(PreferenceNodeRendererCreatorRegistry) protected readonly rendererRegistry: PreferenceNodeRendererCreatorRegistry;
|
||||
@inject(PreferenceSchemaService) protected readonly schemaProvider: PreferenceSchemaService;
|
||||
@inject(PreferencesScopeTabBar) protected readonly tabbar: PreferencesScopeTabBar;
|
||||
@inject(PreferenceProviderProvider) protected readonly providerProvider: PreferenceProviderProvider;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
|
||||
this.id = PreferencesEditorWidget.ID;
|
||||
this.title.label = PreferencesEditorWidget.LABEL;
|
||||
this.addClass('settings-main');
|
||||
this.toDispose.pushAll([
|
||||
this.subscribeToPreferenceProviderChanges(),
|
||||
this.model.onFilterChanged(e => this.handleDisplayChange(e)),
|
||||
this.model.onSelectionChanged(e => this.handleSelectionChange(e)),
|
||||
]);
|
||||
this.createContainers();
|
||||
await this.preferenceService.ready;
|
||||
this.handleDisplayChange({ source: PreferenceFilterChangeSource.Schema });
|
||||
this.rendererRegistry.onDidChange(() => this.handleRegistryChange());
|
||||
}
|
||||
|
||||
protected createContainers(): void {
|
||||
const innerWrapper = document.createElement('div');
|
||||
innerWrapper.classList.add('settings-main-scroll-container');
|
||||
this.scrollContainer = innerWrapper;
|
||||
innerWrapper.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
this.node.appendChild(innerWrapper);
|
||||
const noLeavesMessage = document.createElement('div');
|
||||
noLeavesMessage.classList.add('settings-no-results-announcement');
|
||||
noLeavesMessage.textContent = 'That search query has returned no results.';
|
||||
this.node.appendChild(noLeavesMessage);
|
||||
}
|
||||
|
||||
protected subscribeToPreferenceProviderChanges(): Disposable {
|
||||
const res = new DisposableCollection();
|
||||
for (const scope of this.schemaProvider.validScopes) {
|
||||
const provider = this.providerProvider(scope);
|
||||
if (!provider) { continue; }
|
||||
provider.onDidPreferencesChanged(e => this.handlePreferenceChanges(e), this, res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
protected handleDisplayChange(e: PreferenceFilterChangeEvent): void {
|
||||
const { isFiltered } = this.model;
|
||||
const currentFirstVisible = this.firstVisibleChildID;
|
||||
const leavesAreVisible = this.areLeavesVisible();
|
||||
if (e.source === PreferenceFilterChangeSource.Search) {
|
||||
this.handleSearchChange(isFiltered, leavesAreVisible);
|
||||
} else if (e.source === PreferenceFilterChangeSource.Scope) {
|
||||
this.handleScopeChange(isFiltered);
|
||||
this.showInTree(currentFirstVisible);
|
||||
} else if (e.source === PreferenceFilterChangeSource.Schema) {
|
||||
this.handleSchemaChange(isFiltered);
|
||||
this.showInTree(currentFirstVisible);
|
||||
} else {
|
||||
unreachable(e.source, 'Not all PreferenceFilterChangeSource enum variants handled.');
|
||||
}
|
||||
this.resetScroll(currentFirstVisible, e.source === PreferenceFilterChangeSource.Search);
|
||||
}
|
||||
|
||||
protected handleRegistryChange(): void {
|
||||
for (const [id, renderer, collection] of this.allRenderers()) {
|
||||
renderer.dispose();
|
||||
collection.delete(id);
|
||||
}
|
||||
this.handleDisplayChange({ source: PreferenceFilterChangeSource.Schema });
|
||||
}
|
||||
|
||||
protected handleSchemaChange(isFiltered: boolean): void {
|
||||
for (const [id, renderer, collection] of this.allRenderers()) {
|
||||
const node = this.model.getNode(renderer.nodeId);
|
||||
if (!node || (Preference.LeafNode.is(node) && this.hasSchemaChanged(renderer, node))) {
|
||||
renderer.dispose();
|
||||
collection.delete(id);
|
||||
}
|
||||
}
|
||||
if (this.model.root) {
|
||||
const nodeIterator = Array.from(this.scrollContainer.children)[Symbol.iterator]();
|
||||
let nextNode: HTMLElement | undefined = nodeIterator.next().value;
|
||||
for (const node of new TopDownTreeIterator(this.model.root)) {
|
||||
if (Preference.TreeNode.is(node)) {
|
||||
const { collection, id } = this.analyzeIDAndGetRendererGroup(node.id);
|
||||
const renderer = collection.get(id) ?? this.rendererFactory(node);
|
||||
if (!renderer.node.parentElement) { // If it hasn't been attached yet, it hasn't been checked for the current search.
|
||||
this.hideIfFailsFilters(renderer, isFiltered);
|
||||
collection.set(id, renderer);
|
||||
}
|
||||
if (nextNode !== renderer.node) {
|
||||
if (nextNode) {
|
||||
renderer.insertBefore(nextNode);
|
||||
} else {
|
||||
renderer.appendTo(this.scrollContainer);
|
||||
}
|
||||
} else {
|
||||
nextNode = nodeIterator.next().value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected handleScopeChange(isFiltered: boolean = this.model.isFiltered): void {
|
||||
for (const [, renderer] of this.allRenderers()) {
|
||||
const isHidden = this.hideIfFailsFilters(renderer, isFiltered);
|
||||
if (isFiltered || !isHidden) {
|
||||
renderer.handleScopeChange?.(isFiltered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected hasSchemaChanged(renderer: GeneralPreferenceNodeRenderer, node: Preference.LeafNode): boolean {
|
||||
return !deepEqual(renderer.schema, node.preference.data);
|
||||
}
|
||||
|
||||
protected handleSearchChange(isFiltered: boolean, leavesAreVisible: boolean): void {
|
||||
if (leavesAreVisible) {
|
||||
for (const [, renderer] of this.allRenderers()) {
|
||||
const isHidden = this.hideIfFailsFilters(renderer, isFiltered);
|
||||
if (!isHidden) {
|
||||
renderer.handleSearchChange?.(isFiltered);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected areLeavesVisible(): boolean {
|
||||
const leavesAreVisible = this.model.totalVisibleLeaves > 0;
|
||||
this.node.classList.toggle('no-results', !leavesAreVisible);
|
||||
this.scrollContainer.classList.toggle('hidden', !leavesAreVisible);
|
||||
return leavesAreVisible;
|
||||
}
|
||||
|
||||
protected *allRenderers(): IterableIterator<[string, GeneralPreferenceNodeRenderer, Map<string, GeneralPreferenceNodeRenderer>]> {
|
||||
for (const [id, renderer] of this.commonlyUsedRenderers.entries()) {
|
||||
yield [id, renderer, this.commonlyUsedRenderers];
|
||||
}
|
||||
for (const [id, renderer] of this.renderers.entries()) {
|
||||
yield [id, renderer, this.renderers];
|
||||
}
|
||||
}
|
||||
|
||||
protected handlePreferenceChanges(e: PreferenceProviderDataChanges): void {
|
||||
for (const id of Object.keys(e)) {
|
||||
this.commonlyUsedRenderers.get(id)?.handleValueChange?.();
|
||||
this.renderers.get(id)?.handleValueChange?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the renderer is hidden, false otherwise.
|
||||
*/
|
||||
protected hideIfFailsFilters(renderer: GeneralPreferenceNodeRenderer, isFiltered: boolean): boolean {
|
||||
const row = this.model.currentRows.get(renderer.nodeId);
|
||||
if (!row || (CompositeTreeNode.is(row.node) && (isFiltered || row.visibleChildren === 0))) {
|
||||
renderer.hide();
|
||||
return true;
|
||||
} else {
|
||||
renderer.show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected resetScroll(nodeIDToScrollTo?: string, filterWasCleared: boolean = false): void {
|
||||
if (this.scrollBar) { // Absent on widget creation
|
||||
this.doResetScroll(nodeIDToScrollTo, filterWasCleared);
|
||||
} else {
|
||||
const interval = setInterval(() => {
|
||||
if (this.scrollBar) {
|
||||
clearInterval(interval);
|
||||
this.doResetScroll(nodeIDToScrollTo, filterWasCleared);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
protected doResetScroll(nodeIDToScrollTo?: string, filterWasModified: boolean = false): void {
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollBar?.update();
|
||||
if (filterWasModified) {
|
||||
this.scrollContainer.scrollTop = 0;
|
||||
} else if (nodeIDToScrollTo) {
|
||||
const { id, collection } = this.analyzeIDAndGetRendererGroup(nodeIDToScrollTo);
|
||||
const renderer = collection.get(id);
|
||||
if (renderer?.visible) {
|
||||
this.scrollContainer.scrollTo(0, renderer.node.offsetTop);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
protected doOnScroll(): void {
|
||||
const { scrollContainer } = this;
|
||||
const firstVisibleChildID = this.findFirstVisibleChildID();
|
||||
this.setFirstVisibleChildID(firstVisibleChildID);
|
||||
if (this.isAtScrollTop && scrollContainer.scrollTop !== 0) {
|
||||
this.isAtScrollTop = false;
|
||||
this.tabbar.toggleShadow(true);
|
||||
} else if (!this.isAtScrollTop && scrollContainer.scrollTop === 0) {
|
||||
this.isAtScrollTop = true;
|
||||
this.tabbar.toggleShadow(false);
|
||||
}
|
||||
};
|
||||
|
||||
onScroll = throttle(this.doOnScroll.bind(this), 50);
|
||||
|
||||
protected findFirstVisibleChildID(): string | undefined {
|
||||
const { scrollTop } = this.scrollContainer;
|
||||
for (const [, renderer] of this.allRenderers()) {
|
||||
const { offsetTop, offsetHeight } = renderer.node;
|
||||
if (Math.abs(offsetTop - scrollTop) <= offsetHeight / 2) {
|
||||
return renderer.nodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldUpdateModelSelection = true;
|
||||
|
||||
protected setFirstVisibleChildID(id?: string): void {
|
||||
if (id && id !== this.firstVisibleChildID) {
|
||||
this.firstVisibleChildID = id;
|
||||
if (!this.shouldUpdateModelSelection) { return; }
|
||||
this.showInTree(id);
|
||||
}
|
||||
}
|
||||
|
||||
protected showInTree(id: string): void {
|
||||
let currentNode = this.model.getNode(id);
|
||||
let expansionAncestor;
|
||||
let selectionAncestor;
|
||||
while (currentNode && (!expansionAncestor || !selectionAncestor)) {
|
||||
if (!selectionAncestor && SelectableTreeNode.is(currentNode)) {
|
||||
selectionAncestor = currentNode;
|
||||
}
|
||||
if (!expansionAncestor && ExpandableTreeNode.is(currentNode)) {
|
||||
expansionAncestor = currentNode;
|
||||
}
|
||||
currentNode = currentNode.parent;
|
||||
}
|
||||
if (selectionAncestor) {
|
||||
this.currentModelSelectionId = selectionAncestor.id;
|
||||
expansionAncestor = expansionAncestor ?? selectionAncestor;
|
||||
this.model.selectIfNotSelected(selectionAncestor);
|
||||
if (!this.model.isFiltered && id !== this.lastUserSelection) {
|
||||
this.lastUserSelection = '';
|
||||
this.model.collapseAllExcept(expansionAncestor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected handleSelectionChange(selectionEvent: readonly Readonly<SelectableTreeNode>[]): void {
|
||||
const node = selectionEvent[0];
|
||||
if (node && node.id !== this.currentModelSelectionId) {
|
||||
this.currentModelSelectionId = node.id;
|
||||
this.lastUserSelection = node.id;
|
||||
if (this.model.isFiltered && CompositeTreeNode.is(node)) {
|
||||
for (const candidate of new TopDownTreeIterator(node, { pruneSiblings: true })) {
|
||||
const { id, collection } = this.analyzeIDAndGetRendererGroup(candidate.id);
|
||||
const renderer = collection.get(id);
|
||||
if (renderer?.visible) {
|
||||
// When filtered, treat the first visible child as the selected node, since it will be the one scrolled to.
|
||||
this.lastUserSelection = renderer.nodeId;
|
||||
this.scrollWithoutModelUpdate(renderer.node);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { id, collection } = this.analyzeIDAndGetRendererGroup(node.id);
|
||||
const renderer = collection.get(id);
|
||||
this.scrollWithoutModelUpdate(renderer?.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensures that we don't set the model's selection while attempting to scroll in reaction to a model selection change. */
|
||||
protected scrollWithoutModelUpdate(node?: HTMLElement): void {
|
||||
if (!node) { return; }
|
||||
this.shouldUpdateModelSelection = false;
|
||||
this.scrollContainer.scrollTo(0, node.offsetTop);
|
||||
requestAnimationFrame(() => this.shouldUpdateModelSelection = true);
|
||||
}
|
||||
|
||||
protected analyzeIDAndGetRendererGroup(nodeID: string): { id: string, group: string, collection: Map<string, GeneralPreferenceNodeRenderer> } {
|
||||
const { id, group } = Preference.TreeNode.getGroupAndIdFromNodeId(nodeID);
|
||||
const collection = group === COMMONLY_USED_SECTION_PREFIX ? this.commonlyUsedRenderers : this.renderers;
|
||||
return { id, group, collection };
|
||||
}
|
||||
|
||||
protected override getScrollContainer(): HTMLElement {
|
||||
return this.scrollContainer;
|
||||
}
|
||||
|
||||
storeState(): PreferencesEditorState {
|
||||
return {
|
||||
firstVisibleChildID: this.firstVisibleChildID,
|
||||
};
|
||||
}
|
||||
|
||||
restoreState(oldState: PreferencesEditorState): void {
|
||||
this.firstVisibleChildID = oldState.firstVisibleChildID;
|
||||
this.resetScroll(this.firstVisibleChildID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { TabBar, Widget, Title } from '@theia/core/shared/@lumino/widgets';
|
||||
import { Message, ContextMenuRenderer, LabelProvider, StatefulWidget, codicon } from '@theia/core/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { PreferenceScopeCommandManager } from '../util/preference-scope-command-manager';
|
||||
import { Preference, PreferenceMenus } from '../util/preference-types';
|
||||
import { CommandRegistry, DisposableCollection, Emitter, MenuModelRegistry, PreferenceScope } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
const USER_TAB_LABEL = nls.localizeByDefault('User');
|
||||
const USER_TAB_INDEX = PreferenceScope['User'];
|
||||
const WORKSPACE_TAB_LABEL = nls.localizeByDefault('Workspace');
|
||||
const WORKSPACE_TAB_INDEX = PreferenceScope['Workspace'];
|
||||
const FOLDER_TAB_LABEL = nls.localizeByDefault('Folder');
|
||||
const FOLDER_TAB_INDEX = PreferenceScope['Folder'];
|
||||
|
||||
const PREFERENCE_TAB_CLASSNAME = 'preferences-scope-tab';
|
||||
const GENERAL_FOLDER_TAB_CLASSNAME = 'preference-folder';
|
||||
const LABELED_FOLDER_TAB_CLASSNAME = 'preferences-folder-tab';
|
||||
const FOLDER_DROPDOWN_CLASSNAME = 'preferences-folder-dropdown';
|
||||
const FOLDER_DROPDOWN_ICON_CLASSNAME = 'preferences-folder-dropdown-icon ' + codicon('chevron-down');
|
||||
const TABBAR_UNDERLINE_CLASSNAME = 'tabbar-underline';
|
||||
const SINGLE_FOLDER_TAB_CLASSNAME = `${PREFERENCE_TAB_CLASSNAME} ${GENERAL_FOLDER_TAB_CLASSNAME} ${LABELED_FOLDER_TAB_CLASSNAME}`;
|
||||
const UNSELECTED_FOLDER_DROPDOWN_CLASSNAME = `${PREFERENCE_TAB_CLASSNAME} ${GENERAL_FOLDER_TAB_CLASSNAME} ${FOLDER_DROPDOWN_CLASSNAME}`;
|
||||
const SELECTED_FOLDER_DROPDOWN_CLASSNAME = `${PREFERENCE_TAB_CLASSNAME} ${GENERAL_FOLDER_TAB_CLASSNAME} ${LABELED_FOLDER_TAB_CLASSNAME} ${FOLDER_DROPDOWN_CLASSNAME}`;
|
||||
const SHADOW_CLASSNAME = 'with-shadow';
|
||||
|
||||
export interface PreferencesScopeTabBarState {
|
||||
scopeDetails: Preference.SelectedScopeDetails;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferencesScopeTabBar extends TabBar<Widget> implements StatefulWidget {
|
||||
|
||||
static ID = 'preferences-scope-tab-bar';
|
||||
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
|
||||
@inject(PreferenceScopeCommandManager) protected readonly preferencesMenuFactory: PreferenceScopeCommandManager;
|
||||
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
|
||||
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry) protected readonly menuModelRegistry: MenuModelRegistry;
|
||||
|
||||
protected readonly onScopeChangedEmitter = new Emitter<Preference.SelectedScopeDetails>();
|
||||
readonly onScopeChanged = this.onScopeChangedEmitter.event;
|
||||
|
||||
protected toDispose = new DisposableCollection();
|
||||
protected folderTitle: Title<Widget>;
|
||||
protected currentWorkspaceRoots: FileStat[] = [];
|
||||
protected currentSelection: Preference.SelectedScopeDetails = Preference.DEFAULT_SCOPE;
|
||||
protected editorScrollAtTop = true;
|
||||
|
||||
get currentScope(): Preference.SelectedScopeDetails {
|
||||
return this.currentSelection;
|
||||
}
|
||||
|
||||
protected setNewScopeSelection(newSelection: Preference.SelectedScopeDetails): void {
|
||||
const stringifiedSelectionScope = newSelection.scope.toString();
|
||||
const newIndex = this.titles.findIndex(title => title.dataset.scope === stringifiedSelectionScope);
|
||||
if (newIndex !== -1) {
|
||||
this.currentSelection = newSelection;
|
||||
this.currentIndex = newIndex;
|
||||
if (newSelection.scope === PreferenceScope.Folder) {
|
||||
this.addOrUpdateFolderTab();
|
||||
}
|
||||
this.emitNewScope();
|
||||
}
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = PreferencesScopeTabBar.ID;
|
||||
this.setupInitialDisplay();
|
||||
|
||||
this.tabActivateRequested.connect((sender, args) => {
|
||||
const scopeDetails = this.toScopeDetails(args.title);
|
||||
if (scopeDetails) {
|
||||
this.setNewScopeSelection(scopeDetails);
|
||||
}
|
||||
});
|
||||
this.toDispose.pushAll([
|
||||
this.workspaceService.onWorkspaceChanged(newRoots => this.doUpdateDisplay(newRoots)),
|
||||
this.workspaceService.onWorkspaceLocationChanged(() => this.doUpdateDisplay(this.workspaceService.tryGetRoots())),
|
||||
]);
|
||||
const tabUnderline = document.createElement('div');
|
||||
tabUnderline.className = TABBAR_UNDERLINE_CLASSNAME;
|
||||
this.node.append(tabUnderline);
|
||||
}
|
||||
|
||||
protected toScopeDetails(title?: Title<Widget> | Preference.SelectedScopeDetails): Preference.SelectedScopeDetails | undefined {
|
||||
if (title) {
|
||||
const source = 'dataset' in title ? title.dataset : title;
|
||||
const { scope, uri, activeScopeIsFolder } = source;
|
||||
return {
|
||||
scope: Number(scope),
|
||||
uri: uri || undefined,
|
||||
activeScopeIsFolder: activeScopeIsFolder === 'true' || activeScopeIsFolder === true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected toDataSet(scopeDetails: Preference.SelectedScopeDetails): Title.Dataset {
|
||||
const { scope, uri, activeScopeIsFolder } = scopeDetails;
|
||||
return {
|
||||
scope: scope.toString(),
|
||||
uri: uri ?? '',
|
||||
activeScopeIsFolder: activeScopeIsFolder.toString()
|
||||
};
|
||||
}
|
||||
|
||||
protected setupInitialDisplay(): void {
|
||||
this.addUserTab();
|
||||
if (this.workspaceService.workspace) {
|
||||
this.addWorkspaceTab(this.workspaceService.workspace);
|
||||
}
|
||||
this.addOrUpdateFolderTab();
|
||||
}
|
||||
|
||||
protected override onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.addTabIndexToTabs();
|
||||
}
|
||||
|
||||
protected addTabIndexToTabs(): void {
|
||||
this.node.querySelectorAll('li').forEach((tab, index) => {
|
||||
tab.tabIndex = 0;
|
||||
const handler = () => {
|
||||
if (tab.className.includes(GENERAL_FOLDER_TAB_CLASSNAME) && this.currentWorkspaceRoots.length > 1) {
|
||||
const tabRect = tab.getBoundingClientRect();
|
||||
this.openContextMenu(tabRect, tab, 'keypress');
|
||||
} else {
|
||||
const details = this.toScopeDetails(this.titles[index]);
|
||||
if (details) {
|
||||
this.setNewScopeSelection(details);
|
||||
}
|
||||
}
|
||||
};
|
||||
tab.onkeydown = handler;
|
||||
tab.onclick = handler;
|
||||
});
|
||||
}
|
||||
|
||||
protected addUserTab(): void {
|
||||
this.addTab(new Title({
|
||||
dataset: { uri: '', scope: USER_TAB_INDEX.toString() },
|
||||
label: USER_TAB_LABEL,
|
||||
owner: this,
|
||||
className: PREFERENCE_TAB_CLASSNAME
|
||||
}));
|
||||
}
|
||||
|
||||
protected addWorkspaceTab(currentWorkspace: FileStat): Title<Widget> {
|
||||
const scopeDetails = this.getWorkspaceDataset(currentWorkspace);
|
||||
const workspaceTabTitle = new Title({
|
||||
dataset: this.toDataSet(scopeDetails),
|
||||
label: WORKSPACE_TAB_LABEL,
|
||||
owner: this,
|
||||
className: PREFERENCE_TAB_CLASSNAME,
|
||||
});
|
||||
this.addTab(workspaceTabTitle);
|
||||
return workspaceTabTitle;
|
||||
}
|
||||
|
||||
protected getWorkspaceDataset(currentWorkspace: FileStat): Preference.SelectedScopeDetails {
|
||||
const { resource, isDirectory } = currentWorkspace;
|
||||
const scope = WORKSPACE_TAB_INDEX;
|
||||
return { uri: resource.toString(), activeScopeIsFolder: isDirectory, scope };
|
||||
}
|
||||
|
||||
protected addOrUpdateFolderTab(): void {
|
||||
if (!!this.workspaceService.workspace) {
|
||||
this.currentWorkspaceRoots = this.workspaceService.tryGetRoots();
|
||||
const multipleFolderRootsAreAvailable = this.currentWorkspaceRoots && this.currentWorkspaceRoots.length > 1;
|
||||
const noFolderRootsAreAvailable = this.currentWorkspaceRoots.length === 0;
|
||||
const shouldShowFoldersSeparately = this.workspaceService.saved;
|
||||
|
||||
if (!noFolderRootsAreAvailable) {
|
||||
if (!this.folderTitle) {
|
||||
this.folderTitle = new Title({
|
||||
label: '',
|
||||
caption: FOLDER_TAB_LABEL,
|
||||
owner: this,
|
||||
});
|
||||
}
|
||||
|
||||
this.setFolderTitleProperties(multipleFolderRootsAreAvailable);
|
||||
if (multipleFolderRootsAreAvailable || shouldShowFoldersSeparately) {
|
||||
this.addTab(this.folderTitle);
|
||||
}
|
||||
} else {
|
||||
const folderTabIndex = this.titles.findIndex(title => title.caption === FOLDER_TAB_LABEL);
|
||||
|
||||
if (folderTabIndex > -1) {
|
||||
this.removeTabAt(folderTabIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected setFolderTitleProperties(multipleFolderRootsAreAvailable: boolean): void {
|
||||
this.folderTitle.iconClass = multipleFolderRootsAreAvailable ? FOLDER_DROPDOWN_ICON_CLASSNAME : '';
|
||||
if (this.currentSelection.scope === FOLDER_TAB_INDEX) {
|
||||
this.folderTitle.label = this.labelProvider.getName(new URI(this.currentSelection.uri));
|
||||
this.folderTitle.dataset = this.toDataSet(this.currentSelection);
|
||||
this.folderTitle.className = multipleFolderRootsAreAvailable ? SELECTED_FOLDER_DROPDOWN_CLASSNAME : SINGLE_FOLDER_TAB_CLASSNAME;
|
||||
} else {
|
||||
const singleFolderRoot = this.currentWorkspaceRoots[0].resource;
|
||||
const singleFolderLabel = this.labelProvider.getName(singleFolderRoot);
|
||||
const defaultURI = multipleFolderRootsAreAvailable ? '' : singleFolderRoot.toString();
|
||||
this.folderTitle.label = multipleFolderRootsAreAvailable ? FOLDER_TAB_LABEL : singleFolderLabel;
|
||||
this.folderTitle.className = multipleFolderRootsAreAvailable ? UNSELECTED_FOLDER_DROPDOWN_CLASSNAME : SINGLE_FOLDER_TAB_CLASSNAME;
|
||||
this.folderTitle.dataset = { folderTitle: 'true', scope: FOLDER_TAB_INDEX.toString(), uri: defaultURI };
|
||||
}
|
||||
}
|
||||
|
||||
protected folderSelectionCallback = (newScope: Preference.SelectedScopeDetails): void => { this.setNewScopeSelection(newScope); };
|
||||
|
||||
protected getFolderContextMenu(workspaceRoots = this.workspaceService.tryGetRoots()): void {
|
||||
this.preferencesMenuFactory.createFolderWorkspacesMenu(workspaceRoots, this.currentSelection.uri);
|
||||
}
|
||||
|
||||
override handleEvent(): void {
|
||||
// Don't - the handlers are defined in PreferenceScopeTabbarWidget.addTabIndexToTabs()
|
||||
}
|
||||
|
||||
protected openContextMenu(tabRect: DOMRect | ClientRect, folderTabNode: HTMLElement, source: 'click' | 'keypress'): void {
|
||||
const toDisposeOnHide = new DisposableCollection();
|
||||
for (const root of this.workspaceService.tryGetRoots()) {
|
||||
const id = `set-scope-to-${root.resource.toString()}`;
|
||||
toDisposeOnHide.pushAll([
|
||||
this.commandRegistry.registerCommand(
|
||||
{ id },
|
||||
{ execute: () => this.setScope(root.resource) }
|
||||
),
|
||||
this.menuModelRegistry.registerMenuAction(PreferenceMenus.FOLDER_SCOPE_MENU_PATH,
|
||||
{
|
||||
commandId: id,
|
||||
label: this.labelProvider.getName(root),
|
||||
}
|
||||
)
|
||||
]);
|
||||
}
|
||||
this.contextMenuRenderer.render({
|
||||
menuPath: PreferenceMenus.FOLDER_SCOPE_MENU_PATH,
|
||||
anchor: { x: tabRect.left, y: tabRect.bottom },
|
||||
context: folderTabNode,
|
||||
onHide: () => {
|
||||
setTimeout(() => toDisposeOnHide.dispose());
|
||||
if (source === 'click') { folderTabNode.blur(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected doUpdateDisplay(newRoots: FileStat[]): void {
|
||||
const folderWasRemoved = newRoots.length < this.currentWorkspaceRoots.length;
|
||||
this.currentWorkspaceRoots = newRoots;
|
||||
if (folderWasRemoved) {
|
||||
const removedFolderWasSelectedScope = !this.currentWorkspaceRoots.some(root => root.resource.toString() === this.currentSelection.uri);
|
||||
if (removedFolderWasSelectedScope) {
|
||||
this.setNewScopeSelection(Preference.DEFAULT_SCOPE);
|
||||
}
|
||||
}
|
||||
this.updateWorkspaceTab();
|
||||
this.addOrUpdateFolderTab();
|
||||
}
|
||||
|
||||
protected updateWorkspaceTab(): void {
|
||||
const currentWorkspace = this.workspaceService.workspace;
|
||||
if (currentWorkspace) {
|
||||
const workspaceTitle = this.titles.find(title => title.label === WORKSPACE_TAB_LABEL) ?? this.addWorkspaceTab(currentWorkspace);
|
||||
const scopeDetails = this.getWorkspaceDataset(currentWorkspace);
|
||||
workspaceTitle.dataset = this.toDataSet(scopeDetails);
|
||||
if (this.currentSelection.scope === PreferenceScope.Workspace) {
|
||||
this.setNewScopeSelection(scopeDetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected emitNewScope(): void {
|
||||
this.onScopeChangedEmitter.fire(this.currentSelection);
|
||||
}
|
||||
|
||||
setScope(scope: PreferenceScope.User | PreferenceScope.Workspace | URI): void {
|
||||
const details = scope instanceof URI ? this.getDetailsForResource(scope) : this.getDetailsForScope(scope);
|
||||
if (details) {
|
||||
this.setNewScopeSelection(details);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDetailsForScope(scope: PreferenceScope.User | PreferenceScope.Workspace): Preference.SelectedScopeDetails | undefined {
|
||||
const stringifiedSelectionScope = scope.toString();
|
||||
const correspondingTitle = this.titles.find(title => title.dataset.scope === stringifiedSelectionScope);
|
||||
return this.toScopeDetails(correspondingTitle);
|
||||
}
|
||||
|
||||
protected getDetailsForResource(resource: URI): Preference.SelectedScopeDetails | undefined {
|
||||
const parent = this.workspaceService.getWorkspaceRootUri(resource);
|
||||
if (!parent) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.workspaceService.isMultiRootWorkspaceOpened) {
|
||||
return this.getDetailsForScope(PreferenceScope.Workspace);
|
||||
}
|
||||
return ({ scope: PreferenceScope.Folder, uri: parent.toString(), activeScopeIsFolder: true });
|
||||
}
|
||||
|
||||
storeState(): PreferencesScopeTabBarState {
|
||||
return {
|
||||
scopeDetails: this.currentScope
|
||||
};
|
||||
}
|
||||
|
||||
restoreState(oldState: PreferencesScopeTabBarState): void {
|
||||
const scopeDetails = this.toScopeDetails(oldState.scopeDetails);
|
||||
if (scopeDetails) {
|
||||
this.setNewScopeSelection(scopeDetails);
|
||||
}
|
||||
}
|
||||
|
||||
toggleShadow(showShadow: boolean): void {
|
||||
this.toggleClass(SHADOW_CLASSNAME, showShadow);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { codicon, ReactWidget, StatefulWidget, Widget } from '@theia/core/lib/browser';
|
||||
import { injectable, postConstruct, unmanaged } from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import debounce = require('p-debounce');
|
||||
import { Emitter } from '@theia/core';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
export interface PreferencesSearchbarState {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferencesSearchbarWidget extends ReactWidget implements StatefulWidget {
|
||||
static readonly ID = 'settings.header';
|
||||
static readonly LABEL = 'Settings Header';
|
||||
static readonly SEARCHBAR_ID = 'preference-searchbar';
|
||||
|
||||
protected readonly onFilterStringChangedEmitter = new Emitter<string>();
|
||||
readonly onFilterChanged = this.onFilterStringChangedEmitter.event;
|
||||
|
||||
protected searchbarRef: React.RefObject<HTMLInputElement> = React.createRef<HTMLInputElement>();
|
||||
protected resultsCount: number = 0;
|
||||
|
||||
constructor(@unmanaged() options?: Widget.IOptions) {
|
||||
super(options);
|
||||
this.focus = this.focus.bind(this);
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = PreferencesSearchbarWidget.ID;
|
||||
this.title.label = PreferencesSearchbarWidget.LABEL;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected handleSearch = (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => this.search(e.target.value);
|
||||
|
||||
protected search = debounce(async (value: string) => {
|
||||
this.onFilterStringChangedEmitter.fire(value);
|
||||
this.update();
|
||||
}, 200);
|
||||
|
||||
focus(): void {
|
||||
if (this.searchbarRef.current) {
|
||||
this.searchbarRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the search input and all search results.
|
||||
* @param e on-click mouse event.
|
||||
*/
|
||||
protected clearSearchResults = async (e: React.MouseEvent): Promise<void> => {
|
||||
const search = document.getElementById(PreferencesSearchbarWidget.SEARCHBAR_ID) as HTMLInputElement;
|
||||
if (search) {
|
||||
search.value = '';
|
||||
await this.search(search.value);
|
||||
this.update();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders all search bar options.
|
||||
*/
|
||||
protected renderOptionContainer(): React.ReactNode {
|
||||
const resultsCount = this.renderResultsCountOption();
|
||||
const clearAllOption = this.renderClearAllOption();
|
||||
return <div className="option-buttons"> {resultsCount} {clearAllOption} </div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a badge displaying search results count.
|
||||
*/
|
||||
protected renderResultsCountOption(): React.ReactNode {
|
||||
let resultsFound: string;
|
||||
if (this.resultsCount === 0) {
|
||||
resultsFound = nls.localizeByDefault('No Settings Found');
|
||||
} else if (this.resultsCount === 1) {
|
||||
resultsFound = nls.localizeByDefault('1 Setting Found');
|
||||
} else {
|
||||
resultsFound = nls.localizeByDefault('{0} Settings Found', this.resultsCount.toFixed(0));
|
||||
}
|
||||
return this.searchTermExists() ?
|
||||
(<span
|
||||
className="results-found"
|
||||
title={resultsFound}>
|
||||
{resultsFound}
|
||||
</span>)
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a clear all button.
|
||||
*/
|
||||
protected renderClearAllOption(): React.ReactNode {
|
||||
return <span
|
||||
className={`${codicon('clear-all')} option ${(this.searchTermExists() ? 'enabled' : '')}`}
|
||||
title={nls.localizeByDefault('Clear Search Results')}
|
||||
onClick={this.clearSearchResults}
|
||||
/>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the search input currently has a value.
|
||||
* @returns true, if the search input currently has a value; false, otherwise.
|
||||
*/
|
||||
protected searchTermExists(): boolean {
|
||||
return !!this.searchbarRef.current?.value;
|
||||
}
|
||||
|
||||
protected getSearchTerm(): string {
|
||||
const search = document.getElementById(PreferencesSearchbarWidget.SEARCHBAR_ID) as HTMLInputElement;
|
||||
return search?.value;
|
||||
}
|
||||
|
||||
async updateSearchTerm(searchTerm: string): Promise<void> {
|
||||
const search = document.getElementById(PreferencesSearchbarWidget.SEARCHBAR_ID) as HTMLInputElement;
|
||||
if (!search || search.value === searchTerm) {
|
||||
return;
|
||||
}
|
||||
search.value = searchTerm;
|
||||
await this.search(search.value);
|
||||
this.update();
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const optionContainer = this.renderOptionContainer();
|
||||
return (
|
||||
<div className='settings-header'>
|
||||
<div className="settings-search-container" ref={this.focus}>
|
||||
<input
|
||||
type="text"
|
||||
id={PreferencesSearchbarWidget.SEARCHBAR_ID}
|
||||
spellCheck={false}
|
||||
placeholder={nls.localizeByDefault('Search settings')}
|
||||
className="settings-search-input theia-input"
|
||||
onChange={this.handleSearch}
|
||||
ref={this.searchbarRef}
|
||||
/>
|
||||
{optionContainer}
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the search result count.
|
||||
* @param count the result count.
|
||||
*/
|
||||
updateResultsCount(count: number): void {
|
||||
this.resultsCount = count;
|
||||
this.update();
|
||||
}
|
||||
|
||||
storeState(): PreferencesSearchbarState {
|
||||
return {
|
||||
searchTerm: this.getSearchTerm()
|
||||
};
|
||||
}
|
||||
|
||||
restoreState(oldState: PreferencesSearchbarState): void {
|
||||
const searchInputExists = this.onDidChangeVisibility(() => {
|
||||
this.updateSearchTerm(oldState.searchTerm || '');
|
||||
searchInputExists.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
ExpandableTreeNode,
|
||||
TreeNode,
|
||||
TreeProps,
|
||||
TreeWidget,
|
||||
TREE_NODE_CONTENT_CLASS,
|
||||
} from '@theia/core/lib/browser';
|
||||
import React = require('@theia/core/shared/react');
|
||||
import { PreferenceTreeModel, PreferenceTreeNodeRow, PreferenceTreeNodeProps } from '../preference-tree-model';
|
||||
import { Preference } from '../util/preference-types';
|
||||
|
||||
@injectable()
|
||||
export class PreferencesTreeWidget extends TreeWidget {
|
||||
static ID = 'preferences.tree';
|
||||
|
||||
protected shouldFireSelectionEvents: boolean = true;
|
||||
protected firstVisibleLeafNodeID: string;
|
||||
|
||||
@inject(PreferenceTreeModel) override readonly model: PreferenceTreeModel;
|
||||
@inject(TreeProps) protected readonly treeProps: TreeProps;
|
||||
|
||||
@postConstruct()
|
||||
override init(): void {
|
||||
super.init();
|
||||
this.id = PreferencesTreeWidget.ID;
|
||||
this.toDispose.pushAll([
|
||||
this.model.onFilterChanged(() => {
|
||||
this.updateRows();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
override doUpdateRows(): void {
|
||||
this.rows = new Map();
|
||||
let index = 0;
|
||||
for (const [id, nodeRow] of this.model.currentRows.entries()) {
|
||||
if (nodeRow.visibleChildren > 0 && this.isVisibleNode(nodeRow.node)) {
|
||||
this.rows.set(id, { ...nodeRow, index: index++ });
|
||||
}
|
||||
}
|
||||
this.updateScrollToRow();
|
||||
}
|
||||
|
||||
protected isVisibleNode(node: Preference.TreeNode): boolean {
|
||||
if (Preference.TreeNode.isTopLevel(node)) {
|
||||
return true;
|
||||
} else {
|
||||
return ExpandableTreeNode.isExpanded(node.parent) && Preference.TreeNode.is(node.parent) && this.isVisibleNode(node.parent);
|
||||
}
|
||||
}
|
||||
|
||||
protected override doRenderNodeRow({ depth, visibleChildren, node, isExpansible }: PreferenceTreeNodeRow): React.ReactNode {
|
||||
return this.renderNode(node, { depth, visibleChildren, isExpansible });
|
||||
}
|
||||
|
||||
protected override renderNode(node: TreeNode, props: PreferenceTreeNodeProps): React.ReactNode {
|
||||
if (!TreeNode.isVisible(node)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attributes = this.createNodeAttributes(node, props);
|
||||
|
||||
const content = <div className={TREE_NODE_CONTENT_CLASS}>
|
||||
{this.renderExpansionToggle(node, props)}
|
||||
{this.renderCaption(node, props)}
|
||||
</div>;
|
||||
return React.createElement('div', attributes, content);
|
||||
}
|
||||
|
||||
protected override renderExpansionToggle(node: TreeNode, props: PreferenceTreeNodeProps): React.ReactNode {
|
||||
if (ExpandableTreeNode.is(node) && !props.isExpansible) {
|
||||
return <div className='preferences-tree-spacer' />;
|
||||
}
|
||||
return super.renderExpansionToggle(node, props);
|
||||
}
|
||||
|
||||
protected override toNodeName(node: TreeNode): string {
|
||||
const visibleChildren = this.model.currentRows.get(node.id)?.visibleChildren;
|
||||
const baseName = this.labelProvider.getName(node);
|
||||
const printedNameWithVisibleChildren = this.model.isFiltered && visibleChildren !== undefined
|
||||
? `${baseName} (${visibleChildren})`
|
||||
: baseName;
|
||||
return printedNameWithVisibleChildren;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { createTreeContainer, LabelProviderContribution, WidgetFactory } from '@theia/core/lib/browser';
|
||||
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { Container, interfaces } from '@theia/core/shared/inversify';
|
||||
import { PreferenceTreeModel } from '../preference-tree-model';
|
||||
import { PreferenceTreeLabelProvider } from '../util/preference-tree-label-provider';
|
||||
import { Preference } from '../util/preference-types';
|
||||
import { PreferenceArrayInputRenderer, PreferenceArrayInputRendererContribution } from './components/preference-array-input';
|
||||
import { PreferenceBooleanInputRenderer, PreferenceBooleanInputRendererContribution } from './components/preference-boolean-input';
|
||||
import { PreferenceSingleFilePathInputRenderer, PreferenceSingleFilePathInputRendererContribution } from './components/preference-file-input';
|
||||
import { PreferenceJSONLinkRenderer, PreferenceJSONLinkRendererContribution } from './components/preference-json-input';
|
||||
import { PreferenceHeaderRenderer, PreferenceNodeRendererFactory } from './components/preference-node-renderer';
|
||||
import {
|
||||
DefaultPreferenceNodeRendererCreatorRegistry, PreferenceHeaderRendererContribution, PreferenceNodeRendererContribution, PreferenceNodeRendererCreatorRegistry
|
||||
} from './components/preference-node-renderer-creator';
|
||||
import { PreferenceNumberInputRenderer, PreferenceNumberInputRendererContribution } from './components/preference-number-input';
|
||||
import { PreferenceSelectInputRenderer, PreferenceSelectInputRendererContribution } from './components/preference-select-input';
|
||||
import { PreferenceStringInputRenderer, PreferenceStringInputRendererContribution } from './components/preference-string-input';
|
||||
import { PreferenceMarkdownRenderer } from './components/preference-markdown-renderer';
|
||||
import { PreferencesEditorWidget } from './preference-editor-widget';
|
||||
import { PreferencesScopeTabBar } from './preference-scope-tabbar-widget';
|
||||
import { PreferencesSearchbarWidget } from './preference-searchbar-widget';
|
||||
import { PreferencesTreeWidget } from './preference-tree-widget';
|
||||
import { PreferencesWidget } from './preference-widget';
|
||||
import { PreferenceNullInputRenderer, PreferenceNullRendererContribution } from './components/preference-null-input';
|
||||
|
||||
export function bindPreferencesWidgets(bind: interfaces.Bind): void {
|
||||
bind(PreferenceTreeLabelProvider).toSelf().inSingletonScope();
|
||||
bind(LabelProviderContribution).toService(PreferenceTreeLabelProvider);
|
||||
bind(PreferencesWidget)
|
||||
.toDynamicValue(({ container }) => createPreferencesWidgetContainer(container).get(PreferencesWidget))
|
||||
.inSingletonScope();
|
||||
bind(WidgetFactory).toDynamicValue(({ container }) => ({
|
||||
id: PreferencesWidget.ID,
|
||||
createWidget: () => container.get(PreferencesWidget)
|
||||
})).inSingletonScope();
|
||||
|
||||
bindContributionProvider(bind, PreferenceNodeRendererContribution);
|
||||
|
||||
bind(PreferenceSelectInputRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceSelectInputRendererContribution).inSingletonScope();
|
||||
|
||||
bind(PreferenceArrayInputRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceArrayInputRendererContribution).inSingletonScope();
|
||||
|
||||
bind(PreferenceStringInputRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceStringInputRendererContribution).inSingletonScope();
|
||||
bind(PreferenceNullInputRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceNullRendererContribution).inSingletonScope();
|
||||
|
||||
bind(PreferenceBooleanInputRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceBooleanInputRendererContribution).inSingletonScope();
|
||||
|
||||
bind(PreferenceNumberInputRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceNumberInputRendererContribution).inSingletonScope();
|
||||
|
||||
bind(PreferenceJSONLinkRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceJSONLinkRendererContribution).inSingletonScope();
|
||||
|
||||
bind(PreferenceHeaderRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceHeaderRendererContribution).inSingletonScope();
|
||||
|
||||
bind(PreferenceSingleFilePathInputRenderer).toSelf();
|
||||
bind(PreferenceNodeRendererContribution).to(PreferenceSingleFilePathInputRendererContribution).inSingletonScope();
|
||||
|
||||
bind(DefaultPreferenceNodeRendererCreatorRegistry).toSelf().inSingletonScope();
|
||||
bind(PreferenceNodeRendererCreatorRegistry).toService(DefaultPreferenceNodeRendererCreatorRegistry);
|
||||
}
|
||||
|
||||
export function createPreferencesWidgetContainer(parent: interfaces.Container): Container {
|
||||
const child = createTreeContainer(parent, {
|
||||
model: PreferenceTreeModel,
|
||||
widget: PreferencesTreeWidget,
|
||||
props: { search: false }
|
||||
});
|
||||
child.bind(PreferencesEditorWidget).toSelf();
|
||||
|
||||
child.bind(PreferencesSearchbarWidget).toSelf();
|
||||
child.bind(PreferencesScopeTabBar).toSelf();
|
||||
child.bind(PreferencesWidget).toSelf();
|
||||
|
||||
child.bind(PreferenceNodeRendererFactory).toFactory(({ container }) => (node: Preference.TreeNode) => {
|
||||
const registry = container.get<PreferenceNodeRendererCreatorRegistry>(PreferenceNodeRendererCreatorRegistry);
|
||||
const creator = registry.getPreferenceNodeRendererCreator(node);
|
||||
return creator.createRenderer(node, container);
|
||||
});
|
||||
|
||||
child.bind(PreferenceMarkdownRenderer).toSelf().inSingletonScope();
|
||||
|
||||
return child;
|
||||
}
|
||||
119
packages/preferences/src/browser/views/preference-widget.tsx
Normal file
119
packages/preferences/src/browser/views/preference-widget.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { postConstruct, injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Panel, Widget, Message, StatefulWidget, codicon } from '@theia/core/lib/browser';
|
||||
import { PreferencesEditorState, PreferencesEditorWidget } from './preference-editor-widget';
|
||||
import { PreferencesTreeWidget } from './preference-tree-widget';
|
||||
import { PreferencesSearchbarState, PreferencesSearchbarWidget } from './preference-searchbar-widget';
|
||||
import { PreferencesScopeTabBar, PreferencesScopeTabBarState } from './preference-scope-tabbar-widget';
|
||||
import { Preference } from '../util/preference-types';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { PreferenceScope } from '@theia/core';
|
||||
|
||||
interface PreferencesWidgetState {
|
||||
scopeTabBarState: PreferencesScopeTabBarState,
|
||||
editorState: PreferencesEditorState,
|
||||
searchbarWidgetState: PreferencesSearchbarState,
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreferencesWidget extends Panel implements StatefulWidget {
|
||||
/**
|
||||
* The widget `id`.
|
||||
*/
|
||||
static readonly ID = 'settings_widget';
|
||||
/**
|
||||
* The widget `label` which is used for display purposes.
|
||||
*/
|
||||
static readonly LABEL = nls.localizeByDefault('Settings');
|
||||
|
||||
@inject(PreferencesEditorWidget) protected readonly editorWidget: PreferencesEditorWidget;
|
||||
@inject(PreferencesTreeWidget) protected readonly treeWidget: PreferencesTreeWidget;
|
||||
@inject(PreferencesSearchbarWidget) protected readonly searchbarWidget: PreferencesSearchbarWidget;
|
||||
@inject(PreferencesScopeTabBar) protected readonly tabBarWidget: PreferencesScopeTabBar;
|
||||
|
||||
get currentScope(): Preference.SelectedScopeDetails {
|
||||
return this.tabBarWidget.currentScope;
|
||||
}
|
||||
|
||||
setSearchTerm(query: string): Promise<void> {
|
||||
return this.searchbarWidget.updateSearchTerm(query);
|
||||
}
|
||||
|
||||
setScope(scope: PreferenceScope.User | PreferenceScope.Workspace | URI): void {
|
||||
this.tabBarWidget.setScope(scope);
|
||||
}
|
||||
|
||||
protected override onResize(msg: Widget.ResizeMessage): void {
|
||||
super.onResize(msg);
|
||||
if (msg.width < 600 && this.treeWidget && !this.treeWidget.isHidden) {
|
||||
this.treeWidget.hide();
|
||||
this.editorWidget.addClass('full-pane');
|
||||
} else if (msg.width >= 600 && this.treeWidget && this.treeWidget.isHidden) {
|
||||
this.treeWidget.show();
|
||||
this.editorWidget.removeClass('full-pane');
|
||||
}
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.searchbarWidget.focus();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = PreferencesWidget.ID;
|
||||
this.title.label = PreferencesWidget.LABEL;
|
||||
this.title.caption = PreferencesWidget.LABEL;
|
||||
this.title.closable = true;
|
||||
this.addClass('theia-settings-container');
|
||||
this.title.iconClass = codicon('settings');
|
||||
|
||||
this.searchbarWidget.addClass('preferences-searchbar-widget');
|
||||
this.addWidget(this.searchbarWidget);
|
||||
|
||||
this.tabBarWidget.addClass('preferences-tabbar-widget');
|
||||
this.addWidget(this.tabBarWidget);
|
||||
|
||||
this.treeWidget.addClass('preferences-tree-widget');
|
||||
this.addWidget(this.treeWidget);
|
||||
|
||||
this.editorWidget.addClass('preferences-editor-widget');
|
||||
this.addWidget(this.editorWidget);
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
getPreviewNode(): Node | undefined {
|
||||
return this.node;
|
||||
}
|
||||
|
||||
storeState(): PreferencesWidgetState {
|
||||
return {
|
||||
scopeTabBarState: this.tabBarWidget.storeState(),
|
||||
editorState: this.editorWidget.storeState(),
|
||||
searchbarWidgetState: this.searchbarWidget.storeState(),
|
||||
};
|
||||
}
|
||||
|
||||
restoreState(state: PreferencesWidgetState): void {
|
||||
this.tabBarWidget.restoreState(state.scopeTabBarState);
|
||||
this.editorWidget.restoreState(state.editorState);
|
||||
this.searchbarWidget.restoreState(state.searchbarWidgetState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { PreferenceScope } from '@theia/core/lib/common/preferences';
|
||||
import { WorkspaceService, WorkspaceData } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { AbstractResourcePreferenceProvider } from '../common/abstract-resource-preference-provider';
|
||||
|
||||
@injectable()
|
||||
export class WorkspaceFilePreferenceProviderOptions {
|
||||
workspaceUri: URI;
|
||||
}
|
||||
|
||||
export const WorkspaceFilePreferenceProviderFactory = Symbol('WorkspaceFilePreferenceProviderFactory');
|
||||
export type WorkspaceFilePreferenceProviderFactory = (options: WorkspaceFilePreferenceProviderOptions) => WorkspaceFilePreferenceProvider;
|
||||
|
||||
@injectable()
|
||||
export class WorkspaceFilePreferenceProvider extends AbstractResourcePreferenceProvider {
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(WorkspaceFilePreferenceProviderOptions)
|
||||
protected readonly options: WorkspaceFilePreferenceProviderOptions;
|
||||
|
||||
protected sectionsInsideSettings = new Set<string>();
|
||||
|
||||
protected getUri(): URI {
|
||||
return this.options.workspaceUri;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected override parse(content: string): any {
|
||||
const data = super.parse(content);
|
||||
if (WorkspaceData.is(data)) {
|
||||
const settings = { ...data.settings };
|
||||
for (const key of this.configurations.getSectionNames().filter(name => name !== 'settings')) {
|
||||
// If the user has written configuration inside the "settings" object, we will respect that.
|
||||
if (settings[key]) {
|
||||
this.sectionsInsideSettings.add(key);
|
||||
}
|
||||
// Favor sections outside the "settings" object to agree with VSCode behavior
|
||||
if (data[key]) {
|
||||
settings[key] = data[key];
|
||||
this.sectionsInsideSettings.delete(key);
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
protected override getPath(preferenceName: string): string[] {
|
||||
const firstSegment = preferenceName.split('.', 1)[0];
|
||||
const remainder = preferenceName.slice(firstSegment.length + 1);
|
||||
if (this.belongsInSection(firstSegment, remainder)) {
|
||||
// Default to writing sections outside the "settings" object.
|
||||
const path = [firstSegment];
|
||||
if (remainder) {
|
||||
path.push(remainder);
|
||||
}
|
||||
// If the user has already written this section inside the "settings" object, modify it there.
|
||||
if (this.sectionsInsideSettings.has(firstSegment)) {
|
||||
path.unshift('settings');
|
||||
}
|
||||
return path;
|
||||
}
|
||||
return ['settings'].concat(super.getPath(preferenceName) ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `firstSegment` is a section name (e.g. `tasks`, `launch`)
|
||||
*/
|
||||
protected belongsInSection(firstSegment: string, remainder: string): boolean {
|
||||
return this.configurations.isSectionName(firstSegment);
|
||||
}
|
||||
|
||||
getScope(): PreferenceScope {
|
||||
return PreferenceScope.Workspace;
|
||||
}
|
||||
|
||||
override getDomain(): string[] {
|
||||
// workspace file is treated as part of the workspace
|
||||
return this.workspaceService.tryGetRoots().map(r => r.resource.toString()).concat([this.options.workspaceUri.toString()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { WorkspaceFilePreferenceProviderFactory, WorkspaceFilePreferenceProvider } from './workspace-file-preference-provider';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Emitter, Event, PreferenceProvider, PreferenceProviderDataChanges, PreferenceProviderProvider, PreferenceScope } from '@theia/core';
|
||||
import { JSONObject } from '@theia/core/shared/@lumino/coreutils';
|
||||
|
||||
@injectable()
|
||||
export class WorkspacePreferenceProvider implements PreferenceProvider {
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(WorkspaceFilePreferenceProviderFactory)
|
||||
protected readonly workspaceFileProviderFactory: WorkspaceFilePreferenceProviderFactory;
|
||||
|
||||
@inject(PreferenceProviderProvider)
|
||||
protected readonly preferenceProviderProvider: PreferenceProviderProvider;
|
||||
|
||||
protected readonly onDidPreferencesChangedEmitter = new Emitter<PreferenceProviderDataChanges>();
|
||||
readonly onDidPreferencesChanged: Event<PreferenceProviderDataChanges> = this.onDidPreferencesChangedEmitter.event;
|
||||
|
||||
protected readonly toDisposeOnEnsureDelegateUpToDate = new DisposableCollection();
|
||||
|
||||
protected _ready = new Deferred<void>();
|
||||
readonly ready = this._ready.promise;
|
||||
|
||||
protected readonly disposables = new DisposableCollection();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.workspaceService.ready.then(() => {
|
||||
// If there is no workspace after the workspace service is initialized, then no more work is needed for this provider to be ready.
|
||||
// If there is a workspace, then we wait for the new delegate to be ready before declaring this provider ready.
|
||||
if (!this.workspaceService.workspace) {
|
||||
this._ready.resolve();
|
||||
} else {
|
||||
// important for the case if onWorkspaceLocationChanged has fired before this init is finished
|
||||
this.ensureDelegateUpToDate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
|
||||
canHandleScope(scope: PreferenceScope): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getConfigUri(resourceUri: string | undefined = this.ensureResourceUri(), sectionName?: string): URI | undefined {
|
||||
return this.delegate?.getConfigUri && this.delegate?.getConfigUri(resourceUri, sectionName);
|
||||
}
|
||||
|
||||
getContainingConfigUri(resourceUri: string | undefined = this.ensureResourceUri(), sectionName?: string): URI | undefined {
|
||||
return this.delegate?.getContainingConfigUri?.(resourceUri, sectionName);
|
||||
}
|
||||
|
||||
protected _delegate: PreferenceProvider | undefined;
|
||||
protected get delegate(): PreferenceProvider | undefined {
|
||||
return this._delegate;
|
||||
}
|
||||
|
||||
protected ensureDelegateUpToDate(): void {
|
||||
const delegate = this.createDelegate();
|
||||
if (this._delegate !== delegate) {
|
||||
this.toDisposeOnEnsureDelegateUpToDate.dispose();
|
||||
this.disposables.push(this.toDisposeOnEnsureDelegateUpToDate);
|
||||
|
||||
this._delegate = delegate;
|
||||
|
||||
if (delegate) {
|
||||
// If this provider has not yet declared itself ready, it should do so when the new delegate is ready.
|
||||
delegate.ready.then(() => this._ready.resolve(), () => { });
|
||||
}
|
||||
|
||||
if (delegate instanceof WorkspaceFilePreferenceProvider) {
|
||||
this.toDisposeOnEnsureDelegateUpToDate.pushAll([
|
||||
delegate,
|
||||
delegate.onDidPreferencesChanged(changes => this.onDidPreferencesChangedEmitter.fire(changes))
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected createDelegate(): PreferenceProvider | undefined {
|
||||
const workspace = this.workspaceService.workspace;
|
||||
if (!workspace) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.workspaceService.isMultiRootWorkspaceOpened) {
|
||||
return this.preferenceProviderProvider(PreferenceScope.Folder);
|
||||
}
|
||||
if (this._delegate instanceof WorkspaceFilePreferenceProvider && this._delegate.getConfigUri().isEqual(workspace.resource)) {
|
||||
return this._delegate;
|
||||
}
|
||||
return this.workspaceFileProviderFactory({
|
||||
workspaceUri: workspace.resource
|
||||
});
|
||||
}
|
||||
|
||||
get<T>(preferenceName: string, resourceUri: string | undefined = this.ensureResourceUri()): T | undefined {
|
||||
const delegate = this.delegate;
|
||||
return delegate ? delegate.get<T>(preferenceName, resourceUri) : undefined;
|
||||
}
|
||||
|
||||
resolve<T>(preferenceName: string, resourceUri: string | undefined = this.ensureResourceUri()): { value?: T, configUri?: URI } {
|
||||
const delegate = this.delegate;
|
||||
return delegate ? delegate.resolve<T>(preferenceName, resourceUri) : {};
|
||||
}
|
||||
|
||||
async setPreference(preferenceName: string, value: any, resourceUri: string | undefined = this.ensureResourceUri()): Promise<boolean> {
|
||||
const delegate = this.delegate;
|
||||
if (delegate) {
|
||||
return delegate.setPreference(preferenceName, value, resourceUri);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getPreferences(resourceUri: string | undefined = this.ensureResourceUri()): JSONObject {
|
||||
const delegate = this.delegate;
|
||||
return delegate ? delegate.getPreferences(resourceUri) : {};
|
||||
}
|
||||
|
||||
protected ensureResourceUri(): string | undefined {
|
||||
if (this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened) {
|
||||
return this.workspaceService.workspace.resource.toString();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
const disableJSDOM = enableJSDOM();
|
||||
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
FrontendApplicationConfigProvider.set({});
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { Container } from '@theia/core/shared/inversify';
|
||||
import { AbstractResourcePreferenceProvider, FileContentStatus, PreferenceStorage, PreferenceStorageFactory } from './abstract-resource-preference-provider';
|
||||
import { bindPreferenceService } from '@theia/core/lib/browser/frontend-application-bindings';
|
||||
import { bindMockPreferenceProviders } from '@theia/core/lib/browser/preferences/test';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Listener, MessageService, PreferenceSchemaService } from '@theia/core/lib/common';
|
||||
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { PreferenceTransactionFactory } from '../browser/preference-transaction-manager';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
class MockPreferenceStorage implements PreferenceStorage {
|
||||
onDidChangeFileContent: Listener.Registration<FileContentStatus, Promise<boolean>> = Listener.None as unknown as Listener.Registration<FileContentStatus, Promise<boolean>>;
|
||||
writeValue(key: string, path: string[], value: JSONValue): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dispose(): void { }
|
||||
releaseContent = new Deferred();
|
||||
async read(): Promise<string> {
|
||||
await this.releaseContent.promise;
|
||||
return JSON.stringify({ 'editor.fontSize': 20 });
|
||||
}
|
||||
}
|
||||
const mockSchemaProvider = { getSchemaProperty: () => undefined };
|
||||
|
||||
class LessAbstractPreferenceProvider extends AbstractResourcePreferenceProvider {
|
||||
getUri(): any { }
|
||||
getScope(): any { }
|
||||
}
|
||||
|
||||
describe('AbstractResourcePreferenceProvider', () => {
|
||||
let provider: AbstractResourcePreferenceProvider;
|
||||
let preferenceStorage: MockPreferenceStorage;
|
||||
|
||||
beforeEach(() => {
|
||||
preferenceStorage = new MockPreferenceStorage();
|
||||
const testContainer = new Container();
|
||||
bindPreferenceService(testContainer.bind.bind(testContainer));
|
||||
bindMockPreferenceProviders(testContainer.bind.bind(testContainer), testContainer.unbind.bind(testContainer));
|
||||
testContainer.rebind(<any>PreferenceSchemaService).toConstantValue(mockSchemaProvider);
|
||||
testContainer.bind(<any>PreferenceStorageFactory).toFactory(() => () => preferenceStorage);
|
||||
testContainer.bind(<any>MessageService).toConstantValue(undefined);
|
||||
testContainer.bind(<any>MonacoWorkspace).toConstantValue(undefined);
|
||||
testContainer.bind(<any>EditorManager).toConstantValue(undefined);
|
||||
testContainer.bind(<any>PreferenceTransactionFactory).toConstantValue(undefined);
|
||||
provider = testContainer.resolve(<any>LessAbstractPreferenceProvider);
|
||||
});
|
||||
|
||||
it('should not store any preferences before it is ready.', async () => {
|
||||
const resolveWhenFinished = new Deferred();
|
||||
const errorIfReadyFirst = provider.ready.then(() => Promise.reject());
|
||||
|
||||
expect(provider.get('editor.fontSize')).to.be.undefined;
|
||||
|
||||
resolveWhenFinished.resolve();
|
||||
preferenceStorage.releaseContent.resolve(); // Allow the initialization to run
|
||||
|
||||
// This promise would reject if the provider had declared itself ready before we resolve `resolveWhenFinished`
|
||||
await Promise.race([resolveWhenFinished.promise, errorIfReadyFirst]);
|
||||
});
|
||||
|
||||
it('should report values in file when `ready` resolves.', async () => {
|
||||
preferenceStorage.releaseContent.resolve();
|
||||
await provider.ready;
|
||||
expect(provider.get('editor.fontSize')).to.equal(20); // The value provided by the mock FileService implementation.
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-null/no-null */
|
||||
|
||||
import * as jsoncparser from 'jsonc-parser';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
PreferenceProviderImpl, PreferenceScope, PreferenceProviderDataChange, PreferenceSchemaService,
|
||||
PreferenceConfigurations, PreferenceUtils, PreferenceLanguageOverrideService,
|
||||
Listener
|
||||
} from '@theia/core/lib/common';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Emitter, Event } from '@theia/core';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
export interface FileContentStatus {
|
||||
content: string;
|
||||
fileOK: boolean
|
||||
}
|
||||
/**
|
||||
* Abtracts the way to read and write preferences to a given resource
|
||||
*/
|
||||
export interface PreferenceStorage extends Disposable {
|
||||
/**
|
||||
* Write a value to the underlying preference store
|
||||
* @param key the preference key
|
||||
* @param path the path to the JSON object to change
|
||||
* @param value the new preference value
|
||||
* @returns a promise that will resolve when all "onStored" listeners have finished
|
||||
*/
|
||||
writeValue(key: string, path: string[], value: JSONValue): Promise<boolean>;
|
||||
/**
|
||||
* List of listeners that will get a string with the newly stored resource content and should return a promise that resolves when
|
||||
* they are done with their processing
|
||||
*/
|
||||
onDidChangeFileContent: Listener.Registration<FileContentStatus, Promise<boolean>>;
|
||||
/**
|
||||
* Reds the content of the underlying resource
|
||||
*/
|
||||
read(): Promise<string>;
|
||||
};
|
||||
|
||||
export const PreferenceStorageFactory = Symbol('PreferenceStorageFactory');
|
||||
export type PreferenceStorageFactory = (uri: URI, scope: PreferenceScope) => PreferenceStorage;
|
||||
|
||||
@injectable()
|
||||
export abstract class AbstractResourcePreferenceProvider extends PreferenceProviderImpl {
|
||||
protected preferenceStorage: PreferenceStorage;
|
||||
|
||||
protected preferences: Record<string, any> = {};
|
||||
protected _fileExists = false;
|
||||
protected readonly loading = new Deferred();
|
||||
protected readonly onDidChangeValidityEmitter = new Emitter<boolean>();
|
||||
|
||||
set fileExists(exists: boolean) {
|
||||
if (exists !== this._fileExists) {
|
||||
this._fileExists = exists;
|
||||
this.onDidChangeValidityEmitter.fire(exists);
|
||||
}
|
||||
}
|
||||
|
||||
get onDidChangeValidity(): Event<boolean> {
|
||||
return this.onDidChangeValidityEmitter.event;
|
||||
}
|
||||
|
||||
@inject(PreferenceSchemaService)
|
||||
protected readonly schemaProvider: PreferenceSchemaService;
|
||||
@inject(PreferenceConfigurations)
|
||||
protected readonly configurations: PreferenceConfigurations;
|
||||
@inject(PreferenceLanguageOverrideService)
|
||||
protected readonly preferenceOverrideService: PreferenceLanguageOverrideService;
|
||||
@inject(PreferenceStorageFactory)
|
||||
protected readonly preferenceStorageFactory: PreferenceStorageFactory;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
const uri = this.getUri();
|
||||
this.toDispose.push(Disposable.create(() => this.loading.reject(new Error(`Preference provider for '${uri}' was disposed.`))));
|
||||
|
||||
this.preferenceStorage = this.preferenceStorageFactory(uri, this.getScope());
|
||||
this.preferenceStorage.onDidChangeFileContent(async ({ content, fileOK }) => {
|
||||
this.fileExists = fileOK;
|
||||
this.readPreferencesFromContent(content);
|
||||
await this.fireDidPreferencesChanged(); // Ensure all consumers of the event have received it.¨
|
||||
return true;
|
||||
});
|
||||
await this.readPreferencesFromFile();
|
||||
this._ready.resolve();
|
||||
this.loading.resolve();
|
||||
|
||||
this.toDispose.pushAll([
|
||||
Disposable.create(() => this.reset()),
|
||||
]);
|
||||
}
|
||||
|
||||
protected abstract getUri(): URI;
|
||||
abstract getScope(): PreferenceScope;
|
||||
|
||||
get valid(): boolean {
|
||||
return this._fileExists;
|
||||
}
|
||||
|
||||
override getConfigUri(): URI;
|
||||
override getConfigUri(resourceUri: string | undefined): URI | undefined;
|
||||
override getConfigUri(resourceUri?: string): URI | undefined {
|
||||
if (!resourceUri) {
|
||||
return this.getUri();
|
||||
}
|
||||
return this.valid && this.contains(resourceUri) ? this.getUri() : undefined;
|
||||
}
|
||||
|
||||
contains(resourceUri: string | undefined): boolean {
|
||||
if (!resourceUri) {
|
||||
return true;
|
||||
}
|
||||
const domain = this.getDomain();
|
||||
if (!domain) {
|
||||
return true;
|
||||
}
|
||||
const resourcePath = new URI(resourceUri).path;
|
||||
return domain.some(uri => new URI(uri).path.relativity(resourcePath) >= 0);
|
||||
}
|
||||
|
||||
getPreferences(resourceUri?: string): { [key: string]: any } {
|
||||
return this.valid && this.contains(resourceUri) ? this.preferences : {};
|
||||
}
|
||||
|
||||
async setPreference(key: string, value: any, resourceUri?: string): Promise<boolean> {
|
||||
let path: string[] | undefined;
|
||||
if (this.toDispose.disposed || !(path = this.getPath(key)) || !this.contains(resourceUri)) {
|
||||
return false;
|
||||
}
|
||||
return this.doSetPreference(key, path, value);
|
||||
}
|
||||
|
||||
protected doSetPreference(key: string, path: string[], value: JSONValue): Promise<boolean> {
|
||||
return this.preferenceStorage.writeValue(key, path, value);
|
||||
}
|
||||
|
||||
protected getPath(preferenceName: string): string[] | undefined {
|
||||
const asOverride = this.preferenceOverrideService.overriddenPreferenceName(preferenceName);
|
||||
if (asOverride?.overrideIdentifier) {
|
||||
return [this.preferenceOverrideService.markLanguageOverride(asOverride.overrideIdentifier), asOverride.preferenceName];
|
||||
}
|
||||
return [preferenceName];
|
||||
}
|
||||
|
||||
protected readPreferencesFromFile(): Promise<void> {
|
||||
return this.preferenceStorage.read().then(value => {
|
||||
this.fileExists = true;
|
||||
this.readPreferencesFromContent(value);
|
||||
}).catch(() => {
|
||||
this.fileExists = false;
|
||||
this.readPreferencesFromContent('');
|
||||
});
|
||||
|
||||
}
|
||||
protected readPreferencesFromContent(content: string): void {
|
||||
let preferencesInJson;
|
||||
try {
|
||||
preferencesInJson = this.parse(content);
|
||||
} catch {
|
||||
preferencesInJson = {};
|
||||
}
|
||||
const parsedPreferences = this.getParsedContent(preferencesInJson);
|
||||
this.handlePreferenceChanges(parsedPreferences);
|
||||
}
|
||||
|
||||
protected parse(content: string): any {
|
||||
content = content.trim();
|
||||
if (!content) {
|
||||
return undefined;
|
||||
}
|
||||
const strippedContent = jsoncparser.stripComments(content);
|
||||
return jsoncparser.parse(strippedContent);
|
||||
}
|
||||
|
||||
protected handlePreferenceChanges(newPrefs: { [key: string]: any }): void {
|
||||
const oldPrefs = Object.assign({}, this.preferences);
|
||||
this.preferences = newPrefs;
|
||||
const prefNames = new Set([...Object.keys(oldPrefs), ...Object.keys(newPrefs)]);
|
||||
const prefChanges: PreferenceProviderDataChange[] = [];
|
||||
const uri = this.getUri();
|
||||
for (const prefName of prefNames.values()) {
|
||||
const oldValue = oldPrefs[prefName];
|
||||
const newValue = newPrefs[prefName];
|
||||
const schemaProperty = this.schemaProvider.getSchemaProperty(prefName);
|
||||
if (schemaProperty && schemaProperty.included) {
|
||||
const scope = schemaProperty.scope;
|
||||
// do not emit the change event if the change is made out of the defined preference scope
|
||||
if (!this.schemaProvider.isValidInScope(prefName, this.getScope())) {
|
||||
console.warn(`Preference ${prefName} in ${uri} can only be defined in scopes: ${PreferenceScope.getScopeNames(scope).join(', ')}.`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!PreferenceUtils.deepEqual(newValue, oldValue)) {
|
||||
prefChanges.push({
|
||||
preferenceName: prefName, newValue, oldValue, scope: this.getScope(), domain: this.getDomain()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (prefChanges.length > 0) {
|
||||
this.emitPreferencesChangedEvent(prefChanges);
|
||||
}
|
||||
}
|
||||
|
||||
protected reset(): void {
|
||||
const preferences = this.preferences;
|
||||
this.preferences = {};
|
||||
const changes: PreferenceProviderDataChange[] = [];
|
||||
for (const prefName of Object.keys(preferences)) {
|
||||
const value = preferences[prefName];
|
||||
if (value !== undefined) {
|
||||
changes.push({
|
||||
preferenceName: prefName, newValue: undefined, oldValue: value, scope: this.getScope(), domain: this.getDomain()
|
||||
});
|
||||
}
|
||||
}
|
||||
if (changes.length > 0) {
|
||||
this.emitPreferencesChangedEvent(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
packages/preferences/src/common/cli-preferences.ts
Normal file
22
packages/preferences/src/common/cli-preferences.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export const CliPreferences = Symbol('CliPreferences');
|
||||
export const CliPreferencesPath = '/services/cli-preferences';
|
||||
|
||||
export interface CliPreferences {
|
||||
getPreferences(): Promise<[string, unknown][]>;
|
||||
}
|
||||
65
packages/preferences/src/common/jsonc-editor.ts
Normal file
65
packages/preferences/src/common/jsonc-editor.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as jsoncparser from 'jsonc-parser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { isWindows, PreferenceService } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class JSONCEditor {
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
setValue(model: string, path: jsoncparser.JSONPath, value: JSONValue): string {
|
||||
const edits = this.getEditOperations(model, path, value);
|
||||
return jsoncparser.applyEdits(model, edits);
|
||||
}
|
||||
|
||||
protected getEditOperations(content: string, path: jsoncparser.JSONPath, value: JSONValue): jsoncparser.Edit[] {
|
||||
// Everything is already undefined - no need for changes.
|
||||
if (!content && value === undefined) {
|
||||
return [];
|
||||
}
|
||||
// Delete the entire document.
|
||||
if (!path.length && value === undefined) {
|
||||
return [{
|
||||
offset: 0,
|
||||
length: content.length,
|
||||
content: ''
|
||||
}];
|
||||
}
|
||||
const tabSize = this.preferenceService.get('[json].editor.tabSize', 4);
|
||||
const insertSpaces = this.preferenceService.get('[json].editor.insertSpaces', true);
|
||||
|
||||
const jsonCOptions = {
|
||||
formattingOptions: {
|
||||
insertSpaces,
|
||||
tabSize,
|
||||
eol: this.getEOL()
|
||||
}
|
||||
};
|
||||
return jsoncparser.modify(content, path, value, jsonCOptions);
|
||||
}
|
||||
|
||||
getEOL(): string {
|
||||
const eol = this.preferenceService.get('[json].files.eol');
|
||||
if (eol && typeof eol === 'string' && eol !== 'auto') {
|
||||
return eol;
|
||||
}
|
||||
return isWindows ? '\r\n' : '\n';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { AbstractResourcePreferenceProvider } from './abstract-resource-preference-provider';
|
||||
import { PreferenceConfigurations } from '@theia/core';
|
||||
|
||||
export const SectionPreferenceProviderUri = Symbol('SectionPreferenceProviderUri');
|
||||
export const SectionPreferenceProviderSection = Symbol('SectionPreferenceProviderSection');
|
||||
|
||||
/**
|
||||
* This class encapsulates the logic of using separate files for some workspace configuration like 'launch.json' or 'tasks.json'.
|
||||
* Anything that is not a contributed section will be in the main config file.
|
||||
*/
|
||||
@injectable()
|
||||
export abstract class SectionPreferenceProvider extends AbstractResourcePreferenceProvider {
|
||||
@inject(SectionPreferenceProviderUri)
|
||||
protected readonly uri: URI;
|
||||
@inject(SectionPreferenceProviderSection)
|
||||
protected readonly section: string;
|
||||
@inject(PreferenceConfigurations)
|
||||
protected readonly preferenceConfigurations: PreferenceConfigurations;
|
||||
|
||||
private _isSection?: boolean;
|
||||
|
||||
private get isSection(): boolean {
|
||||
if (typeof this._isSection === 'undefined') {
|
||||
this._isSection = this.preferenceConfigurations.isSectionName(this.section);
|
||||
}
|
||||
return this._isSection;
|
||||
}
|
||||
|
||||
protected getUri(): URI {
|
||||
return this.uri;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected override parse(content: string): any {
|
||||
const prefs = super.parse(content);
|
||||
if (this.isSection) {
|
||||
if (prefs === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const result: { [k: string]: unknown } = {
|
||||
|
||||
};
|
||||
result[this.section] = { ...prefs };
|
||||
return result;
|
||||
} else {
|
||||
return prefs;
|
||||
}
|
||||
}
|
||||
|
||||
protected override getPath(preferenceName: string): string[] | undefined {
|
||||
if (!this.isSection) {
|
||||
return super.getPath(preferenceName);
|
||||
}
|
||||
if (preferenceName === this.section) {
|
||||
return [];
|
||||
}
|
||||
if (preferenceName.startsWith(`${this.section}.`)) {
|
||||
return [preferenceName.slice(this.section.length + 1)];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { UserPreferenceProvider, UserPreferenceProviderFactory } from '../common/user-preference-provider';
|
||||
import { PreferenceProviderImpl, PreferenceConfigurations, PreferenceResolveResult, PreferenceUtils } from '@theia/core';
|
||||
|
||||
export const UserStorageLocationProvider = Symbol('UserStorageLocationProvider');
|
||||
|
||||
/**
|
||||
* Binds together preference section prefs providers for user-level preferences.
|
||||
*/
|
||||
@injectable()
|
||||
export class UserConfigsPreferenceProvider extends PreferenceProviderImpl {
|
||||
|
||||
@inject(UserPreferenceProviderFactory)
|
||||
protected readonly providerFactory: UserPreferenceProviderFactory;
|
||||
|
||||
@inject(UserStorageLocationProvider)
|
||||
private userStorageLocationProvider: () => Promise<URI>;
|
||||
|
||||
@inject(PreferenceConfigurations)
|
||||
protected readonly configurations: PreferenceConfigurations;
|
||||
|
||||
protected readonly providers = new Map<string, UserPreferenceProvider>();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
const userStorageUri = await this.userStorageLocationProvider();
|
||||
this.createProviders(userStorageUri);
|
||||
|
||||
const readyPromises: Promise<void>[] = [];
|
||||
for (const provider of this.providers.values()) {
|
||||
readyPromises.push(provider.ready.catch(e => console.error(e)));
|
||||
}
|
||||
Promise.all(readyPromises).then(() => this._ready.resolve());
|
||||
}
|
||||
|
||||
protected createProviders(userStorageLocation: URI): void {
|
||||
for (const configName of [...this.configurations.getSectionNames(), this.configurations.getConfigName()]) {
|
||||
const sectionUri = userStorageLocation.resolve(configName + '.json');
|
||||
const sectionKey = sectionUri.toString();
|
||||
if (!this.providers.has(sectionKey)) {
|
||||
const provider = this.createProvider(sectionUri, configName);
|
||||
this.providers.set(sectionKey, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override getConfigUri(resourceUri?: string, sectionName: string = this.configurations.getConfigName()): URI | undefined {
|
||||
for (const provider of this.providers.values()) {
|
||||
const configUri = provider.getConfigUri(resourceUri);
|
||||
if (configUri && this.configurations.getName(configUri) === sectionName) {
|
||||
return configUri;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override resolve<T>(preferenceName: string, resourceUri?: string): PreferenceResolveResult<T> {
|
||||
const result: PreferenceResolveResult<T> = {};
|
||||
for (const provider of this.providers.values()) {
|
||||
const { value, configUri } = provider.resolve<T>(preferenceName, resourceUri);
|
||||
if (configUri && value !== undefined) {
|
||||
result.configUri = configUri;
|
||||
result.value = PreferenceUtils.merge(result.value as any, value as any) as any;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getPreferences(resourceUri?: string): { [p: string]: any } {
|
||||
let result = {};
|
||||
for (const provider of this.providers.values()) {
|
||||
const preferences = provider.getPreferences();
|
||||
result = PreferenceUtils.merge(result, preferences) as any;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async setPreference(preferenceName: string, value: any, resourceUri?: string): Promise<boolean> {
|
||||
const sectionName = preferenceName.split('.', 1)[0];
|
||||
const defaultConfigName = this.configurations.getConfigName();
|
||||
const configName = this.configurations.isSectionName(sectionName) ? sectionName : defaultConfigName;
|
||||
|
||||
const setWithConfigName = async (name: string): Promise<boolean> => {
|
||||
for (const provider of this.providers.values()) {
|
||||
if (this.configurations.getName(provider.getConfigUri()) === name) {
|
||||
if (await provider.setPreference(preferenceName, value, resourceUri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (await setWithConfigName(configName)) { // Try in the section we believe it belongs in.
|
||||
return true;
|
||||
} else if (configName !== defaultConfigName) { // Fall back to `settings.json` if that fails.
|
||||
return setWithConfigName(defaultConfigName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected createProvider(uri: URI, sectionName: string): UserPreferenceProvider {
|
||||
const provider = this.providerFactory(uri, sectionName);
|
||||
this.toDispose.push(provider);
|
||||
this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change)));
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
35
packages/preferences/src/common/user-preference-provider.ts
Normal file
35
packages/preferences/src/common/user-preference-provider.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { SectionPreferenceProvider } from './section-preference-provider';
|
||||
import { PreferenceScope } from '@theia/core';
|
||||
|
||||
export const UserPreferenceProviderFactory = Symbol('UserPreferenceProviderFactory');
|
||||
export interface UserPreferenceProviderFactory {
|
||||
(uri: URI, section: string): UserPreferenceProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
* A @SectionPreferenceProvider that targets the user-level settings
|
||||
*/
|
||||
@injectable()
|
||||
export class UserPreferenceProvider extends SectionPreferenceProvider {
|
||||
getScope(): PreferenceScope {
|
||||
return PreferenceScope.User;
|
||||
}
|
||||
}
|
||||
117
packages/preferences/src/node/backend-preference-storage.ts
Normal file
117
packages/preferences/src/node/backend-preference-storage.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Listener, ListenerList, URI } from '@theia/core';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { FileContentStatus, PreferenceStorage } from '../common/abstract-resource-preference-provider';
|
||||
import { EncodingService } from '@theia/core/lib/common/encoding-service';
|
||||
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
import { JSONCEditor } from '../common/jsonc-editor';
|
||||
import { DiskFileSystemProvider } from '@theia/filesystem/lib/node/disk-file-system-provider';
|
||||
import { UTF8 } from '@theia/core/lib/common/encodings';
|
||||
|
||||
interface WriteOperation {
|
||||
key: string,
|
||||
path: string[],
|
||||
value: JSONValue
|
||||
}
|
||||
|
||||
export class BackendPreferenceStorage implements PreferenceStorage {
|
||||
|
||||
protected pendingWrites: WriteOperation[] = [];
|
||||
protected writeDeferred = new Deferred<boolean>();
|
||||
protected writeFile = debounce(() => {
|
||||
this.doWrite();
|
||||
}, 10);
|
||||
|
||||
protected currentContent: string | undefined = undefined;
|
||||
protected encoding: string = UTF8;
|
||||
|
||||
constructor(
|
||||
protected readonly fileSystem: DiskFileSystemProvider,
|
||||
protected readonly uri: URI,
|
||||
protected readonly encodingService: EncodingService,
|
||||
protected readonly jsonEditor: JSONCEditor) {
|
||||
|
||||
this.fileSystem.watch(uri, { excludes: [], recursive: false });
|
||||
this.fileSystem.onDidChangeFile(events => {
|
||||
for (const e of events) {
|
||||
if (e.resource.isEqual(uri)) {
|
||||
this.read().then(content => this.onDidChangeFileContentListeners.invoke({ content, fileOK: true }, () => { }))
|
||||
.catch(() => this.onDidChangeFileContentListeners.invoke({ content: '', fileOK: false }, () => { }));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writeValue(key: string, path: string[], value: JSONValue): Promise<boolean> {
|
||||
this.pendingWrites.push({
|
||||
key, path, value
|
||||
});
|
||||
return this.waitForWrite();
|
||||
}
|
||||
|
||||
waitForWrite(): Promise<boolean> {
|
||||
const result = this.writeDeferred.promise;
|
||||
this.writeFile();
|
||||
return result;
|
||||
}
|
||||
|
||||
async doWrite(): Promise<void> {
|
||||
try {
|
||||
if (this.currentContent === undefined) {
|
||||
await this.read();
|
||||
}
|
||||
let newContent = this.currentContent || '';
|
||||
for (const op of this.pendingWrites) {
|
||||
newContent = this.jsonEditor.setValue(newContent, op.path, op.value);
|
||||
}
|
||||
await this.fileSystem.writeFile(this.uri, this.encodingService.encode(newContent, {
|
||||
encoding: this.encoding,
|
||||
hasBOM: false
|
||||
}).buffer, {
|
||||
create: true,
|
||||
overwrite: true
|
||||
});
|
||||
this.currentContent = newContent;
|
||||
this.pendingWrites = [];
|
||||
await Listener.awaitAll({ content: newContent, fileOK: true }, this.onDidChangeFileContentListeners);
|
||||
this.writeDeferred.resolve(true);
|
||||
} catch (e) {
|
||||
this.currentContent = undefined;
|
||||
console.error(e);
|
||||
this.writeDeferred.resolve(false);
|
||||
} finally {
|
||||
this.writeDeferred = new Deferred();
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly onDidChangeFileContentListeners = new ListenerList<FileContentStatus, Promise<boolean>>();
|
||||
onDidChangeFileContent: Listener.Registration<FileContentStatus, Promise<boolean>> = this.onDidChangeFileContentListeners.registration;
|
||||
|
||||
async read(): Promise<string> {
|
||||
const contents = BinaryBuffer.wrap(await this.fileSystem.readFile(this.uri));
|
||||
this.encoding = (await this.encodingService.detectEncoding(contents)).encoding || this.encoding;
|
||||
this.currentContent = this.encodingService.decode(contents, this.encoding);
|
||||
return this.currentContent;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
}
|
||||
|
||||
}
|
||||
49
packages/preferences/src/node/preference-backend-module.ts
Normal file
49
packages/preferences/src/node/preference-backend-module.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { CliContribution } from '@theia/core/lib/node/cli';
|
||||
import { PreferenceCliContribution } from './preference-cli-contribution';
|
||||
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
||||
import { CliPreferences, CliPreferencesPath } from '../common/cli-preferences';
|
||||
import { bindPreferenceProviders } from './preference-bindings';
|
||||
import { PreferenceStorageFactory } from '../common/abstract-resource-preference-provider';
|
||||
import { PreferenceScope, URI } from '@theia/core';
|
||||
import { BackendPreferenceStorage } from './backend-preference-storage';
|
||||
import { JSONCEditor } from '../common/jsonc-editor';
|
||||
import { EncodingService } from '@theia/core/lib/common/encoding-service';
|
||||
import { DiskFileSystemProvider } from '@theia/filesystem/lib/node/disk-file-system-provider';
|
||||
|
||||
const preferencesConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bindBackendService(CliPreferencesPath, CliPreferences);
|
||||
});
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(PreferenceCliContribution).toSelf().inSingletonScope();
|
||||
bind(CliPreferences).toService(PreferenceCliContribution);
|
||||
bind(CliContribution).toService(PreferenceCliContribution);
|
||||
bind(JSONCEditor).toSelf().inSingletonScope();
|
||||
|
||||
bind(PreferenceStorageFactory).toFactory(({ container }) => (uri: URI, scope: PreferenceScope) => new BackendPreferenceStorage(
|
||||
container.get(DiskFileSystemProvider),
|
||||
uri,
|
||||
container.get(EncodingService),
|
||||
container.get(JSONCEditor)
|
||||
));
|
||||
|
||||
bind(ConnectionContainerModule).toConstantValue(preferencesConnectionModule);
|
||||
bindPreferenceProviders(bind);
|
||||
});
|
||||
31
packages/preferences/src/node/preference-bindings.ts
Normal file
31
packages/preferences/src/node/preference-bindings.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { UserPreferenceProvider, UserPreferenceProviderFactory } from '../common/user-preference-provider';
|
||||
import { SectionPreferenceProviderUri, SectionPreferenceProviderSection } from '../common/section-preference-provider';
|
||||
import { bindFactory, PreferenceProvider, PreferenceScope, URI } from '@theia/core';
|
||||
import { UserConfigsPreferenceProvider, UserStorageLocationProvider } from '../common/user-configs-preference-provider';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
|
||||
export function bindPreferenceProviders(bind: interfaces.Bind): void {
|
||||
bind(UserStorageLocationProvider).toDynamicValue(context => async () => {
|
||||
const env: EnvVariablesServer = context.container.get(EnvVariablesServer);
|
||||
return new URI(await env.getConfigDirUri());
|
||||
});
|
||||
bind(PreferenceProvider).to(UserConfigsPreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.User);
|
||||
bindFactory(bind, UserPreferenceProviderFactory, UserPreferenceProvider, SectionPreferenceProviderUri, SectionPreferenceProviderSection);
|
||||
}
|
||||
48
packages/preferences/src/node/preference-cli-contribution.ts
Normal file
48
packages/preferences/src/node/preference-cli-contribution.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { Argv } from '@theia/core/shared/yargs';
|
||||
import { CliContribution } from '@theia/core/lib/node/cli';
|
||||
import { CliPreferences } from '../common/cli-preferences';
|
||||
|
||||
@injectable()
|
||||
export class PreferenceCliContribution implements CliContribution, CliPreferences {
|
||||
|
||||
protected preferences: [string, unknown][] = [];
|
||||
|
||||
configure(conf: Argv<{}>): void {
|
||||
conf.option('set-preference', {
|
||||
nargs: 1,
|
||||
desc: 'sets the specified preference'
|
||||
});
|
||||
}
|
||||
|
||||
setArguments(args: Record<string, unknown>): void {
|
||||
if (args.setPreference) {
|
||||
const preferences: string[] = args.setPreference instanceof Array ? args.setPreference : [args.setPreference];
|
||||
for (const preference of preferences) {
|
||||
const firstEqualIndex = preference.indexOf('=');
|
||||
this.preferences.push([preference.substring(0, firstEqualIndex), JSON.parse(preference.substring(firstEqualIndex + 1))]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPreferences(): Promise<[string, unknown][]> {
|
||||
return this.preferences;
|
||||
}
|
||||
|
||||
}
|
||||
31
packages/preferences/tsconfig.json
Normal file
31
packages/preferences/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../editor"
|
||||
},
|
||||
{
|
||||
"path": "../filesystem"
|
||||
},
|
||||
{
|
||||
"path": "../monaco"
|
||||
},
|
||||
{
|
||||
"path": "../userstorage"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user