Skip to content

Commit d06e3d7

Browse files
authored
Merge pull request #28 from CodinGame/improve-configuration-service-override
Improve configuration service override
2 parents 3dfbf27 + 3d3f31a commit d06e3d7

8 files changed

Lines changed: 176 additions & 5 deletions

File tree

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Services.install({
2828
})
2929
```
3030

31+
## Monaco standalone services
32+
3133
Also, monaco-editor use `standalone` versions or the vscode services, which are much simpler.
3234

3335
You may want to provide your custom implementations of them, especially for: `textModelService`, `codeEditorService` and `notificationService`. To do so, you can provide them as the third parameter while creating your first editor.
@@ -64,12 +66,40 @@ StandaloneServices.initialize({
6466
// It will be called when for instance the user ctrl+click on an import
6567
}),
6668
...getMessageServiceOverride(document.body),
67-
...getConfigurationServiceOverride(readConfiguration, configurationChangeEvent)
69+
...getConfigurationServiceOverride(() => userConfigurationJson, configurationChangeEvent)
6870
})
6971
```
7072

7173
Note: using `vscode/service-override/modelEditor`, you'll be able to use the `vscode.workspace.registerTextDocumentContentProvider` api
7274

75+
### Troubleshoot
76+
77+
`StandaloneServices.initialize` can only be called once (note that `monaco.editor.create` calls `StandaloneServices.initialize`).
78+
79+
Also, a service that is used cannot be overriden anymore. So `StandaloneServices.initialize` should be called as soon as possible to prevent most of the issues.
80+
81+
## Editor configuration
82+
83+
The editors created using `monaco.editor.create` don't use the configuration from the configurationService.
84+
85+
This library exposes functions to create editors binded on the configuration:
86+
87+
before:
88+
```typescript
89+
import * as monaco from 'monaco-editor'
90+
monaco.editor.create(...)
91+
```
92+
93+
after:
94+
```typescript
95+
import { createConfiguredEditor } from 'vscode/monaco'
96+
97+
createConfiguredEditor(...)
98+
```
99+
100+
`createConfiguredEditor` returns a subclass of what is returned by `monaco.editor.create`, the `updateOptions` method can still be used.
101+
The only difference is that is will use the `configurationService` as a default configuration
102+
73103
### Installation
74104

75105
```bash

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
"./service-override/configuration": {
3636
"types": "./dist/configuration.d.ts",
3737
"default": "./dist/configuration.js"
38+
},
39+
"./monaco": {
40+
"types": "./dist/monaco.d.ts",
41+
"default": "./dist/monaco.js"
3842
}
3943
},
4044
"typesVersions": {
@@ -50,6 +54,9 @@
5054
],
5155
"service-override/configuration": [
5256
"./dist/configuration.d.ts"
57+
],
58+
"monaco": [
59+
"./dist/monaco.d.ts"
5360
]
5461
}
5562
},

rollup/rollup.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ export default (args: Record<string, string>): rollup.RollupOptions => {
8585
services: './src/services.ts',
8686
messages: './src/service-override/messages.ts',
8787
modelEditor: './src/service-override/modelEditor.ts',
88-
configuration: './src/service-override/configuration.ts'
88+
configuration: './src/service-override/configuration.ts',
89+
monaco: './src/monaco'
8990
},
9091
plugins: [
9192
{

rollup/rollup.types.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export default rollup.defineConfig([
1616
'./dist/types/src/services.d.ts',
1717
'./dist/types/src/service-override/messages.d.ts',
1818
'./dist/types/src/service-override/modelEditor.d.ts',
19-
'./dist/types/src/service-override/configuration.d.ts'
19+
'./dist/types/src/service-override/configuration.d.ts',
20+
'./dist/types/src/monaco.d.ts'
2021
].map((input): rollup.RollupOptions => ({
2122
input,
2223
output: {

src/monaco.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { StandaloneServices } from 'vs/editor/standalone/browser/standaloneServices'
2+
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'
3+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'
4+
import { IStandaloneEditorConstructionOptions, StandaloneDiffEditor, StandaloneEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor'
5+
import { IEditorOptions } from 'vs/editor/common/config/editorOptions'
6+
import { IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'
7+
import { isObject } from 'vs/base/common/types'
8+
import { deepClone, distinct } from 'vs/base/common/objects'
9+
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'
10+
import type { create as createEditor, createDiffEditor } from 'vs/editor/standalone/browser/standaloneEditor'
11+
import { createInjectedClass } from './tools/injection'
12+
13+
function computeConfiguration (configuration: IEditorConfiguration, isDiffEditor: boolean, overrides?: Readonly<IEditorOptions>): IEditorOptions {
14+
const editorConfiguration: IEditorOptions = isObject(configuration.editor) ? deepClone(configuration.editor) : Object.create(null)
15+
if (isDiffEditor && isObject(configuration.diffEditor)) {
16+
Object.assign(editorConfiguration, configuration.diffEditor)
17+
}
18+
if (isObject(configuration.diffEditor)) {
19+
Object.assign(editorConfiguration, overrides)
20+
}
21+
return editorConfiguration
22+
}
23+
24+
/**
25+
* A StandaloneEditor which is plugged on the `textResourceConfigurationService` for its options
26+
* So instead of just taking the options given by the user via the `updateOptions` method,
27+
* it fallbacks on the current configurationService configuration
28+
*/
29+
class ConfiguredStandaloneEditor extends createInjectedClass(StandaloneEditor) {
30+
// use createInjectedClass because StandaloneEditor has a lot of injected services and it would be a pain to inject them all here to be able to forward them
31+
// Also, the injected services may vary so relying on the annotations is more robust (and useful for @codingame/monaco-editor which removes a service from the list)
32+
33+
private optionsOverrides?: Readonly<IEditorOptions>
34+
private lastAppliedEditorOptions?: IEditorOptions
35+
36+
constructor (
37+
domElement: HTMLElement,
38+
private isDiffEditor: boolean,
39+
_options: Readonly<IStandaloneEditorConstructionOptions> | undefined,
40+
@IInstantiationService instantiationService: IInstantiationService,
41+
@ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService
42+
) {
43+
const computedOptions = computeConfiguration(textResourceConfigurationService.getValue<IEditorConfiguration>(_options?.model?.uri), isDiffEditor, _options)
44+
super(instantiationService, domElement, computedOptions)
45+
this.lastAppliedEditorOptions = computedOptions
46+
47+
this.optionsOverrides = _options
48+
this._register(textResourceConfigurationService.onDidChangeConfiguration(() => this.updateEditorConfiguration()))
49+
this._register(this.onDidChangeModelLanguage(() => this.updateEditorConfiguration()))
50+
this._register(this.onDidChangeModel(() => this.updateEditorConfiguration()))
51+
}
52+
53+
/**
54+
* This method is widely inspired from vs/workbench/browser/parts/editor/textEditor
55+
*/
56+
private updateEditorConfiguration (): void {
57+
const resource = this.getModel()?.uri
58+
if (resource == null) {
59+
return
60+
}
61+
const configuration = this.textResourceConfigurationService.getValue<IEditorConfiguration | undefined>(resource)
62+
if (configuration == null) {
63+
return
64+
}
65+
66+
const editorConfiguration = computeConfiguration(configuration, this.isDiffEditor, this.optionsOverrides)
67+
68+
// Try to figure out the actual editor options that changed from the last time we updated the editor.
69+
// We do this so that we are not overwriting some dynamic editor settings (e.g. word wrap) that might
70+
// have been applied to the editor directly.
71+
let editorSettingsToApply = editorConfiguration
72+
if (this.lastAppliedEditorOptions != null) {
73+
editorSettingsToApply = distinct(this.lastAppliedEditorOptions, editorSettingsToApply)
74+
}
75+
76+
if (Object.keys(editorSettingsToApply).length > 0) {
77+
this.lastAppliedEditorOptions = editorConfiguration
78+
79+
super.updateOptions(editorSettingsToApply)
80+
}
81+
}
82+
83+
override updateOptions (newOptions: Readonly<IEditorOptions>): void {
84+
this.optionsOverrides = newOptions
85+
this.updateEditorConfiguration()
86+
}
87+
}
88+
89+
class ConfiguredStandaloneDiffEditor extends StandaloneDiffEditor {
90+
protected override _createInnerEditor (instantiationService: IInstantiationService, container: HTMLElement, options: Readonly<IEditorOptions>): CodeEditorWidget {
91+
return instantiationService.createInstance(ConfiguredStandaloneEditor, container, true, options)
92+
}
93+
}
94+
95+
export const createConfiguredEditor: typeof createEditor = (domElement, options, override) => {
96+
const instantiationService = StandaloneServices.initialize(override ?? {})
97+
return instantiationService.createInstance(ConfiguredStandaloneEditor, domElement, false, options)
98+
}
99+
100+
export const createConfiguredDiffEditor: typeof createDiffEditor = (domElement, options, override) => {
101+
const instantiationService = StandaloneServices.initialize(override ?? {})
102+
return instantiationService.createInstance(ConfiguredStandaloneDiffEditor, domElement, options)
103+
}

src/service-override/configuration.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { FileChangeType, FileSystemProviderCapabilities, FileType, IFileChange,
99
import { FileService } from 'vs/platform/files/common/fileService'
1010
import { ILogService } from 'vs/platform/log/common/log'
1111
import { Schemas } from 'vs/base/common/network'
12+
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'
13+
import { TextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'
14+
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'
1215
import { unsupported } from '../tools'
1316

1417
function createConfigurationFileSystemProvider (settingsResource: URI, readConfiguration: () => string, onChange: Event<void>) {
@@ -69,6 +72,7 @@ export default function getServiceOverride (readConfiguration: () => string, onC
6972
})
7073

7174
return {
72-
[IConfigurationService.toString()]: configurationService
75+
[IConfigurationService.toString()]: configurationService,
76+
[ITextResourceConfigurationService.toString()]: new SyncDescriptor(TextResourceConfigurationService)
7377
}
7478
}

src/tools/injection.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { BrandedService, IInstantiationService, _util } from 'vs/platform/instantiation/common/instantiation'
2+
3+
function getInjectedParameters<Ctor extends new (...args: any[]) => InstanceType<Ctor>> (instantiationService: IInstantiationService, ctor: Ctor) {
4+
return instantiationService.invokeFunction((accessor) => {
5+
return _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index).map(d => accessor.get(d.id))
6+
})
7+
}
8+
9+
declare type GetLeadingNonServiceArgs<Args> = Args extends [...BrandedService[]] ? [] : Args extends [infer A, ...BrandedService[]] ? [A] : Args extends [infer A, ...infer R] ? [A, ...GetLeadingNonServiceArgs<R>] : never
10+
11+
/**
12+
* Takes a class with injected services as parameters and returns a child class that only takes the injector as parameter
13+
* @param ctor The class to inject
14+
* @returns A class that only needs the injector
15+
*/
16+
export function createInjectedClass<Ctor extends new (...args: any[]) => InstanceType<Ctor>>(ctor: Ctor): new (instantiationService: IInstantiationService, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>) => InstanceType<Ctor> {
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
const _ctor = (ctor as (new (...args: any[]) => any))
19+
return class extends _ctor {
20+
constructor (...args: any[]) {
21+
super(...args.slice(1), ...getInjectedParameters(args[0], ctor))
22+
}
23+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
24+
} as any
25+
}

tsconfig.types.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@
5252
"emitDeclarationOnly": true,
5353
"skipLibCheck": true
5454
},
55-
"include": ["src/services.ts", "src/types.d.ts", "src/service-override/messages.ts", "src/service-override/modelEditor.ts", "src/service-override/configuration.ts"]
55+
"include": ["src/services.ts", "src/types.d.ts", "src/service-override/messages.ts", "src/service-override/modelEditor.ts", "src/service-override/configuration.ts", "src/monaco.ts"]
5656
}

0 commit comments

Comments
 (0)