@@ -9,51 +9,78 @@ open Microsoft.VisualStudio.Settings
99open Newtonsoft.Json
1010
1111type IPersistSettings =
12- abstract member Read :unit -> 't
13- abstract member Write :'t -> unit
12+ abstract member LoadSettings :unit -> 't
13+ abstract member SaveSettings :'t -> unit
1414
1515[<Guid( Guids.svsSettingsPersistenceManagerIdString) >]
1616type SVsSettingsPersistenceManager = class end
1717
1818type SettingsStore ( serviceProvider : IServiceProvider ) =
1919
2020let settingsManager = serviceProvider.GetService( typeof< SVsSettingsPersistenceManager>) :?> ISettingsManager
21- // settings quallified type names are used as keys, this should be enough to avoid collisions
22- let storageKey ( typ : Type ) = typ.Namespace+ " ." + typ.Name
21+
22+ let storageKeyVersions ( typ : Type ) =
23+ // "TextEditor" prefix seems to be required for settings changes to be synced between IDE instances
24+ [ " TextEditor.FSharp." + typ.Namespace+ " ." + typ.Name
25+ // we keep this old storage key to upgrade without reverting user changes
26+ typ.Namespace+ " ." + typ.Name]
27+
28+ let storageKey ( typ : Type ) = storageKeyVersions typ|> List.head
2329
2430// Each group of settings is a value of some named type, for example 'IntelliSenseOptions', 'QuickInfoOptions'
25- // We cache exactly one instance of each, treating them as immutable.
26- // This cache is updated by the SettingsStore when the user changes an option.
27- let cache = System.Collections.Concurrent.ConcurrentDictionary< Type, obj>()
31+ // and it is usually representing one separate option page in the UI.
32+ // We cache exactly one immutable value of each type.
33+ // This cache is updated by the SettingsStore when the user makes changes in the Options dialog
34+ // or when a change is propagated from another VS IDE instance by SVsSettingsPersistenceManager.
35+ let cache = ConcurrentDictionary< Type, obj>()
2836
29- let read () =
37+ let getCached () =
3038match cache.TryGetValue( typeof< 't>) with
31- | true , value -> value:?> 't
32- | _ -> failwithf" Settings%s are not registered." typeof< 't>. Name
39+ | true , (:? 't as value) -> value
40+ | _ -> failwithf" Settings%s are not registered." typeof< 't>. Name
3341
34- let write settings = cache.[ settings.GetType()] <- settings
42+ let keepInCache settings = cache.[ settings.GetType()] <- settings
43+
44+ // The settings record, even though immutable, is being effectively mutated in two instances:
45+ // when it is passed to the UI (provided it is marked with CLIMutable attribute);
46+ // when it is being populated from JSON using JsonConvert.PopulateObject;
47+ // We make a deep copy in these instances to isolate and contain the mutation
48+ let clone ( v : 't ) = JsonConvert.SerializeObject v|> JsonConvert.DeserializeObject< 't>
3549
3650let updateFromStore settings =
37- let result , json = settings.GetType() |> storageKey|> settingsManager.TryGetValue
38- if result= GetValueResult.Successthen
39- // if it fails we just return what we got
40- try JsonConvert.PopulateObject( json, settings) with _ -> ()
41- settings
42-
43- member __.Read () = read()
51+ // make a deep copy so that PopulateObject does not alter the original
52+ let copy = clone settings
53+ // if the new key is not found by ISettingsManager, we try the old keys
54+ // so that user settings are not lost
55+ settings.GetType() |> storageKeyVersions
56+ |> Seq.map( settingsManager.TryGetValue)
57+ |> Seq.tryPick( function GetValueResult.Success, json-> Some json| _ -> None)
58+ |> Option.iter( fun json -> try JsonConvert.PopulateObject( json, copy) with _ -> ())
59+ copy
4460
45- member __.Write settings =
46- write settings
47- // we replace default serialization with Newtonsoft.Json for easy schema evolution
61+ member __.Get () = getCached()
62+
63+ // Used by the AbstractOptionPage to populate dialog controls.
64+ // We always have the latest value in the cache so we just return
65+ // cloned value here because it may be altered by the UI if declared with [<CLIMutable>]
66+ member __.LoadSettings () = getCached() |> clone
67+
68+ member __.SaveSettings settings =
69+ // We replace default serialization with Newtonsoft.Json for easy schema evolution.
70+ // For example, if we add a new bool field to the record, representing another checkbox in Options dialog
71+ // deserialization will still work fine. When we pass default value to JsonConvert.PopulateObject it will
72+ // fill just the known fields.
4873 settingsManager.SetValueAsync( settings.GetType() |> storageKey, JsonConvert.SerializeObject settings, false )
49- |> Async.AwaitTask|> Async.StartImmediate
74+ |> Async.AwaitTask|> Async.Start
5075
51- member __.Register ( defaultSettings : 'options ) =
52- defaultSettings|> updateFromStore|> write
76+ // This is the point we retrieve the initial value and subscribe to watch for changes
77+ member __.Register ( defaultSettings : 'options ) =
78+ defaultSettings|> updateFromStore|> keepInCache
5379let subset = defaultSettings.GetType() |> storageKey|> settingsManager.GetSubset
54-
80+ // this event is also raised when a setting change occurs in another VS instance, so we can keep everything in sync
5581 PropertyChangedAsyncEventHandler( fun _ _ ->
56- ( read () : 'options) |> updateFromStore|> write
82+ ( getCached (): 'options) |> updateFromStore|> keepInCache
5783 System.Threading.Tasks.Task.CompletedTask)
5884|> subset.add_ SettingChangedAsync
85+
5986