@@ -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 } ${ extensionId }  , 
254+                     ) ; 
255+                     continue ; 
256+                 } 
257+                 log . trace ( `Extension ${ extensionId } ${ k }  ,  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