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

[REFACTORING] Improve Hedy Editor interface #4435

Merged
merged 26 commits into from
Sep 13, 2023

Conversation

@jpelay jpelay marked this pull request as draft August 28, 2023 18:38
@jpelay
Copy link
Member Author

jpelay commented Sep 4, 2023

I changed the way in which we create the editors, and now all of the internal logic that has to do with Ace is handled in the constructor. I kept the factory, but now there are only 2 methods there: initializeWritableEditor and initializeReadOnlyEditor that in turn call the constructor of the HedyAceEditor class. I also added an Enum so we can properly set up the editor when we call the constructor. The next step now is to remove the make the HedyEditor emit events itself!

setEditorMode: (isReadMode: boolean) => void;
getBreakpoints(): Breakpoints;
// TODO: improve this type definition
on(key: any, handler: any): void;
Copy link
Member Author

Choose a reason for hiding this comment

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

Guys I don't know how I can put the types that I used in the definition of this function here, can someone help?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's start with the declaration of HedyAceEditor.on():

  public on(key: Parameters<typeof this.editorEvent.on>[0], handler: Parameters<typeof this.editorEvent.on>[1]) {

Let's look at this fragment:

Parameters<typeof this.editorEvent.on>[0]

This says:

  • Take the type of this.editorEvent.on (typeof turns a value into a type).
  • Assuming that's a function type, get the types of the parameter types of that function type
  • Then take the first element of that array

The reason you can't copy/paste this is because it contains typeof this.editorEvent.on; since this is an interface definition and not a class, we can't say "the field editorEvent on myself", because you don't even know that you have such a field.

But, we we know what type this.editorEvent would have if it existed, so we can substitute that!

Because on HedyAceEditor this is its declaration:

  private editorEvent = new EventEmitter<EditorEvent>({change: true});

So the type of this.editorEvent is EventEmitter<EditorEvent>. Indexing into a type looks slightly different from indexing into an object (for reasons that I also don't quite understand), so we need to replace typeof this.editorEvent.on => EventEmitter<EditorEvent>['on'].

Putting it all together, the first element would look like:

Parameters<EventEmitter<EditorEvent>['on']>[0]

Now that's obviously an ugly mouthful, so let's introduce a type alias to make that palatable:

type EditorEventEmitter = EventEmitter<EditorEvent>;
type OnEditorEventParameters = Parameters<EditorEventEmitter['on']>;

  on(key: OnEditorEventParameters[0], handler: OnEditorEventParameters[1]): void;

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you so muuuuch!!!! I really tried to look this up, but none of my attempts quite worked. Thank you for the detailed explanation!

@jpelay jpelay marked this pull request as ready for review September 6, 2023 16:08
@jpelay
Copy link
Member Author

jpelay commented Sep 6, 2023

This PR is ready for review!

@jpelay jpelay requested a review from rix0rrr September 6, 2023 16:14
Copy link
Collaborator

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

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

Nice! As always, I have some tips and questions to make you think, but I'll leave it completely up to you what to do with those!

Looking good though!

static/js/app.ts Outdated
theGlobalEditor = editorCreator.initializeWritableEditor($editor, EditorType.MAIN, dir);
attachMainEditorEvents();
error.setEditor(theGlobalEditor);
window.Range = ace.require('ace/range').Range // get reference to ace/range
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oof 😥. I see why this might be done, but sticking it window.Range feels a little dirty to me.

Can we not make a global:

let Range: /* needs a type here, not sure what that needs to be, maybe Ace.Range? */ Ace.Range;

// and then
Range = ace.require('ace/range').Range;

?

At least it would be keeping out of the global browser object.

Copy link
Member Author

Choose a reason for hiding this comment

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

The weird part is that this is not being referenced anywhere, so I thought this might be a property of the window object. If that's not the case maybe we can remove this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

window is the object that represents the global scope.

So if you do:

window.banana = 'banana';

At any point later you can do

console.log(banana);

And it will work

Copy link
Member Author

Choose a reason for hiding this comment

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

I understand. I removed the reference to windows.Range since it wasn't really used anywhere

static/js/app.ts Outdated
@@ -239,6 +244,79 @@ export function initializeCodePage(options: InitializeCodePageOptions) {
$('#program_name').on('blur', () => saveIfNecessary());
}

function attachMainEditorEvents() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You might pass the editor in as an argument, and get rid of at least one use of the global variable?

}
});

// Removed until we can fix the skip lines feature
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ohh, this is the "debugger", isn't it?

What's the issue with it?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, this is the skip line feature. It had a weird problem where if 2 different programs in different sessions where being executed at the same time and both had errors, they had their errors mixed up. We already have a fix for this in #4414, but we decided to wait a few weeks and keep testing before shipping it.

Comment on lines +7 to +10
COMMON_MISTAKES,
CHEATSHEET,
PARSONS,
EXAMPLE
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it important to make a distinction between these? Are they not all "just some syntax highlighted block" ?

Copy link
Member Author

Choose a reason for hiding this comment

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

All of them require different options! And I didn't want to expose a setOptions method, so I decided to set that up in the constructors. Since there are different kinds of these blocks I created this enum to configure all of them in the constructor. You can see that in the constructor.

"adventures_restored": "Die Standardabenteuer wurden wiederhergestellt.",
"adventures_restored": "The default adventures have been restored!",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oops?

} else { // It's an example editor
// Fits to content size
this._editor.setOptions({ maxLines: Infinity });
if(editorType === EditorType.CHEATSHEET) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oof, do these really all need to be different?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think so, because some of them serve different purpose and have different looks, I believe

}
}
} else {
if (editorType === EditorType.MAIN) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I just saw this same if a couple of lines above. Can they be collapsed? Is one of them never executed?

Copy link
Member Author

Choose a reason for hiding this comment

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

I just deleted one of those if!

export class HedyAceEditor implements HedyEditor {
private _editor: AceAjax.Editor;
private _markers?: Markers
isReadOnly: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

You probably want to make this

Suggested change
isReadOnly: boolean;
private isReadOnly: boolean;

Because right now in other parts of the code you're accidentally directly accessing this public field, instead of going through the getter/setter as I think you probably intended.

/**
* @returns if the editor is set to read-only mode
*/
public get getIsReadOnly(): boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably

public get isReadOnly(): boolean {
  return this._isReadOnly;
}

?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Or honestly, can you read directly from the editor you wrapped?

Copy link
Member Author

Choose a reason for hiding this comment

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

You are so right. I'm using the editor to handle that now.

Comment on lines +222 to +227
// TODO: FIX THIS
initializeDebugger({
editor: this._editor,
markers: this.markers,
level: theLevel,
language: theLanguage,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, it's interesting to think about whether the debugger should be inside the editor, or outside it.

You could say that the debugger is external to the editor, just using some features of the editor like "putting markers in the gutter" and "striking through certain lines".

But no need to change that now. You can always do that later, once you start adding a new editor implementation and you notice that there's a lot of repeated code between them 😉

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, exactly, this is a weird one. Because yeah, I can say that the debugger is part of an editor so it should be part of this interface. On the other hand, it'll probably have to deal with HTML and DOM objects, and we're trying to avoid to include those things in this interface.

However, I think that will become clearer once I start working on including code mirror and replacing the current debugger.

@jpelay jpelay mentioned this pull request Sep 7, 2023
4 tasks
@jpelay
Copy link
Member Author

jpelay commented Sep 7, 2023

I found that we can't remove the trimWhiteSpaceFunction since if we trim the contents of the editor every time you get the contents, you can't add spaces to the end of the line when editing.

@Felienne
Copy link
Member

OK, let's test this extensively on alpha before deploying :) @jpelay can you let me know when you are sure we can deploy? Maybe also test the new dashboard a bit (less core, so less important)

@mergify
Copy link
Contributor

mergify bot commented Sep 13, 2023

Thank you for contributing! Your pull request is now going on the merge train (choo choo! Do not click update from main anymore, and be sure to allow changes to be pushed to your fork).

@jpelay
Copy link
Member Author

jpelay commented Sep 13, 2023

OK, let's test this extensively on alpha before deploying :) @jpelay can you let me know when you are sure we can deploy? Maybe also test the new dashboard a bit (less core, so less important)

Yes, I will! And since the tests that we made for the editor that we remove earlier today work locally, I'll use them to test these changes in my computer. I'll let you know when I think everything's been tested extensively!

@mergify mergify bot merged commit 0a54e7c into hedyorg:main Sep 13, 2023
8 checks passed
@mergify
Copy link
Contributor

mergify bot commented Sep 13, 2023

Thank you for contributing! Your pull request is now going on the merge train (choo choo! Do not click update from main anymore, and be sure to allow changes to be pushed to your fork).

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.

[REFACTORING] Fix small issues from #4341
3 participants