-
Notifications
You must be signed in to change notification settings - Fork 7.6k
Conversation
# Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
@RaymondLim regarding a project scope, I'm not sure that we'd want one (a .brackets.state.json file in every project). On the one hand, it seems cleaner to do that than storing these project-level values in the user's global file. On the other hand, we'd basically be creating .brackets.state.json in every directory the user opens and the user would need to keep adding it to .gitignore and such. For that reason, I suggest that we stick with just the single state.json file in the user's appdata. If we were getting fancy, we could have a Layer object that automatically pulls out the project-specific view state. If we didn't do that, we could still do the equivalent in the meantime... in other words, have the view state look something like this: {
"project": {
"/path/to/project": {
"project.files": [list of project files here]
}
}
} Without a layer, you could basically just call getViewState("project") and then navigate the JS objects the normal way. With a layer, the project would be automatically set and calling getViewState("project.files") would return the correct value for the current project. If you look at the path layer in PreferencesBase, you'll see that layers don't really do anything fancy other than look inside of the JSON data for a preference. |
@dangoor Do you have an example of using a layer object? I can create "project.files" key with the data hierarchy that you suggested, but I have to navigate the JS objects to set/get the files. And I also wonder how to call convertPreferences to migrate old preferences to the new pref key with your suggested structure. I know that I can provide new key as "user project.files" via my callback but still don't know how to tell the location. So I think we also need to handle the layer objects in convertPreferences function. |
Assuming there's a ProjectLayer that looks something like this (untested, adapted straight from PathLayer): function ProjectLayer() {
this.projectPath = null;
}
ProjectLayer.prototype = {
key: "project",
get: function (data, id, context) {
if (!data || !this.projectPath) {
return;
}
if (data[this.projectPath] && data[this.projectPath][id]) {
return data[this.projectPath][id];
}
return;
},
getPreferenceLocation: function (data, id, context) {
if (!data || !this.projectPath) {
return;
}
if (data[this.projectPath] && data[this.projectPath][id]) {
return this.projectPath;
}
return;
},
set: function (data, id, value, context, layerID) {
if (!layerID) {
layerID = this.getPreferenceLocation(data, id, context);
}
if (!layerID) {
return false;
}
var section = data[layerID];
if (!section) {
data[layerID] = section = {};
}
if (value === undefined) {
delete section[id];
} else {
section[id] = value;
}
return true;
},
getKeys: function (data, context) {
if (!data) {
return;
}
return _.union.apply(null, _.map(_.values(data), _.keys));
},
setProjectPath: function (projectPath) {
this.projectPath = projectPath;
}
} You would instantiate that layer and add it to the "user" scope in the stateManager. Then, when the project changes you need to call You're correct that convertPreferences knows nothing about this, so it would need to be extended in some way to support that. I don't know if there's any conflict between our changes, but you might consider rebasing on top of dangoor/6578-hidden-editor-options because I did change a fair bit there. You decide, though... my work there wasn't focused on things that are likely to impact the view state work much so any conflicts shouldn't be too hard to resolve. |
…failing test in WorkingSetView tests.
@@ -155,7 +155,7 @@ define(function (require, exports, module) { | |||
}); | |||
}); | |||
|
|||
it("should rebuild the ui from the model correctly", function () { | |||
xit("should rebuild the ui from the model correctly", function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dangoor I have to comment out this specific test since it's using localStorage.setItem("doLoadPreferences", true)
and localStorage.removeItem("doLoadPreferences")
, and I don't know how to convert them to the new preferences model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to be the only test that is using the doLoadPreferences
mechanism. It looks like this test is verifying that the files in the working set automatically get re-added to the working set. You could add an option to SpecRunnerUtils.createTestWindowAndRun
(something like options.viewState) that takes an object to be passed to the MemoryStorage when the stateManager is created... that's an easy way for a test to control the initial values on startup.
@dangoor Thanks a lot for your guide in designing the new PathLayer. I've already tried and it does not work mainly due to the empty "data" in the "user" scope object. The first time the layer object is called to set a value, it checks for the existence of "project" key and then "project[folderPath]" key and bails out if any of them is missing. I did call Update: I was able to fix the issue of bailing out when "project" key and then "project[folderPath]" key are missing. I modified getPreferenceLocation to prevent from bailing as follows.
Now I have one new bigger issue. All view states are stored in the layer regardless of whether it belongs to a project or not. |
You should be able to call stateManager.set("key", "value", {
scope: "user",
"layer": "project",
"layerID": "/foo/bar/baz"
}); Or, you could call it without the layerID and have the ProjectLayer fill that in. ...but it appears that you can't because a minor change would need to be made to if (layer) {
var wasSet = layer.set(this.data[layer.key], id, value, context, location.layerID); That snippet of code should check to see if Does that make sense? |
I'm done with all the conversion including two view settings using the project layer. So it's ready for the review. One exception is the commented test case that is using localStorage. |
@@ -213,7 +212,7 @@ define(function main(require, exports, module) { | |||
} else { | |||
LiveDevelopment.hideHighlight(); | |||
} | |||
prefs.setValue("highlight", config.highlight); | |||
PreferencesManager.setViewState("livedev.highlight", config.highlight); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this one view state or an actual pref? Given that it's coming from a menu item, I think this should probably be a preference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, we need a good definition of what constitutes a pref and what is a pure view state. I talked to @peterflynn and his view seems to be different from you. He considers any settings that users do not want to share across machines to be a view setting regardless of whether the user can change it from the UI. We talked about three settings that users can change from Brackets UI; sorting order in the working set, case and regexp settings in Find bar and livedev highlighting. I was originally thinking all of them to be actual pref, but he convinced me to see them as view settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had been thinking of this difference in terms of what someone might edit by hand, but I like that definition of Peter's because it's really simple. Given that, making it view state seems fine by me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one vote in favor of the definition being "what someone might edit by hand" is in #6815. That adds a "staticserver.port" pref. You might want to share that between machines, but maybe not. The main thing is that humans have a reason to change that value.
I'm still going on the review (I'm at ProjectManager). The way DocumentManager is working with the ProjectLayer isn't really the way layers are intended to be used... the layer seemed to be adding more code when it should actually be simplifying things. It may be possible to actually just delete some code there, but I'll need to think about whether there are consequences. Ideally, the project path is just set in ProjectManager and DocumentManager would not need to look up the project path but just set its state on |
@RaymondLim I just pushed a sample change to e7c4eb6 so that you can see what I mean. Here's how the ProjectLayer should work, given that the project path is set in ProjectManager. When setting a view state value:
When retrieving a value, it tries to find the most specific matching value by walking the scopes and the layers in those scopes. So, it starts with the user scope and the project layer to see if there's a value there. So, (Edit: the remaining files would need to be cleaned up like DocumentManager before I finish the rest of the review) |
I don't think the performance will be any worse than what we have now, given that we've been storing this data in a single key in localStorage... but I noticed that my file is 50k already and I don't have that many projects or working set files. It's just a lot more obvious how much data we're storing in here! At some point, we might find it necessary to create a new Storage object that stores this data more efficiently (possibly in IndexedDB with a separate key for each preferences key...) I'll make a note on the card that in scenario testing we might want to test file switching performance. |
Thanks for the sample change to use the project layer the right way. I just pushed another commit with the necessary changes. And I know that the state.json file can be pretty big due to the UpdateInfo that we store there. |
For the deprecation warnings, I was thinking of utility function that is something like this: displayedWarnings = {};
function deprecationWarning(message) {
if (displayedWarnings[message]) { return; }
var error;
try {
deprecationWarning.doesNotExist();
} catch (e) {
error = e;
}
console.warn(message);
console.warn(error.stack);
displayedWarnings[message] = true;
}
exports.deprecationWarning = deprecationWarning; That displays each message just once and includes a stack which will help the user identify where the problem is. Python's deprecation warnings also let you strip off some stack frames so that you can point directly at the caller. If you wanted to do that, there's a library for generating stack traces that returns consistently formatted traces in an array. But that's just a nice-to-have. |
I think the deprecation warning utility function and doc comments for the ProjectLayer are all that's left here. I'll probably do a bit more testing, but then I think this is ready for merging and scenario testing. |
@dangoor Can you explain what the try block supposed to do? Especially, it's the |
@RaymondLim It's getting an exception object so that you can get the stack in order to print what caused the deprecation warning to be displayed. As far as I know, causing an exception to be raised is the only way to get a stack trace in JS. |
Got it. Thanks! Instead of the try block, we can also explicitly get the stack with console.warn(new Error().stack); Also, I have a question for you regarding of displaying each message once. You suggested to show the preference key or client ID with the message. If so, the message for each deprecated api call will be different. Or am I missing something for not to be spammy with deprecation messages? |
Conflicts: src/preferences/PreferencesManager.js
This is following the pattern @RaymondLim established in #6740.
@RaymondLim I missed that you had commented here. Sorry! The general idea is that we want to show each distinct usage of the deprecated call so that the person who wrote that code can go and fix it, without repeatedly showing that same call. |
@dangoor Ready for re-review. I think I implemented the deprecation warning as the way you want, although I'm not using stack trace library that you suggested. |
@RaymondLim The new DeprecationWarning module looks great at first glance (two minor nits: it's 2014, and you don't need the I'll finish the review in the morning. |
…in jslint directive any more.
// migrating the old preferences to the new model. So if this is called without | ||
// having _doNotCreate set to true, then the caller is using the old preferences model. | ||
if (!_doNotCreate) { | ||
DeprecationWarning.deprecationWarning("getPreferenceStorage is called with client ID '" + clientID + ",' use PreferencesManager.definePreference instead."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If clientID is an object, this will just say getPreferenceStorage is called with client ID '[object Object]'
.
I fixed the nits, since they were tiny things. I'm going to file a followup bug about the disabled test, but otherwise I think this is good to go. |
Preferences / view state migration
No description provided.