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

Lots of new goodies from refactoring branch #127

Merged
merged 36 commits into from
Mar 13, 2016
Merged

Lots of new goodies from refactoring branch #127

merged 36 commits into from
Mar 13, 2016

Conversation

richrd
Copy link
Owner

@richrd richrd commented Mar 3, 2016

I somehow went into full rampage mode and began major refactoring. :shipit:

Current changes:

  • Implemented new input prompt based on Editor. Fixes Input promts don't support special (multi byte) characters #95
  • Better string formatting (more use of format).
  • Refactored keymap to separate file with Sublime Text syntax.
  • Mapped Ctrl+Z and Ctrl+Y to undo / redo.
  • Implemented tab indicators
  • Improved running modules from key bindings
  • Added prompt for creating missing dirs during save_as
  • Fixed logging on Python 2.6 and disabled unicode symbols on Python 2.X
  • Restructured config command, added keymap command.
  • Began refactoring better separation for Viewer and Editor
  • Added more scopes to lexer.
  • Dark status bars by default.
  • Some performance optimization.

@richrd richrd self-assigned this Mar 3, 2016
@@ -182,14 +190,14 @@ def main_loop(self):

if event:
got_input = True
self.on_input(event)
self.on_input(event) # Up to 30% processing time
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment doesn't make much sense. What's the context?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Oops, didn't clean that up yet. I've been doing some performance analysis and finding out where the most time is consumed. The 30% indicates what proportion of the main loops execution time is spent on on_input.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, k. It might be wise to add a PERF: prefix to the comment so it can easily be found via git grep during cleanup

"home": self.home, # Home
"end": self.end, # End
"find": self.find_query # Ctrl + F
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This mapping seems pretty redundant. It's good to have some level of whitelisting to prevent exploits but here are some other options:

  • A decorator which flags a function as an operation so it can be added to this list on __init__
  • A class inside of BaseViewer which has the operations as methods and receives BaseViewer as a parameter on __init__ (so its calls would be self.base_viewer.cursors)

Copy link
Owner Author

Choose a reason for hiding this comment

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

I feel that the decorator might be a good choice for now although I'm ok with the current solution too. However in some cases the operation name might be different from the method name (like find). The current mapping is used also for the Ctrl+E run command prompt. I think it's handy to have the operation names stored as strings.

How would you incorporate the decorator into the class? I haven't used decorators much. This is related but didn't really solve my problem (specifically adding operations on __init__) http://stackoverflow.com/questions/1263451/python-decorators-in-classes ?

Copy link
Contributor

Choose a reason for hiding this comment

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

The purpose of the abstraction would be to reduce the maintenance and make the functions/commands consistent (e.g. find -> find, no renaming). Otherwise, every time you add a new operation, you will be adding a new key. This isn't a lot to start but it's overwhelming when there's 50 keys.

Upon further consideration, I think an Operation class is probably easier to manage since it can be placed in a separate file.

That being said, the decorator implementation would look like (I realized that saving to a dict is much saner than marking/searching functions):

operations = {}

def operation(fn):
    """Save our function as an operation"""
    operations[fn.__name__] = fn

class Viewer(object):
    def run_operation(self, operation):
      # Simplified for example
      return operations[operation](self)

    @operation
    def arrow_right(self):
      pass

For renaming, we would define a proxy operation:

operations = {}

def operation(name):
    """Save our function as an operation"""
    def operation_proxy(fn):
      operations[name] = fn
    return operation_proxy

class Viewer(object):
    def run_operation(self, operation):
      # Simplified for example
      return operations[operation](self)

    @operation("arrow_right")
    def arrow_right(self):
      pass

Copy link
Contributor

Choose a reason for hiding this comment

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

But for an operations class, it's much cleaner/saner:

class Operations(object):
    # Mark non-operations
    forbidden_operations = ('__init__', '__contains__')

    def __init__(self, viewer):
        # Save viewer or whatever relevant info we want (e.g. window)
        self.viewer = viewer

    def __contains__(self, key):
        # Verify the operation exists
        return hasattr(self, key) and key not in self.forbidden_operations

    def arrow_right(self):
        pass

def Viewer(object):
  def __init__(self):
      # Create an operations set for this viewer
      self.operations = Operations(self)

  def run_operation(self):
    if operation in self.operations:
      self.operations.arrow_right()

Copy link
Owner Author

Choose a reason for hiding this comment

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

This does seem more robust so I'll transition to the Operations class when I have time. Thanks for the good examples to get started with! One question though, how does run_operation know which method to call?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, sorry. I missed a parameter for run_operation in the example. It would receive an operation parameter as it does now in the code:

suplemon/suplemon/viewer.py

Lines 637 to 646 in 819fe6e

def run_operation(self, operation):
"""Run an editor core operation."""
if operation in self.operations:
cancel = self.app.trigger_event_before(operation)
if cancel:
return False
result = self.operations[operation]()
self.app.trigger_event_after(operation)
return result
return False

  def run_operation(self, operation):
    # Simplified for example
    if operation in self.operations:
      self.operations.arrow_right()

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I see what you were asking. It was with respect to arrow_right. We can use getattr for that. It will retrieve the instance bound function for us to run:

  def run_operation(self, operation):
    # Simplified for example
    if operation in self.operations:
      operation_fn = getattr(self.operations, operation)
      operation_fn()

Copy link
Owner Author

Choose a reason for hiding this comment

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

Oh yeah, ofcourse we have getattr. Looks nice and straightforward, and I like the idea of a separate file for operations. A third option I've been considering is moving all operations to dedicated modules, any thoughts on that? I'm not quite sure if its a good idea or worth it.

For now though the Operations class feels like the way to go. I'll shedule it for my next round of refactoring.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think breaking out operations to their own files is reasonable. However, it's mostly useful when the contents of an operation are overwhelming for a developer. I think they are pretty bite-sized for now so a class works.

My preferred approach for something like that is usually a hybrid; a core class which has lots of small/simple operations with imported external functions for more complex ones:

from suplemon.operations.super_complex import super_complex

class Operations(object):
    def arrow_right:
        # ...

    @classmethod
    def add_operation(cls, name, fn):
      """Helper to attach a new operation to our operations class""
      cls.name = fn

# Add external operations
Operations.add_operation('super_complex', super_complex)

"""Move cursors right."""
for cursor in self.cursors:
line = self.lines[cursor.y]
if cursor.y != len(self.lines)-1 and (cursor.x >= len(line) or len(line) == 0):
Copy link
Contributor

Choose a reason for hiding this comment

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

While the logic will work, shouldn't we check EOL before checking the next line exists? (e.g. if we are going to scroll to the next line, then verify there is a next line)

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact, we can prob move the cursor.y check inside of this if block since we don't want an else scenario. Then, we can prob convert the elif to an else

Copy link
Contributor

Choose a reason for hiding this comment

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

line = self.lines[cursor.y]
# If we are at the end of the line
if cursor.x >= len(line) or len(line) == 0:
    # If there is another line, then move down
    if cursor.y != len(self.lines)-1
        cursor.move_down()
        cursor.set_x(0)
# Otherwise, move the cursor right
else:
    cursor.move_right()

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thanks, that does look better. These were implemented long ago when I just needed to get things working, and didn't pay as much attention to code style as I should have.

cursor.move_up()
cursor.set_x(len(self.lines[cursor.y])+1)
self.move_cursors((-1, 0))
self.scroll_up()
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking about cursors more... I think move_cursors is a good start but maybe rename it to move_cursors_relative and let that compute absolute positions. Then define a move_cursors_absolute.

move_cursors_absolute will be practical for performance so kudos on that.

For high level user interfaces though, we will want to handle lots of different cases (e.g. moving by 2 characters, 3 characters, 1 word, 1 line). So it's something to think about (e.g. reconsider arrow_left/arrow_right as more move_by_character(forward=False)/move_by_character(forward=True))

https://github.com/twolfson/sublime-files/blob/3103.0.0/Packages/Default/Default%20%28Linux%29.sublime-keymap#L51-L85

Copy link
Owner Author

Choose a reason for hiding this comment

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

I must admit that the sublime way looks very nice and that's how I'd like it to work in suplemon too. This requres some more refactoring in many places so I'll postpone this for later. I'll try to finalize and :shipit: the bug fixes and new stuff in this PR first.

Thanks for the suggestion, I'll definitely come back to this when I have time.

@landscape-bot
Copy link

Code Health
Repository health decreased by 0.09% when pulling 7664ca6 on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health decreased by 0.58% when pulling 3a62ecd on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health increased by 0.21% when pulling 1f97386 on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health increased by 0.01% when pulling eb44235 on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health increased by 0.41% when pulling 7a13894 on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health increased by 0.41% when pulling 819fe6e on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health increased by 0.41% when pulling 3ebe6e3 on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health increased by 0.08% when pulling 3a17e57 on refactoring into ef5eff2 on dev.

@landscape-bot
Copy link

Code Health
Repository health increased by 0.08% when pulling 1ef4cde on refactoring into ef5eff2 on dev.

@richrd
Copy link
Owner Author

richrd commented Mar 13, 2016

I'll merge now so I can release the new input prompt and other improvements. I'll continue with more refactoring later.

richrd added a commit that referenced this pull request Mar 13, 2016
Lots of new goodies from refactoring branch
@richrd richrd merged commit e5db54c into dev Mar 13, 2016
@richrd richrd deleted the refactoring branch March 13, 2016 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants