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 functionaF
csharpto-a-FUNCTION
: lines spamming current function + surrounding blank linesif
csharpto-i-function
: from first to last character of current function’s bodyiF
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 scopeaS
csharpto-a-SCOPE
: lines spamming current statement with a scope + surrounding blank linesis
csharpto-i-scope
: from first to last character of current statement’s scopeiS
csharpto-i-SCOPE
: lines spamming current statement’s scope + spaces until ”{ }
”
- 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.
- Clone this repository in you machine, and make sure it is in your Load Path.
- Load it
(require csharpto)
somewhere in your init files. - 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). - 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, callcsharpto-bind-keys-globally
. To set the keybindings dynamically only forcsharp-mode
, callcsharpto-use-default-bindings-in-csharp-mode
.
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.
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.