Skip to content

mabo3n/csharpto.el

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

csharpto.el

csharp t ext o bjects

This emacs extension aims to provide useful Evil text objects to facilitate the manipulation of C# code. Currently, the following are the available ones:

  • af csharpto-a-function: from first to last character of current function
  • aF csharpto-a-FUNCTION: lines spamming current function + surrounding blank lines
  • if csharpto-i-function: from first to last character of current function’s body
  • iF csharpto-i-FUNCTION: lines spamming current function’s body + spaces until ”{ }
  • as csharpto-a-scope: from first to last character of current statement with a scope
  • aS csharpto-a-SCOPE: lines spamming current statement with a scope + surrounding blank lines
  • is csharpto-i-scope: from first to last character of current statement’s scope
  • iS csharpto-i-SCOPE: lines spamming current statement’s scope + spaces until ”{ }

Behavior

  • Support for both regular { } and expression-bodied => syntax ✔
  • Support for Allman and K&R indentation styles ✔
  • Support for comments // /* */ and [Attributes]

The adopted convention is that i/a stands for inner/outer content and OBJECT (upper-cased) stands for object with surrounding blank lines/spaces.

The csharpto-a-FUNCTION and csharpto-a-SCOPE in particular behave similarly to evil-a-paragraph, in which they include the surrounding blank lines. This makes it easy to strip them from source code without messing up with what is left.

Scope text objects are still an experimental feature (untested), but they are built over function’s and should work fine.

See Limitations and Why sections below for more details.

Usage

  1. Clone this repository in you machine, and make sure it is in your Load Path.
  2. Load it (require csharpto) somewhere in your init files.
  3. Customize the variable csharpto-default-bindings-alist if you’re not comfortable with the default keybindings (note that scope text objects are bound to s/S, hiding the existent sentence text objects).
  4. To test out the text objects in a buffer, open it and call csharpto-bind-keys-locally. To set the keybindings globally for all buffers, call csharpto-bind-keys-globally. To set the keybindings dynamically only for csharp-mode, call csharpto-use-default-bindings-in-csharp-mode.

Limitations

This extension is still in early development. The text objects don’t support a count argument and they don’t take current selection (region) into account (yet).

The implementation is backed by a custom, heuristics-based approach using regular expressions, which doesn’t make use of Evil utilities and is not integrated with thingatpt.el (let alone that it became complicated and needs a clean up). It relies heavily on blank lines and proper indentation to work seamlessly, so with "badly" formatted code it won’t work out of the box.

It should work in most cases given:

  • Functions are separated by [at least one] blank lines;
  • There’s no blank lines within a function signature, nor anywhere inside an expression-bodied function;
  • There’s no fields/properties between functions;
  • There’s no weird indentation and comments.

Why

It is useful (and efficient) to be able to operate over a function while editing programming files, as it is with words, sentences, paragraphs etc. while editing text.

This extension was created to make it easier to operate on C# functions, taking into account the idiosyncrasies of the language.

C# allows declaring functions with the standard C-like syntax using curly brackets { }, and also also as expressions => (the so called expression-bodied methods). Plus, it is common to find both Allman and K&R indentation styles in C# codebases. For example:

class SomeClass {
    
    public Function1(string name)
    {
        Id = Guid.NewGuid();
        logs = new List<LogEntry>() { };
        Name = name;
    }
    
    public int Function2(int a, int b) {
        a++;
    
        b++;
    
        return a + b;
    
    }
    
    int Function3() => 1 + 2 + 3 + 4 + 5;
    
    void Function4(
        DateTime timestamp, LogLevel? level = default
    )
        => throw new NotImplementedException();
    
    
    public IEnumerable<char> Function5() =>
        this.GetHashCode()
            .ToString();
}

We can easily recognize 5 declared functions above, but is not straightforward to refer to them with our fingers. In some cases it is possible to get around this with existing text objects.

For example, usually there are no empty lines within expression-bodied function declarations, so you can refer to them with the standard paragraph text objects (evil-a-paragraph if you want the accompaining blank lines and evil-inner-paragraph if you don’t). But that won’t work if the function is the first/last/only one in the class.

If you only have bracketed functions with both the signature and the { spanning a single line (like Function2 in the previous example), you can select them with the evil-indent-plus-i-indent-up-down text object from evil-indent-plus. But for that to work the cursor must be inside the function (body), and also not under an empty line, otherwise the operand will be the whole surrounding class.

But if a function’s signature spans multiple lines, or there’s a line break before opening its scope, or even it has attributes or comments tied to it, there’s no easy way to refer to the whole function even though you call it a “function” or “method”.

Well, actually now there is! Take the example below, to delete the first function from the class, instead of trying to hack your way through visual mode (e.g. viJjokkd with evil-indent-plus or V3ko9jd with relative line numbers), you can just press daF (or any other keybinding you chose) to delete a csharpto-a-FUNCTION:

namespace Tests
{
    public class PersonTests
    {
        [Fact(Skip = "Fixed on b38a7b16")]
        public void ChangeName_ShouldChangeName()
        {
            // Cursor is here:█
            var oldName = "Mario";
            var person = new Person(oldName, Guid.NewGuid());
    
            var newName = "Paul";
            person.ChangeName(newName);
    
            person.Name.Should().Be(newName);
        }
    
        [Theory]
        [InlineDataAttribute(null)]
        [InlineDataAttribute("")]
        [InlineDataAttribute(
            " "
        )]
        public void Constructor_ShouldThrowArgumentException_WhenNameIsEmpty(
            string name
        )
            => Record.Exception(
                () => new Origin(name, Guid.NewGuid())
            )
                .Should()
                .NotBeNull()
                .And
                .BeOfType<ArgumentException>();
    }
}

Within the other (second) function, despite the unorthodox syntax, it works the same way.

More examples of supported syntaxes can be found in these fixture files.

About

C# text objects for Evil (emacs)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published