-
Notifications
You must be signed in to change notification settings - Fork 289
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] Add interface for code editors #4341
Conversation
This is done! And the tests pass. There's a part that I'd like to discuss a bit more before moving forward, and that is the debugger. I think that maybe we can do an interface for the debugger in another PR, but we can discuss that tomorrow! |
After discussing today in the meeting we are planning to move this refactoring forward, and in the future we want to add an interface for the debugger, but since we need to learn how to to that in CodeMirror is better leaving that for a further PR. |
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 love what you're doing! Moving it all into one place makes the editor code much more manageable.
I happen to do TypeScript every day so I have a number of comments/tips 😉 but don't wait for me to merge. This is definitely better than we have today, and I don't want to stand in the way of progress.
As general comments, I think the following tips:
- The smaller an API is, the better. So any property/method/whatever has to earn their keep and be there for a reason. If we can rewrite something to get rid of it, so much the better!
- Let's try to keep a clear separation of responsibilities. If something depends on Ace, it should be in this class. If something does not, then it shouldn't.
- Think of the life cycle of an object. What information can an object have at creation time? What stays constant during its lifetime? That information can get passed in via the constructor.
But again, good work!
* | ||
*/ | ||
|
||
export interface HedyEditorMarker { |
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 still used?
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 forgot to delete this! The plan here was to make an interface of markes used in both editors. But I think that's not really doable, since both editor use quite a different way of handling markers and annotations.
* Ininitialize an editor that appears in a modal | ||
* @param $editor reference to the div that contains this editor | ||
*/ | ||
initializeModalEditor: ($editor: JQuery) => HedyEditor; |
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.
What's the difference between an editor that appears in a modal and an editor that appears elsewhere?
I wonder if it's the same as the distinction between the "main" editor and not?
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.
That's an interesting question. I think so .The main editor has different options than the modal editor, like adding options for the ask modal, and being runnable. It also has the debugger attached.
* @param isReadOnly to decide weather to remove the cursor | ||
* @param isMainEditor should we show the line numbers | ||
*/ | ||
turnIntoEditor: (element: HTMLElement, isReadOnly: boolean, isMainEditor: boolean) => HedyEditor; |
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.
Would we ever set isMainEditor: true
here? Because we also have initializeMainEditor
, seems like you would call that instead?
If it so happens to be the case that initializeMainEditor
calls turnIntoAceEditor
-- then that's fair enough, but now that we have classes and everything, you could have a private isMainEditor: boolean
field on the class, and then turnIntoEditor
could read that field.
And if you are first leaving everything mostly the same before you start further cleanup, to make the refactor simpler -- even more fair enough! 😅
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 it so happens to be the case that
initializeMainEditor
callsturnIntoAceEditor
-- then that's fair enough, but now that we have classes and everything, you could have aprivate isMainEditor: boolean
field on the class, and thenturnIntoEditor
could read that field.
Yeah exactly! initializeMainEditor
calls turnIntoAceEditor
I imagine that that field would be not in the class HedyAceCreator
but rather in the class HedyAceEditor
itself?
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.
And if you are first leaving everything mostly the same before you start further cleanup, to make the refactor simpler -- even more fair enough! sweat_smile
Also yes 😅 this first refactor was about putting everything in a single place, and seeing what could go in the interface!
/** | ||
* Represents whether there's an open 'ask' prompt | ||
*/ | ||
askPromptOpen: boolean; |
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 really an editor feature? Or is it more about the chrome that contains the editor?
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 thought a bit about whether to include this variable in the interface or not. This variable is used in the main editor to focus on the ask modal, and other things. I included here, because I think that aspects that control HTML of the page that contains the editor and also control behaviors of what one would think is the editor, should go in this interface.
* Set the highlither rules for a particular level | ||
* @param level | ||
*/ | ||
setHighliterForLevel: (level: number) => void; |
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.
"highlight" 🙃
This declares a function-valued member, which of course works. But, you will also be able to assign the member if you declare it like this, because it's a read/write field.
If you want to declare a method, it looks like this:
interface HedyEditor {
setHighlighterForLevel(level: number): void;
}
Isn't this function more about setting a level than setting a highligher?
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.
"highlight" upside_down_face
Nice catch!
This declares a function-valued member, which of course works. But, you will also be able to assign the member if you declare it like this, because it's a read/write field.
If you want to declare a method, it looks like this:
interface HedyEditor { setHighlighterForLevel(level: number): void; }
Thanks for the tip!
Isn't this function more about setting a level than setting a highlighter?
You mean it should be called, setLevel
? I think so, but maybe setHighlighterForLevel
is more explicit?
this._editor?.setOptions(options); | ||
} | ||
|
||
get editor(): AceAjax.Editor { |
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 this editor needs to be public (why?) then you can declare as a public member and you don't need the getter anymore.
public readonly editor: AceAjax.Editor;
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.
The editor being public is a temporary measure as I further refactor the debugger and the markers!
stopDebug(); | ||
}); | ||
|
||
this._markers = new Markers(this._editor!); |
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.
The functionality of the Markers will probably also need to move into this class in the future. Because that contains stuff like "set a marker on line 3", and that's going to be editor-specific as well.
But probably leave that for a future PR, no need to boil the ocean in this one!
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.
The functionality of the Markers will probably also need to move into this class in the future. Because that contains stuff like "set a marker on line 3", and that's going to be editor-specific as well.
But probably leave that for a future PR, no need to boil the ocean in this one!
Yes! But that requires also a bit of digging about markers in CodeMirror, and see what they share and build an abstraction that suits them both
// If prompt is shown and user enters text in the editor, hide the prompt. | ||
this._editor?.on('change', () => { | ||
if (this.askPromptOpen) { | ||
stopit(); |
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.
Now the editor needs to know about the JS running, and it needs to know about HTML elements that are not itself.
This is a bit of mixing responsibilities that we probably shouldn't have.
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 think the best thing to do is to probably make the HedyEditor
emit a change
event itself (either by being a full-blown EventEmitter
, or by making some simple callback mechanism ourselves), and leaving this logic where it was.
We should have some EventEmitter code in a file somewhere. If not, we can always build something like this ourselves
type ChangeListener = (x: string) => void;
interface HedyEditor {
public readonly onChange: ChangeListener[];
}
class AceHedyEditor {
public readonly onChange = new Array<ChangeListener>();
constructor() {
this.editor.on('change', () => {
const contents = this.getContents();
for (const listener of this.onChange) {
listener(contents);
}
});
}
}
// Consumer
editor.onChange.push((contents) => {
console.log('Editor changed to', contents);
});
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 love this idea!
getBreakpoints(): Breakpoints { | ||
return this._editor?.session.getBreakpoints() as unknown as Breakpoints; | ||
} |
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.
Same here, we probably need the functionality inside the class (because the Breakpoints
class is also Ace specific)
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.
Yes!
export type Breakpoints = Record<number, string>; | ||
|
||
|
||
export interface HedyEditorCreator { |
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.
Ahh, I see what you're doing. Makes it easy to switch out all different entry points in one go later, right?
Clever!
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.
Let's merge despite the small issues. @jpelay, shall we make 1 issue for the remaining open points so we don't forget?
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). |
Hi @rix0rrr! Thank you for your comments! Felienne and I discussed and we decided to merge this and address your comments ina future PR. I will also try to answer the questions you have about the code. |
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). |
**Description** After we merged #4341 there was a error were the errors were not being shown properly. This was a front-end error that happened because I didn't change a coupe of calls to the markers interface. This PR fixes that, in a way that needs to be addressed furthermore when we keep on working on #4382, namely: the Hedy Interface shouldn't have Markers inside it. **Fixes #4393 ** **How to test** Make some errors and see if the skipping errors feature works properly and the errors are being shown. **Checklist** Done? Check if you have it all in place using this list:* - [ ] Contains one of the PR categories in the name - [ ] Describes changes in the format above - [ ] Links to an existing issue or discussion - [ ] Has a "How to test" section If you're unsure about any of these, don't hesitate to ask. We're here to help!
**Description** This PR tackles the different comments left by @rix0rrr in #4341. The changes requested are the following: - [x] [Delete or develop the Hedy Editor Markers interface](#4341 (comment)) Deleted for now. - [x] [Maybe remove `initializeModalEditor` if the difference between a main editor if the same as "main" editor and not ](#4341 (comment)) - [x] [Remove `askPromptOpen` ](#4341 (comment)) - [x] [Change function-valued members to methods](#4341 (comment)) - [x] [Change name of `getValue` method to something more descriptive and maybe turn it into a read-only property](#4341 (comment)) - [x] [Cahge `isReadOnly` from method to read-only property passed in the constructor](#4341 (comment)) (~not passed in the constructor yet~ done!, and can't put it to read only mode due to [this](https://github.com/hedyorg/hedy/blob/345dc3b26e66229483d863702b4d20a1212a489c/static/js/app.ts#L1629) and [this](https://github.com/hedyorg/hedy/blob/345dc3b26e66229483d863702b4d20a1212a489c/static/js/app.ts#L1658) ) - [ ] [Remove `trimTrailingSpace` method? Maybe do it automatically in `getValue`](#4341 (comment)) - [x] [Remove `configureMainEditor` as part of the generic interface](#4341 (comment)) - [x] [Remove `setEditorMode`](#4341 (comment)) - [x] [Give the class a constructor and guarantee that there's always an editor.](#4341 (comment)) - [x] [The editor doesn't need to be public](#4341 (comment)) - [ ] [The markers will probably need to be merged into the ace class.](#4341 (comment)) Left for a future PR - [x] [Make the HedyEditor emit a change event itself (either by being a full-blown EventEmitter, or by making some simple callback mechanism ourselves](#4341 (comment)) **Fixes #4382** **How to test** Everything should work as expected
**Description** One of the points in #4341 was to move the markers functionality to the editor interface. This PR does that, also modifying the debugger file in accordance to the new interface. This changes will allow me to keep working on #4479, as this is a previous step to that change. I basically just copy and pasted the `Markers` class into the `HedyAceEditor` class, modifying it when needed. I also added a couple more editor events, since that was needed in order for the debugger to work. **How to test** Everything should work as previously.
Switching the keyword language produced "converting circular JSON" error message. The reason was that we were trying to post the entire editor object as JSON to the server, instead of just the code from the editor (bug introduced in the following refactoring #4341).
Switching the keyword language produced "converting circular JSON" error message. The reason was that we were trying to post the entire editor object as JSON to the server, instead of just the code from the editor (bug introduced in the following refactoring #4341). **How to test** Log in, set your language to non-English and your preferred keyword language to non-English. Then use the language switcher on a code page.
Description
This is the first step towards replacing the Ace editor with CodeMirror, at least for languages that don't use the latin alphabet. This PR will build an interface that must be followed by whatever implementation we decide to use.
fixes #4339
How to test
Everything should work the same once everything is done