@@ -4,21 +4,33 @@ import * as path from "path";
44import * as vscode from "vscode" ;
55import { expectNotUndefined , log , normalizeDriveLetter , unwrapUndefinable } from "./util" ;
66import type { Env } from "./util" ;
7- import type { Disposable } from "vscode " ;
7+ import { cloneDeep , get , pickBy , set } from "lodash " ;
88
99export type RunnableEnvCfgItem = {
1010 mask ?: string ;
1111 env : { [ key : string ] : { toString ( ) : string } | null } ;
1212 platform ?: string | string [ ] ;
1313} ;
1414
15+ export type ConfigurationTree = { [ key : string ] : ConfigurationValue } ;
16+ export type ConfigurationValue =
17+ | undefined
18+ | null
19+ | boolean
20+ | number
21+ | string
22+ | ConfigurationValue [ ]
23+ | ConfigurationTree ;
24+
1525type ShowStatusBar = "always" | "never" | { documentSelector : vscode . DocumentSelector } ;
1626
1727export class Config {
1828 readonly extensionId = "rust-lang.rust-analyzer" ;
29+
1930 configureLang : vscode . Disposable | undefined ;
31+ workspaceState : vscode . Memento ;
2032
21- readonly rootSection = "rust-analyzer" ;
33+ private readonly rootSection = "rust-analyzer" ;
2234 private readonly requiresServerReloadOpts = [ "server" , "files" , "showSyntaxTree" ] . map (
2335 ( opt ) => `${ this . rootSection } .${ opt } ` ,
2436 ) ;
@@ -27,8 +39,13 @@ export class Config {
2739 ( opt ) => `${ this . rootSection } .${ opt } ` ,
2840 ) ;
2941
30- constructor ( disposables : Disposable [ ] ) {
31- vscode . workspace . onDidChangeConfiguration ( this . onDidChangeConfiguration , this , disposables ) ;
42+ constructor ( ctx : vscode . ExtensionContext ) {
43+ this . workspaceState = ctx . workspaceState ;
44+ vscode . workspace . onDidChangeConfiguration (
45+ this . onDidChangeConfiguration ,
46+ this ,
47+ ctx . subscriptions ,
48+ ) ;
3249 this . refreshLogging ( ) ;
3350 this . configureLanguage ( ) ;
3451 }
@@ -37,6 +54,44 @@ export class Config {
3754 this . configureLang ?. dispose ( ) ;
3855 }
3956
57+ private readonly extensionConfigurationStateKey = "extensionConfigurations" ;
58+
59+ /// Returns the rust-analyzer-specific workspace configuration, incl. any
60+ /// configuration items overridden by (present) extensions.
61+ get extensionConfigurations ( ) : Record < string , Record < string , unknown > > {
62+ return pickBy (
63+ this . workspaceState . get < Record < string , ConfigurationTree > > (
64+ "extensionConfigurations" ,
65+ { } ,
66+ ) ,
67+ // ignore configurations from disabled/removed extensions
68+ ( _ , extensionId ) => vscode . extensions . getExtension ( extensionId ) !== undefined ,
69+ ) ;
70+ }
71+
72+ async addExtensionConfiguration (
73+ extensionId : string ,
74+ configuration : Record < string , unknown > ,
75+ ) : Promise < void > {
76+ const oldConfiguration = this . cfg ;
77+
78+ const extCfgs = this . extensionConfigurations ;
79+ extCfgs [ extensionId ] = configuration ;
80+ await this . workspaceState . update ( this . extensionConfigurationStateKey , extCfgs ) ;
81+
82+ const newConfiguration = this . cfg ;
83+ const prefix = `${ this . rootSection } .` ;
84+ await this . onDidChangeConfiguration ( {
85+ affectsConfiguration ( section : string , _scope ?: vscode . ConfigurationScope ) : boolean {
86+ return (
87+ section . startsWith ( prefix ) &&
88+ get ( oldConfiguration , section . slice ( prefix . length ) ) !==
89+ get ( newConfiguration , section . slice ( prefix . length ) )
90+ ) ;
91+ } ,
92+ } ) ;
93+ }
94+
4095 private refreshLogging ( ) {
4196 log . info (
4297 "Extension version:" ,
@@ -176,18 +231,43 @@ export class Config {
176231 // We don't do runtime config validation here for simplicity. More on stackoverflow:
177232 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
178233
179- private get cfg ( ) : vscode . WorkspaceConfiguration {
234+ // Returns the raw configuration for rust-analyzer as returned by vscode. This
235+ // should only be used when modifications to the user/workspace configuration
236+ // are required.
237+ private get rawCfg ( ) : vscode . WorkspaceConfiguration {
180238 return vscode . workspace . getConfiguration ( this . rootSection ) ;
181239 }
182240
241+ // Returns the final configuration to use, with extension configuration overrides merged in.
242+ public get cfg ( ) : ConfigurationTree {
243+ const finalConfig = cloneDeep < ConfigurationTree > ( this . rawCfg ) ;
244+ for ( const [ extensionId , items ] of Object . entries ( this . extensionConfigurations ) ) {
245+ for ( const [ k , v ] of Object . entries ( items ) ) {
246+ const i = this . rawCfg . inspect ( k ) ;
247+ if (
248+ i ?. workspaceValue !== undefined ||
249+ i ?. workspaceFolderValue !== undefined ||
250+ i ?. globalValue !== undefined
251+ ) {
252+ log . trace (
253+ `Ignoring configuration override for ${ k } from extension ${ extensionId } ` ,
254+ ) ;
255+ continue ;
256+ }
257+ log . trace ( `Extension ${ extensionId } overrides configuration ${ k } to ` , v ) ;
258+ set ( finalConfig , k , v ) ;
259+ }
260+ }
261+ return finalConfig ;
262+ }
263+
183264 /**
184265 * Beware that postfix `!` operator erases both `null` and `undefined`.
185266 * This is why the following doesn't work as expected:
186267 *
187268 * ```ts
188269 * const nullableNum = vscode
189270 * .workspace
190- * .getConfiguration
191271 * .getConfiguration("rust-analyzer")
192272 * .get<number | null>(path)!;
193273 *
@@ -197,7 +277,7 @@ export class Config {
197277 * So this getter handles this quirk by not requiring the caller to use postfix `!`
198278 */
199279 private get < T > ( path : string ) : T | undefined {
200- return prepareVSCodeConfig ( this . cfg . get < T > ( path ) ) ;
280+ return prepareVSCodeConfig ( get ( this . cfg , path ) ) as T ;
201281 }
202282
203283 get serverPath ( ) {
@@ -223,7 +303,7 @@ export class Config {
223303 }
224304
225305 async toggleCheckOnSave ( ) {
226- const config = this . cfg . inspect < boolean > ( "checkOnSave" ) ?? { key : "checkOnSave" } ;
306+ const config = this . rawCfg . inspect < boolean > ( "checkOnSave" ) ?? { key : "checkOnSave" } ;
227307 let overrideInLanguage ;
228308 let target ;
229309 let value ;
@@ -249,7 +329,12 @@ export class Config {
249329 overrideInLanguage = config . defaultLanguageValue ;
250330 value = config . defaultValue || config . defaultLanguageValue ;
251331 }
252- await this . cfg . update ( "checkOnSave" , ! ( value || false ) , target || null , overrideInLanguage ) ;
332+ await this . rawCfg . update (
333+ "checkOnSave" ,
334+ ! ( value || false ) ,
335+ target || null ,
336+ overrideInLanguage ,
337+ ) ;
253338 }
254339
255340 get problemMatcher ( ) : string [ ] {
@@ -367,26 +452,24 @@ export class Config {
367452 }
368453
369454 async setAskBeforeUpdateTest ( value : boolean ) {
370- await this . cfg . update ( "runnables.askBeforeUpdateTest" , value , true ) ;
455+ await this . rawCfg . update ( "runnables.askBeforeUpdateTest" , value , true ) ;
371456 }
372457}
373458
374- export function prepareVSCodeConfig < T > ( resp : T ) : T {
459+ export function prepareVSCodeConfig ( resp : ConfigurationValue ) : ConfigurationValue {
375460 if ( Is . string ( resp ) ) {
376- return substituteVSCodeVariableInString ( resp ) as T ;
377- // eslint-disable-next-line @typescript-eslint/no-explicit-any
378- } else if ( resp && Is . array < any > ( resp ) ) {
461+ return substituteVSCodeVariableInString ( resp ) ;
462+ } else if ( resp && Is . array ( resp ) ) {
379463 return resp . map ( ( val ) => {
380464 return prepareVSCodeConfig ( val ) ;
381- } ) as T ;
465+ } ) ;
382466 } else if ( resp && typeof resp === "object" ) {
383- // eslint-disable-next-line @typescript-eslint/no-explicit-any
384- const res : { [ key : string ] : any } = { } ;
467+ const res : ConfigurationTree = { } ;
385468 for ( const key in resp ) {
386469 const val = resp [ key ] ;
387470 res [ key ] = prepareVSCodeConfig ( val ) ;
388471 }
389- return res as T ;
472+ return res ;
390473 }
391474 return resp ;
392475}
0 commit comments