Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP]: Track recently selected languages #305

Closed
wants to merge 0 commits into from
Closed

[WIP]: Track recently selected languages #305

wants to merge 0 commits into from

Conversation

demalex
Copy link

@demalex demalex commented Oct 14, 2018

Description

Implementation of enhancement #299 suggested by @cmyr. Every time user makes a language selection, the language is stored to UserDefaults.

Using UserDefaults directly makes classes tightly coupled with it and makes it hard to maintain and test. To get rid of problems like this, UserDefaults was covered with additional abstraction. The idea is to keep settings provider in one place (AppDelegate for now) and inject it as dependency into other consumers. There are other places where UserDefaults is used directly and from my perspective it requires to be refactored, but such refactoring is out of this enhancement scope.

SettingsProvider is responsible for communication with KeyValueStorage. It executes save and fetch operations, while SettingsProvidingProcessor decorates SettingsProvider and brings additional logic for save languages operation. Appropriate test cases also have been added.

Screenshot

screen shot 2018-10-16 at 8 39 39 pm

@cmyr
Copy link
Member

cmyr commented Oct 15, 2018

Cool, thanks for this. Will have to give it a closer read, but looks nice and tidy at first glance, and thanks for the tests!

(if anyone else @xi-editor/xi-mac wants to take a look here that would be welcome!)

Copy link
Contributor

@jansol jansol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tests! Way too little attention has been paid to them on the xi-mac side so far.

The dependency injection makes sense, but I don't think the settings storage needs quite that many levels of abstraction.

XiEditor/Storage/KeyValueStorage.swift Outdated Show resolved Hide resolved
XiEditor/Storage/SettingsProvider.swift Outdated Show resolved Hide resolved
XiEditor/Storage/SettingsProvider.swift Outdated Show resolved Hide resolved
XiEditor/Storage/SettingsProvidingProcessor.swift Outdated Show resolved Hide resolved
XiEditorTests/Mocks/DictionaryStorage.swift Outdated Show resolved Hide resolved
XiEditorTests/SettingsProviderTests.swift Outdated Show resolved Hide resolved
XiEditorTests/SettingsProvidingProcessorTests.swift Outdated Show resolved Hide resolved
XiEditor/Storage/SettingsProvider.swift Outdated Show resolved Hide resolved
XiEditor/Storage/SettingsProvider.swift Outdated Show resolved Hide resolved
@demalex
Copy link
Author

demalex commented Oct 15, 2018

@cmyr @jansol I have updated license headers and changed components naming a bit. Also I have added some info for conversations above. Please, have a look. Thanks!

@cmyr
Copy link
Member

cmyr commented Oct 15, 2018

I've given this a read through, and I do see @jansol's point: it feels like there's maybe one more layer of abstraction than is necessary here? I like the idea of using a protocol for the main interface, but I did find the overall inheritance chain tricky to follow, in a way that didn't seem totally necessary.

For instance: if SettingsProcessor was combined with SettingsStore, I don't think this would make the code any harder to maintain?

@demalex
Copy link
Author

demalex commented Oct 16, 2018

@cmyr Yes, it makes sense. The initial idea was just to separate store interaction logic and business logic, so they both could be modified independently. But we can definitely combine this two classes and it wont make the codebase any harder to maintain. Lets keep it simple at least at this stage of project.

@demalex
Copy link
Author

demalex commented Oct 16, 2018

@cmyr @jansol Guys, I have removed SettingsProcessor as separate entity. Now there is only SettingsStore that conforms to SettingsProvider protocol. As we discussed with @jansol KeyValueStorage protocol is good to have for testing and mocking, so this protocol remains the same. Also, I have attached a screenshot to initial post. Please, have a look! Thank you!

@jansol
Copy link
Contributor

jansol commented Oct 16, 2018

So languages are supposed to show as in use in both "recently used" and the general list at the same time? That seems like reasonable behavior, but I don't see where the recently used one is marked as in use in the code. Missed when committing?

@demalex
Copy link
Author

demalex commented Oct 16, 2018

@jansol Hmm, no in use language is supposed to be selected only in general list section. I am sorry, but I think screenshot is misleading and I even do not know how I made it with that item selected. I will update the screenshot.
Do you think it makes sense to show language as selected in both sections?

@jansol
Copy link
Contributor

jansol commented Oct 16, 2018

Yeah, I think it's more consistent that way.

@demalex
Copy link
Author

demalex commented Oct 16, 2018

@cmyr @jansol Updated screenshot and implementation. Please, have a look!

Copy link
Contributor

@jansol jansol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

I'm not 100% sure that the indentation is the right thing to do for recently used languages, but won't block this PR on that. If you know of some prior art for this sort of thing, please let me know.

@demalex
Copy link
Author

demalex commented Oct 17, 2018

@jansol Great! Thanks a lot! As for that indentation, I just had a look at macOS system implementations and the indentation is used in very rare variety of cases, so I will probably remove it.

Here are few examples from system implementation:
screen shot 2018-10-17 at 11 46 48 am screen shot 2018-10-17 at 11 48 03 am

Also, do you think we need to add available languages title? I think it will be more consistent that way. What do you think?

@jansol
Copy link
Contributor

jansol commented Oct 17, 2018

Yeah looks like not indenting is more in line with the rest of the system. (And we can always put the indentation back if it turns out problematic)

I don't think a subtitle for the available languages is necessary - the menu entry that opens this menu is already called Language so that seems quite obvious...

@cmyr
Copy link
Member

cmyr commented Oct 17, 2018

I agree that the indentation might not be necessary.

I've played around with this, and I do have a couple notes:

  • Ideally the list would always be the five most recently used languages. However, if I select a language that's already recently used, its state isn't updated; so if I select the fifth most recently used language, and then select a new language, the language I was just using is removed from the list. My solution might be to always have the recents list be in descending order, most recently used to last?

  • I was able at some point to get a language called 'Language' added to the recents list. This was on first launch. I'm not sure how I did this. 😕

Overall this looks good, and feels like a nice improvement; I look forward to getting it merged. 👍

Copy link
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay wanted to go do one last pass through the code, looks good, two little nits.

@@ -0,0 +1,23 @@
// Copyright 2016 The xi-editor Authors.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be 2018. 😅

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cmyr 😆 You are right. I have just copy pasted the header. We might need to have custom header and automatically add it to all newly created source files. What do think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an Xcode feature? I'd forgotten about that, it could be helpful though!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is.

XiEditor/Storage/KeyValueStorage.swift Outdated Show resolved Hide resolved
@demalex
Copy link
Author

demalex commented Oct 18, 2018

@cmyr I just played with the application and feature for a while and did not have a change to reproduce the issue with initial Language value. Could you please provide more details?

As for descending ordering I think it would be better to have it. So, I will apply change and let you know when it is ready! Thanks.

@demalex
Copy link
Author

demalex commented Oct 18, 2018

@cmyr I have updated implementation. Please, have a look at it! Now most recent language appears at the top of the list and pushes previously used languages to the bottom. If one of the recently used languages is selected it is moved to the top of the list again. Thus, recently used languages list represents ordered history of languages which were used.

Copy link
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't reproduce my earlier issue, but I do now have an issue where I can somehow make the menu only show the recent languages?

If I open a blank document (with nsuserdefaults cleared, not sure if that is important) and select a few different languages from the list, and then I open a file with a known extension that is not one of the languages I've already chosen, the available languages disappear?

There's some other strange sync stuff in here as well; for instance on first launch, I can end up in a state where the frontmost view is assigned the incorrect language in the menu.

This stuff all feels a bit race-y; I suspect that funny things are happening when languageChanged gets called a bunch of times by core, while the frontend is still doing some setup?

In any case maybe play around and see if you can see any of these issues?

@demalex demalex closed this Oct 19, 2018
@demalex demalex reopened this Oct 19, 2018
@demalex
Copy link
Author

demalex commented Oct 19, 2018

@cmyr Yes, I can see the issue. It appeared because I have changed Language menu building logic a bit.
From what I see, when application started it receives selected languages and available languages notifications. But, when main window is closed and then opened again only selected language notification is received. Could you please tell, whether it is right behavior?

@cmyr
Copy link
Member

cmyr commented Oct 19, 2018

@demalex Ah, right, this makes sense.

What we should do is handle available_languages in the app delegate, and then make it accessible from there to all view controllers, instead of trying to handle it in the view controller itself.

basically: available_languages is really a global setting, but selected_language is scoped to the individual document.

@demalex
Copy link
Author

demalex commented Oct 19, 2018

@cmyr Initially I had same thought, but then I decided to clarify this behaviour. Thanks for the explanation!

Basically I renamed SettingsStore to LanguagesStore and now it can provide recently used languages as well as available languages. I think Settings it is another entity and it requires separate abstraction to be created (it could be very similar to LanguagesStore).

I do not think that it is a good idea to keep all shared objects in AppDelegate and get access them by calling to shared application and then make a cast to AppDelegate. Such approach violates design rules and makes code hard to maintain, change and test. I would rather pass dependencies explicitly and use DI container, but it requires code to be refactored and such improvement out of this feature scope.

Nevertheless, I have updated the implementation. Please have a look and let me know what you think.

Thanks!


let newLanguage = language.trimmingCharacters(in: .whitespacesAndNewlines)

if newLanguage.count == 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check seems like a perfect job for a guard 😄
guard newLanguage.count > 0 else { return }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmatoszko you think if can't handle it? 😎 It is not a big difference in this situation what to use. I think it easy to read simple if than guard. guard is good when you need to declare variables but do not want to increase cyclomatic complexity. Yes, compiler checks and if there is no return at some point it breaks compilation, but in such case (having tests) it is not so critical.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally agree 👍


class DictionaryStorage: KeyValueStorage {

var dictionary: [String : Any]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not private?

Copy link
Author

@demalex demalex Oct 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmatoszko DictionaryStorage should be used for tests only when you need to mock a class that conforms to KeyValueStorage protocol. The property is not private because sometimes during testing it is needed to check what exact values were saved to storage. Having this property public allows to do that.

@cmyr
Copy link
Member

cmyr commented Oct 21, 2018

@demalex I definitely agree that our use of app delegate is pretty sketchy, and I'm open to refactoring. One related project is #275, which would move most of the RPC handling stuff out of the app delegate as well.

Have been a bit distracted the past few days, but will try to give this another proper review either tonight or tomorrow. Thanks for your patience!

@demalex
Copy link
Author

demalex commented Oct 22, 2018

@cmyr Thanks a lot and take your time! Let me know if you think that something needs to be improved. I will have a look at another task for now.

Copy link
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this looks generally good. I have a couple little notes, but overall happy to get this in soon!

private var currentLanguage: String = ""

/// Object provides available and recently used languages
var languagesProvider: LanguagesProvider!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so I've been thinking about this a bit:

I think it's important that we have a clear sense of what the source of truth is for language information. I'm thinking of the situation where (theoretically) a new plugin is loaded in core that adds a new language: What happens in this case? As currently implemented, the LanguagesProvider in the AppDelegate would get updated, but the individual views wouldn't end up being notified?

There are a variety of approaches to this. One simple one would be to have all access to the languagesProvider be explicitly shared; the view controller would be returning a reference to the document, which would be returning the shared object in the AppDelegate. Then when the state changed, we would iterate through the view controllers and notify them of the state change, and they can reload menus etc as needed.

Alternatively, when the state gets set in core we could go and set the new languageProvider on each view controller, and then handle any updates in a didSet method.

Another confusion here is that because LanguagesStore is a class, everyone should currently be sharing a single instance, and so the state should be in sync; but LanguagesProvider isn't declared as a class protocol, so if it were implemented as a struct this would no longer be true?

Another thing I might think about is whether this state belongs more in the document or in the view controller. I think it might be reasonable to have it only be in the document? I could go either way, though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cmy Thank you for review!

You are right, class protocol constraint definitely needs to be added to LanguagesProvider 👍🏻

Ideally ViewController should not contain any state. ViewController is an entity of UI / Presentation layer and its main responsibility to be a glue between model (view-model) and view and handle view's lifecycle. I definitely would not keep state in view controller and most likely it is Document object's responsibility.

I have been thinking about it a bit and I came up with possible solution for the problem. The idea is to have LanguagesStore as single source of truth and listeners which can listen to notifications about the updates. The LanguageStore itself can be updated by receiving notification from the core. I have created high level UML diagram to make it a little bit clear. Please, have a look:

languages store-2

Diagram is high level in some cases (Document and XiDocumentController, Core), but still brings visibility how data flows from presentation layer to core and to LanguagesStore.

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think we're broadly on the same page; what we want is a single source of truth and a notification mechanism.

We have options in terms of what our notification mechanism is. The simplest is that when the state changes, we iterate through our documents and call some stateChanged() function; then each document goes and checks the new state and performs some update logic. Another reasonable option is to lean on the Cocoa NotificationCenter infrastructure, which you're doing here. In this case, though, I think it makes more sense to pass some reference type containing the state along as the notification's object; so in this case I would register for, say, the 'availableLanguagesChanged' notification, and the notification would contain a reference to some type, say, CoreState, (which can be a protocol) which has a member var availableLanguages { get }.

I think this is mostly what you're suggesting? The main thing I want to push is that we'd like to balance the benefits (clear separation of responsibilities, clear flow of data) against the costs of increasing architectural complexity; so whatever we do here should be something that we anticipate being the general model for communicating changes to shared state.

Copy link
Author

@demalex demalex Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cmyr Yes, its true! This is what I am suggesting. Please, take a note that LanguageStoringNotification is actually an interface. Interface name is misleading and will be changed. Having interface makes a loose coupling and allows to do checking during compile time, while NSNotification.Name is actually a String and it is easy to break an implementation. So, I would not use NotificationCenter.

So, the basic idea is very simple:

  1. Subscriber implements protocol and can be subscribed for receiving updates from LanguagesStore
  2. LanguagesStore receives updates from the Core, updates it's state and notifies all subscribers by iterating over the collection and calling appropriate function
  3. As you mentioned above, updated state of the store (as a type of lets say LanguageStoringOutput) can passed to the function as parameter

What do you think? Does it make sense?

Architecture is what allows you to manage complexity and makes it easy to adapt the code base for new requirements. So, there is always some level of architecture complexity. But, I agree, there should be a trade off.

Please, let know what do you think about my suggestion. Thank you!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thanks for the explanation. I think we're on the same page, and this approach sounds reasonable. 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My one thought is that we might want to eventually expand this so that we have a single mechanism for various types of state (themes are an obvious example), so I'd keep that in mind; for instance we might want multiple types of events that can be subscribed to, or we might want them to be a package deal.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cmyr Thank you very much for the feedback! Yes, I am also thinking about generic solution that can be used in variety of cases. I was busy at work these days and going to have look at it on the weekend. Thanks!

protocol LanguagesProvider {
func fetchRecentlyUsedLanguages() -> [String]
func saveRecentlyUsed(language: String)
var availableLanguages: [String] { get set }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we really want the set method exposed here? availableLanguages should only really be set in response to a core RPC, so exposing the setter to other users feels a bit iffy.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cmyr You are absolutely right. I think we need to change data flow a bit. I have some ideas and will share it soon.

@cmyr
Copy link
Member

cmyr commented Nov 3, 2018

@demalex just checking in, if this is ready for a review just 🔔 me!

@demalex
Copy link
Author

demalex commented Nov 6, 2018

@cmyr Sorry for the delay in answering. I am busy with work lately, but I am moving on with implementation. I hope, I can finish it soon. Let me know if it acceptable.

@cmyr
Copy link
Member

cmyr commented Nov 6, 2018

@demalex absolutely, work at whatever pace is good for you. I'm generally more worried that things are blocked on review or some question I've missed, so wanted to make sure that wasn't the case.

@demalex demalex changed the title Track recently selected languages WIP: Track recently selected languages Nov 11, 2018
@demalex demalex changed the title WIP: Track recently selected languages [WIP]: Track recently selected languages Nov 11, 2018
@cmyr
Copy link
Member

cmyr commented Dec 21, 2018

@demalex are you still making progress on this? It feels like it's really close, and it would be nice to get this feature done, but no worries if you're busy. 😃

@demalex
Copy link
Author

demalex commented Dec 23, 2018

@cmyr Hi! Excuse me that it is taking so long to complete this. I a little bit overloaded at work at the end of the year, but I am making a progress and submit a new patch set soon. Hope it works for you.
Happy holidays! 🎄

@cmyr
Copy link
Member

cmyr commented Dec 23, 2018

@demalex amazing, glad to hear you're still hacking away. Happy holidays!

@demalex demalex closed this Feb 14, 2019
@demalex
Copy link
Author

demalex commented Feb 14, 2019

Closed it due to big difference between master and feature branch. Will resubmit it soon

@jansol
Copy link
Contributor

jansol commented Apr 15, 2019

@demalex ping?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants