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

Editor cleanup #4363

Closed
Closed

Conversation

matthijskooijman
Copy link
Collaborator

This pullrequest contains a large number of cleanups and code refactorings concerning the editor part of the IDE, which takes care of loading files, showing an editor and tabs, saving files, modifying files, etc.

The original goal of this PR was to use separate RSyntaxTextArea instances for all tabs, preventing some issues resulting from sharing a single instance, and cleaning up the code around this change. The second goal was to clean up the sketch classes (e.g. Sketch, SketchData, SketchCode and SketchCodeDocument, of which it was not really clear what purpose each served). Now, there is SketchController, Sketch and SketchFile, with better defined purposes.

The commits in this PR jump around the code base a bit, since often one cleanup either required another one to happen before, or allowed one after. Also, some commits are simply small cleanups of things I came across while going through the code. It might be possible to reorder the commits to group related commits, but there are a lot of dependencies between them (both conceptual or just textual), which would make this take a lot of time, which does not seem worth the investment.

Some commits do not seem to make much of an improvement in themselves (or even introduce new ugly code), but these usually prepare for a later cleanup (which often removes the ugly code again).

This PR should mostly leave behavior unchanged, only changing the code. In some cases there are some corner cases which are handled better or differently, and some small things have improved as result of some cleanups as well.

A large part of this code could be further improved and decoupled by applying the Observable pattern, or some other event handling mechanism (e.g. to let the EditorHeader get notified when a file gets renamed, so it can update, instead of having the rename code explicitely refresh the EditorHeader). This is something for a future PR, though, since I haven't yet figured out what, if any, framework would best for doing this easily.

The first two commits of this PR come from #4358, so use that PR for comments about these commits.

@lmihalkovic
Copy link

It is sad to see some of the best things in the original codebase going away without even having been noticed for how useful they were. To be clear, many of the improvements i made, at times with minimal amount of work, would become the bigger efforts that some of the core team mentionned in previous comments. The positive news is that you will no longer have to deal with my comments as this codebase is no longer worth my time.

@matthijskooijman
Copy link
Collaborator Author

I just got the test suite working, which showed a few tests were failing due to a problem with the caret position saving and undo menu items. I added two commits to fix that, which I'll later merge into the appropriate earlier commits (but leaving them separate is easier to review).

@PaulStoffregen
Copy link
Contributor

@lmihalkovic - You speak of "many of the improvements i made", but all your public activity appears to only rhetoric, without any real code contributions.

@Chris--A
Copy link
Contributor

Chris--A commented Jan 1, 2016

@PaulStoffregen, that is a job for other people, apparently
http://forum.arduino.cc/index.php?topic=366459.msg2531345#msg2531345

@lmihalkovic
Copy link

@PaulStoffregen regarding the debugger you are writting, the impact these changes will have on your work (once you've evaluated it), I thought you'd be one of the people understanding (#4010)

Eventually, I will probably be the guy who implements this.....

But don't let that slow or discourage you. If you want it, this is open source, so 
you can do it. Only a small matter of programming is required!

@Chris--A I pasted the exact working solution to the folding issue from my codebase (very different from yours) and even explained what should not need any explanations. Clearly one of you can write the handfull of lines of code to adapt it to your codebase

@matthijskooijman it is only in the software industry that people are denied an opinion unless they are willing to redo the work. Personally when someone tells me I am clearly not considering the full impact of what i am doing, I have a critical look at my work, not the messenger. This patch closes the doors it closes regardless of who makes the evaluation, and I live comfortably with the knowledge that for the issues I saw, other more experienced developpers will undoubtedly see more. Issues live in source code, not in the eyes of the beholder

@vicnevicne
Copy link

@matthijskooijman The changes you propose seem great, but their number looks massive to me... What would be the best way to go forward ? My contribution might not be of very high value as I'm new to the IDE code, but I could review them all one-by-one, learning along the way. Would it help ? If not, how can I (how can we) help ?

@lmihalkovic
Copy link

Some perspective might be useful.

In june 2015 #3441 was added to this repo regarding the editor loosing the state of foldings when switching around between tabs. An assessment was made by a member of the core team that:

That will take a while to get solved. The root cause is that we use the same editor for every tab, replacing its content every time your switch one. This resets its previous settings, like folded code blocks.
This is a legacy behaviour, which forced us event to code a customized version of the new editor (RSyntaxTextArea)
Solution is to stop recycling the editor and create new ones for each tab. Once done, this issue should auto solve

For the better part of 6 months this remained the only voiced opinion on the topic and nothing further happened.

Reading the comment I thought it was clearly the wrong assessment, which was also missing an obvious opportunity. To pre-empt another of @PaulStoffregen's rethoric only comments, I pasted my code as-is, even adding a redundant explanation to further reduce the efforts for anyone adapting my solution to this codebase.

#3441 is a small example of what I regularly refer to as low hanging fruits. Regardless of personal opinions, it might prompt at least one person to start thinking that maybe even just out of sheer luck, what I say about the existence of many such opportunities to drastically improve this application at low cost might have some degree of truth, that some of them might actually be as trivial as #3441 is, and that I might even have a reasonable enough sense of software development to actually be meaningfully inviting you to consider some of the further consequences of this patch... regardless of when I publish my own fork.

@vicnevicne
Copy link

@lmihalkovic I read you, Laurent, and I can understand your frustration that it took a long time to fix a bug you proposed a solution for. But the fact is that we now have a proposed version that fixes that bug (agreed, not using your code but as part of a more massive refactoring), so I think the only constructive move is to help to have it merged into the tree. If there is a good reason for not merging it (regression), let's find it and fix it. What best way to start a new year ? :-). KR. Vicne

@matthijskooijman
Copy link
Collaborator Author

@vicnevicne, review of these commits would certainly be helpful. Even if you're not familiar with the code, just flagging things that seem strange or wrong to you is fine (easiest is to just comment on the commits themselves). Some things might look wrong, but will be fixed later, but don't let that stop you from flagging things, it's probably counter-productive to read through all commits before commenting :-)

@lmihalkovic as for the fix you proposed, I'll add a coment to #3441 about that, to keep the discussion about that in that issue.

@lmihalkovic
Copy link

@vicnevicne i appreciate your comment and the effort it reflects to try to touch on these issues.
The code I shared in #3441 was never directly usable because the editor it applies to in my currently private fork has diverged from the official arduino editor (cleaning the original editor code was required to support ASM, markdown, images, libraries.properties, keyword.txt, single-compressed-file sketch, and asciidoc). I only pasted my working code to avoid a long debate and show how trivial the solution to the code folding problem is and always was, when compared to the long explanation shared by the core team. My second goal in referencing #3441 was to highlight that there are more cases like that in the codebase where people view complications that are in IMHO not there.
As for my position on this proposed series of changes, IMHO it currently contains more harm than good as it eliminates, seemingly without acknowledging their strength or IMHO offering any improvement, some of what I saw as strong points in the original codebase and which offer flexible points of enhancements. I shared this view as an invitation to consider any deep refactoring in the broader context of where this application is going, possibly with the involvement of @mbanzi, @damellis @cmaglie and the rest of the core team's vision for both Arduino products, as opposed to conducting it as an isolated musical chairs exercise where code will randomly drop when the music stops.

@cmaglie
Copy link
Member

cmaglie commented Jan 2, 2016

As for my position on this proposed series of changes, IMHO it currently contains more harm than good as it eliminates, seemingly without acknowledging their strength or IMHO offering any improvement, some of what I saw as strong points in the original codebase and which offer flexible points of enhancements.

@lmihalkovic
can you be more specific on that? what are the srtong points on the original codebase that are being removed and which enhancements they offer?

@lmihalkovic
Copy link

IMHO, everything starts with the vision Arduino LLC has for its software products. You have an existing multi-year investment in IDE and a growing stake in Create. If the immediate future is to let IDE go into sunset mode and let the feature gap grow, then nothing can be wrong about any changes and this is as good a patch as any. If on the other hand there are some features that Arduino LLC would like to schedule as complements or stepping stones or even vendor locking into Create, then any big refactoring should be evaluated in terms of how much closer it gets you to these goals, rather than how easy or trend compliant it is.
Without presuming anything about where Arduino LLC wants to go, you might consider that without any drastic alterations, the existing split of responsibilities between SketchCode, SCodeDoc, Sketch, STArea, EditorHeader, EditorConsole and Editor (and some cleanup of the existing boundaries) allowed support for adding/editing local libs, viewing the source code for any system lib, using single file sketches, and viewing/editing most file types related to the platform, in under 2 months. The list is longer but my path is more akin to a 2.0 than a dot increment. IMHO some of the proposed redifined complings/decouplings are bound to stand in your way, in a similar fashion to how the excessive decoupling of Compiler from Editor is currently holding several issues.
That's the best summary I can offer you at this time.

@matthijskooijman
Copy link
Collaborator Author

I just added three more commits that fix up some things in previous commits. I'll make sure to merge these into the commits they fix before merging this PR, but I'll keep them separately for now for easier review.

For some toolbar buttons, when it is clicked while shift is pressed, its
function changes. When handling the click event, this information is
directly taken from KeyEvent.isShiftDown(). However, to also show the
proper tooltip *before* clicking, EditorToolbar listened to key events
on the main text area, to know when shift is (not) pressed.

This approach means that pressing shift while the text area is not
focused will not change the tooltip, and creates some unwanted coupling
between the toolbar and the text area.

This commit changes this approach to instead use the global
KeyboardFocusManager. Any key presses pass through there before being
dispatched to the currently focused component, so this makes sure that
any shift presses are caught, as well as making EditorToolbar a bit more
self-contained.
RSyntaxTextArea appears to support using a single instance and replacing
the underlying text and document when switching between tabs, but in
practice this support is not complete and even though the
RSyntaxTextArea developers did some work to improve the situation, they
recommend to just use a seperate instance for each tab.

This commit implements exactly that. A new class EditorTab is introduce
to wrap the RSyntaxTextArea and containing scroll pane, and to
encapsulate the code related to handling the text area itself. Doing so
removes some quirks and prepares for some later additions. In
particular, error highlights are now no longer shared between all tabs,
which was previously the case.

This commit mostly moves code from Editor into EditorTab, and updates
the callers to use getCurrentTab() and call methods on the result
instead of calling them on Editor. Some code is added to take care of
creating multiple EditorTab objects and switching between them. Some
small changes have been made to make the flow of opening files work,
though these are mostly a bit hacky.

While moving code, changes to the rest of the code were kept minimal,
retaining existing interfaces as much as possible. This sometimes result
in less than ideal code, which should be cleaned up in subsequent
commits.

The SketchCodeDocument class has been pretty much emptied out, since
it was mostly used to store things for tabs in the background, which are
now just stored in each RSyntaxTextArea separately. The last remaining
bits of this class can probably be moved or implemented differently
later, so it can be removed.

The entire flow of working with sketches and files needs to be cleaned
up next, so no thorough attempt at testing this commit was done. It is
likely that there are plenty of corner cases and race conditions, which
will be fixed once the reset of the code is cleaned up.

Fixes arduino#3441
It was not used anymore, and removing it makes subsequent refactoring
easier.
Previously, EditorTab set the Document on the SketchCodeDocument, and
the latter would listen for changes, only forwarding the modified status
to SketchCode. This commit cuts out a step and lets EditorTab call
SketchCode::setModified directly.

Additionally, the DocumentTextChangedListener helper class is added,
which wraps a simple (lambda) function to be called whenever anything
about the document text is modified. This hides the verbosity of having
to handle both insertion and deletion, and instead suffices with just
having a single lambda function instead.
Now that each file in the sketch has its own text area in the GUI, it is
no longer needed to store the (possibly modified) contents of each file
inside SketchCode. Keeping the contents in the text area is sufficient.
Doing so allows removing the code that dealt with copying contents from
the text area into the SketchCode instance at the right time, which was
fragile and messy.

However, when compiling a sketch, the current (modified) file contents
still should be used. To allow this, the TextStorage interface is
introduced. This is a simple interface implemented by EditorTab, that
allows the SketchCode class to query the GUI for the current contents.
By using an interface, there is no direct dependency on the GUI code. If
no TextStorage instance is attached to a SketchCode, it will just assume
that the contents are always unmodified and the contents from the file
will be used during compilation.

When not using the GUI (e.g. just compiling something from the
commandline), there is no need to load the file contents from disk at
all, the filenames just have to be passed to arduino-builder and the
compiler. So, the SketchCode constructor no longer calls its `load()`
function, leaving this to the GUI code to call when appropriate. This
also modifies the `SketchCode.load()` function to return the loaded
text, instead of storing it internally.

To still support adding new files to a sketch (whose file does not
exist on disk yet), the EditorTab constructor now allows an initial
contents to be passed in, to be used instead of loading from disk. Only
the empty string is passed for new files now, but this could also be
used for the bare minimum contents of a new sketch later (which is now
down by creating a .ino file in a temporary directory).

Another side effect of this change is that all changes to the contents
now happen through the text area, which keeps track of modifications
already. This allows removing all manual calls to `Sketch.setModified()`
(even more, the entire function is removed, making `Sketch.isModified()`
always check the modification status of the contained files).
Previously, some of the GUI code would use Editor.getSketch() to get the
current sketch, and Sketch.getCurrentCode() to find out the currently
selected tab. Since this code is really concerned with the currently
open tab in the GUI, it makes more sense to query the Editor tabs list
directly.

This removes all references the current sketch code, as tracked by
Sketch, external to Sketch itself. This prepares for removing the
current tab tracking from Sketch later.
Instead of letting Sketch (also) keep track of the currently selected
tab, this moves the responsibility to Editor instead. When Sketch need
to know the current tab and file, it now asks Editor.

Switching between tabs is still handled through Sketch methods, but that
will be cleaned up later.
This lets all code directly call `Editor.selectTab()`, or the newly
introduced `Editor.selectNextTab()` or `Editor.selectPrevTab()`. This
also adds a new `Editor.findTabIndex(String)` to look up a tab based on
the filename (what `Sketch.setCurrentCode(String)` used to do). At some
point, this method might need to be removed, but for now it allows other
code to keep working with minimal changes.
This class served no purpose anymore, so it can be removed. The
`SketchCode.getMetadata()` and `setMetaData()` methods only served to
keep track of a SketchCodeDocument instance (and were no longer used),
so these are removed too, just like some SketchCode constructors dealing
with this metadata object.
It was not used, and since it only updated the `name` attribute, but not
the corresponding `file` attribute, nor actually handled renaming actual
files, having this method around would actually be harmful, so just drop
it.
matthijskooijman and others added 18 commits January 21, 2016 08:31
This makes a few related changes:
 - `FileUtils.replaceExtension()` is introduced to handle replacing the
   .pde extension with .ino.
 - Instead of iterating .pde files on disk, this iterates SketchFiles in
   memory, saving another lookup from filename -> SketchFile later.
 - `SketchController.renameCodeToInoExtension()` is removed. Now it no
   longer needs to look up the SketchFile and FileUtils handles the
   extension replacement, this method did not have any reason to exist
   anymore.
 - Instead of hardcoding the .pde extension, a new
   Sketch.OLD_SKETCH_EXTENSIONS constant is introduced.
No need to call the File constructor ourselves, if
`File.getParentFile()` can just do that for us.
This commits replaces a significant part of the code handling these
features. A lot of responsibilities are moved from SketchController to
Sketch, though the code involved is rewritten mostly.

Most of the handling now happens inside Sketch, including various checks
against the new filename. Basically SketchController processes the user
input to decide what needs to be done, and Sketch checks if it can be
done and does it.

If problems occur, an IOException is thrown, using a translated error
message that is shown by SketchController as-is. This might not be the
best way to transfer error messages (regular IOExceptions might contain
less-friendly messages), so this might need further improvement later.

In addition to moving around code and responsibilities, this code also
changes behaviour in some places:
 - Because Sketch and SketchFile are now in control of renames and
   saves, they can update their internal state after a rename. This
   removes the need for reloading the entire sketch after a rename or
   save as and allows `Editor.handleOpenUnchecked()` to be removed.
 - When renaming the entire sketch, all files used to be saved before
   renaming, since the sketch would be re-opened after renaming. Since
   the re-opening no longer happens, there is no longer a need to save
   the sketch, so any unsaved changes remain unsaved in the editor after
   renaming the sketch.
 - When renaming or adding new files, duplicate filenames are detected.
   Initially, this happened case sensitively, but it was later changed to
   use case insensitive matching to prevent problems on Windows (where
   filenames cannot differ in just case). To prevent complexity, this
   did not distinguish between systems. In commit 5fbf962 (Sketch
   rename: allowig a case change rename if NOT on windows), the
   intention was to only do case insensitive checking on Windows, but it
   effectively disabled all checking on other systems, making the check
   not catch duplicate filenames at all.

   With this commit, all these checks are done using `File.equals()`
   instead of comparing strings, which is already aware of the case
   sensitivity of the platform and should act accordingly.
 - Some error messages were changed.
 - When adding a file, an empty file is not created directly, but only a
   SketchFile and EditorTab is added. When the sketch is saved, the file
   is created.
 - When importing a file that already exists (thus overwriting it),
   instead of replacing the SketchFile instance, this just lets the
   EditorTab reload its contents. This was broken since the introduction
   of EditorTab. The file would be replaced, but not this was not
   reflected in the editor, which is now fixed. This change allows
   `Sketch.replaceFile()` to be removed.
 - When importing a file that does not exist yet (thus adding it), a tab
   is now also added for it (in addition to a SketchFile). This was
   broken since the introduction of EditorTab, and would result in the
   file being added, but not shown in the editor.

This commit adds a `Sketch.renameFileTo()` method, to rename a single
file within the sketch. It would be better to integrate its contents
into `Sketch.renameTo()`, but that does not have access to the `Sketch`
instance it is contained in. This will be changed in a future commit.
These methods shouldn't really be in Base (or BaseNoGui, which did the
actual work), especially since there is already a
`FileUtils.recursiveDelete()` which just does the same thing. This
commit removes the code from Base and BaseNoGui and instead uses the
method from FileUtils.

There is one difference between these methods: the Base methods did not
delete files if the "compiler.save_build_files" preference was set.
However, the Base methods were only used when deleting a sketch, or
deleting an existing folder before overwriting it on save as, so this
preference didn't actually do what it was supposed to anyway, so
dropping it shouldn't be a problem.
This allows simplifying some other things later.
Now that SketchFile keeps a reference to its Sketch,
`SketchFile.renameTo()` can call `Sketch.checkNewFilename()`, so there
is no need for the renaming itself to go through Sketch.

This changes the parameter for `SketchFile.renameTo()` from File to
String, to enforce that only the filename is changed, not the directory
name.
This isn't much code, but it makes deletion more consistent with
renaming and saving with the SketchController handling the UI part and
Sketch actually doing the delete.
The extra "File" in the name was a bit redundant, and this is more
consistent with `Sketch.delete()`.
Previously, callers of `SketchFile.delete()` would also call
`Sketch.removeFile()`, but letting SketchFile handle this is more
robust.

This is possible now that SketchFile keeps a reference to Sketch and
makes updating the Sketch file list less fragile.

Eventually this might be further decoupled by letting SketchFile
broadcast a "deleted" event instead.
Previously, the caller of the constructor did this, but now that
SketchFile keeps a reference to its containing sketch, it can figure it
out itself.
Previously, everywhere where it was needed, the path was requested from
BaseNoGui. Because the path is based on a hash of the sketch filename,
every caller would get the same path for the same sketch.

However, it makes more sense to store the path used for a given sketch
inside the Sketch object. This prevents having to pass around or
regenerate the build path everywhere, and no longer requires the build
path to be deterministic (though it still is in this commit).

This allows removing some methods and constructors of which two versions
were available - one with a build path argument and one without.
Previously, this used a hash of the sketch filename, so the same build
path would be generated for the same sketch between multiple
compilations. Now that the build path is stored, this requirement has
disappeared, so a random filename can be generated again. While here,
this commit also changes the prefix from "build" to "arduino_build_",
which makes it a bit more clear what the directory's purpose is.
Comparing a File object automatically takes care of filesystem case
sensitivity, whereas strings do not, so this makes the comparison
slightly more reliable.
This comment still talked about Processing related stuff which doesn't
really apply anymore.
Turns out the approached used to keep caret and scroll position only
keeps the scroll position. More code is needed to also keep caret
position.
Turns out I dropped a setUndoManager call somewhere, as well typed Redo
instead of Undo somewhere.
@matthijskooijman
Copy link
Collaborator Author

I rebased this PR on top of current master, now that #4358 is merged. This also ran into a small conflict with #4444 which was easy to fix. No meaningful code changes were made to any of the commits.

@facchinm
Copy link
Member

Rebased and merged

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.

8 participants