Elastic TabStops: A Better Way to Indent and Align Code (2017)
nickgravgaard.com
nickgravgaard.com
My conviction from personal experience is that your personal tools need to be adaptable to fit any organization. This splits largely along tech stack culture lines. Our shop was originally Ruby before we started moving to NodeJS, we all use spaces for indentation.
So unless I want to force my whole team to start using tabs, I can't use elastic tabstops.
This isn't a viable improvement until it can accommodate the existing state of affairs. The other obstacle in the way is code linters. I have to make a case to turn off linter rules at my shop and if the only objection I have is that it makes a workflow I haven't even tried yet difficult to implement, it's not going to be taken seriously if it requires work from the whole team.
The way I usually accomplish workflow improvement objectives is to find a way to accommodate both my needs and the status quo while thinking deeply about the underlying context creating the obstacle.
Environment managers get gnarly when you have to commit .env files to the codebase. Every time someone changes the .env file and commits, on pull the expected behavior of the system changes. Not having a .env file committed to the codebase caused issues with the insufficient handling of environment by our CI process.
So I use .env-local for overriding the environment and one line in the codebase overrides the .env file in case .env-local exists. That way I don't have to care about what's in .env and I won't get surprising behavior upon pulling the repo.
One year later I got around to eliminating the 12 factor violation. One month after that we decided to replatform. -_-
Installing "Tabs for indentation, spaces for alignment" on wetware is probably the best solution in terms of maximizing utility for minimized development/workflow costs. But even that is going present a wetware tax for some developers, and it's not superior in terms of utility to the article's proposal.
But installing ideas onto wetware is perhaps the most expensive thing that can ever be considered. Because it often involves fighting a war.
int main() {
printf("%s, %s, %d",
somevariable,
somefunction(),
42);
}
Because beyond that I personally (and very subjectively) consider that adding comments on the same line as the code is usually a bad idea when you can put them on the previous line and it's both better suited to long comments and easier to match the comment with the line. In the example from the gif in the article consider the distance between the `x();` code and the matching comment all the way to the right. Putting it on the line before would make more sense IMO. Most decent code editors will also be able to wrap the comment more easily that way. That being said I'm anal about enforcing 80 columns in my code so maybe I'm just a weirdo.Elastic tabstops are great for making table like code, not just for aligning comments. But for me, the cons outweighs the pros. The biggest problem is that it requires a specific interpretation of the tab character that is uncommon and nonstandard.
⇥printf(⇥"%s, %s, %d",
⇥ ⇥somevariable,
⇥ ⇥somefunction(),
⇥ ⇥42);
Doesn't look very pretty and wastes one space. int main() {
printf(
"%s, %s, %d",
somevariable,
somefunction(),
42
);
} int someLongFunctionNameThatWillBeRefactored(int foo,
int bar);
someLongFunctionNameThatWillBeRefactored(longVariableName1,
longVariableName2);
and do int someLongFunctionNameThatWillBeRefactored(
int foo,
int bar);
someLongFunctionNameThatWillBeRefactored(
longVariableName1,
longVariableName2);
or even int someLongFunctionNameThatWillBeRefactored(
int foo, int bar);
Now tabs work fine. Spaces work fine. Elastic TabStops? Ironic that he mentions "Keep it simple, stupid!" and then overengineers this thing and every editor along the way.Anyway, this will probably not go down well since everybody has been trained to use the former style, and now people "prefer" it, out of what seems to me to be habit.
I do note that even your proposition doesn't really solve the problem of using Elastic TabStops, because if you imagine a comment spanning all three lines:
someLongFunctionCall(⇥ /* This function call */
⇥ longVariableName1,⇥ /* is really cool *
⇥ longVariableName2);⇥ /* and stuff */
You have an additional tab at the beginning of the two last lines which means (as far as I understand it) that the first comment line won't necessarily line up with the two others. Of course that showcases a potential issue with Elastic TabStops, not your particular coding style. /* This function call is really cool and stuff */
someLongFunctionNameThatWillBeRefactored(
longVariableName1,
longVariableName2);
So I sort of dismissed that use-case as a toy example. But yes, if you did that elastic tabstops would be useful... except the contents of the block comment doesn't get reflowed when the tabstop width changes. Which I'm sure we could do, but then we're getting into interesting territory w.r.t edge cases/usability for sure.Python is especially bad for me here, despite having used it everyday for years, because "if" and "while" and etc don't even have those closing markers.
Beats trying to keep weird alignment with the method names and makes param lists more consistent with other open/close bracket situations.
http://nickgravgaard.com/elastic-tabstops/images/columnblock...
The red section in that GIF is analogous to your example.
-rw-r--r--@ 1 x staff 538584 Jul 30 17:36 columnblocks_coloured.gif
-rw-r--r-- 1 x staff 282676 Jul 30 17:42 columnblocks_coloured_opt.gif
-rw-r--r--@ 1 x staff 131143 Jul 30 17:36 logo.gif
-rw-r--r-- 1 x staff 92369 Jul 30 17:42 logo_opt.gifA simple, if inelegant, way to achieve this: Use a standardised format on save. On read, pretty-print based on user-defined settings.
What line splitting issues are you referring to? I've been pretty happy with the approach used by Prettier (and I believe Black) of fitting it on one line if possible, and doing one per line otherwise. I'd say that's my favorite part of Prettier: I don't need to think about wrapping anymore, it figures it out, so it's sad to see that gofmt doesn't do that (from my testing just now).
> but the same principle of code -> AST -> code seems just as valid
The question is really the reversibility of the transform in a reasonable way. If you are translating all your source to an AST there is technically no reason multiple programmers even need to be working in the same language. However in practice, even within a single language, there are many possible transforms from an AST back into code. Some will make sense, some will be nearly indecipherable. In practice you either limit the allowed transforms such that there is little benefit over a simple linting tool or the problem explodes in complexity.
Store the bytecode, indents and dedents are present as <indent> and <dedent> token which are no different than e.g. <brace, open> and <brace, closed>.
Well technically those are in the tokeniser, the AST or bytecode would reify the structures they delimit, and if you stored the AST or bytecode the separator would be reified from there.
But the point is that for the machine indentation is no different than any other token (in fact Haskell is defined using braces and semi-colons, the common indentation-based version is an augmentation of the base language which semantically just inserts braces and semicolons)
The point is that the code on disk needs to work when it's compiled. That's all. You could write an editor that displays the code in a circle so long as it writes properly indented code when you save. Nothing about the IDE is actually important.
I find that Rust deals with that mostly well, the doc format is part of the language, the documentation generation part of the toolchain. You can even validate example code from the comments. You can enable warnings for missing documentation on methods. In my experience documentation for Rust crate is generally at least okay-ish, it generally saves me a trip into the code itself. That being said Rust prototypes are also a lot more expressive than their C counterparts (no questions about "should I free that pointer?") so that probably helps as well.
I guess it does add a little bit of friction but if somebody tells me they don't write docstrings because they don't want to bother with the Markdown syntax I'd reply I'm going to assume that they're just looking for excuses at this point.
That said, you could address that and other issues, you're just working with something several steps removed from an AST. Or you could call an AST the 90% solution.
This looks "trivial" for trivial cases, but I can imagine lots of difficult corner cases there. Depending on how you define what constitutes a "rename", this may even be undecidable.
No need to put that sort of thing in core.
(For what it is worth, I toyed with what I think to be a useful compromise to the idea, which is to use syntax highlighting tokenizers, which perform well and interoperate well with character-based diffs: https://github.com/WorldMaker/tokdiff)
If it doesn't have a valid AST, then it's not a valid program, either. If you're using a formatting program (like prettier or gofmt) and pass it an invalid program, you're either going to get an error, or undefined behavior. And anyone else opening it in a different editor is going to have their language-mode interpret the invalid program differently than yours, too. Source code tools like structured search won't work predictably, either.
This sounds to me like saying "An XML structure editor? But what if I want to put an invalid XML structure in a file called foo.xml and commit it to the repository?" Or my first boss complaining that visual text editors didn't let you see every byte. Or Mel needing to know drum addresses so he could use them for constants.
As time goes by, we move to higher level abstractions, and (thanks to tools like gofmt) we're already at the point where you probably shouldn't be pushing source code which doesn't even have a valid AST, and expect all your tools to work perfectly. There's plenty of ways to write and commit WIP code without needing an invalid AST on disk.
Then I started paying attention to all the reasons why we naturally might want to check in "invalid" code.
All the cases where I'm never going to finish an entire refactor in a single commit, and the whole refactor makes far more sense in documented steps where many of the intermediate steps will never compile.
All the cases where sending broken code to a source control server at the end of the day is both the best way to back it up and the easiest way to get fresh eyeballs on it in the morning. Even with CI systems in place, sometimes the errors that the CI bot can tell you are as useful as the ones your own machine's build environment can tell you. (Especially in those weird cases where it turns out to be that maybe its the build environment on your own machine that's the problem and you've been beating yourself up over a bad install of something that should be unrelated, and the build errors on the remote machine lead you to the real problem.)
All the cases where I might build tests first, and the compiler is a test, especially in a static typed language, before working backwards to make the tests pass/run/compile.
A lot of people like to think of programming code as some purely logical construct, but good code is poetry, even in its mistakes. Sometimes we need drafts to tell our stories right. Sometimes we need bad poetry in source control as a warning to others to help them realize what good poetry can be.
It's interesting to want code editors to keep us from ever writing bad poetry to disk, but it's also somewhat inhumane. People write bad poetry all the time, it's a natural skill. Saving bad poetry for posterity is sometimes the only way we get better poetry.
Small textual changes can lead to large changes in the AST, which results in confusing diffs. Merging is also non-trivial.
On the other hand, for top level structures following the AST keeps things saner.
One of the things that I never got around to doing was exploring the higher-level opportunities, but they are there. Just as the tokenizer is the first pass in AST building, tokenized diffs should contain enough information that if you wanted to try to do higher level diff analysis you could do some interesting things.
All that remains to be implemented is the "aligned second column" feature.
The tab was added there for a reason and is flexible. Use it.
It's usually teams or projects agreeing on a coding standard, and those standards dictate everything other trivial detail. As a developer, you are an author, and you're writing with a "voice." On a team, it's a courtesy to the reader to write with the team's voice rather than your own.
Beyond that it's quite weird to make tab width configurable but nothing else. What if I prefer to have opening { on the same line as the function prototype? What if I prefer my identifiers to be in CamelCase? What if I prefer not having spaces around arithmetic operators? All that could be done relatively easily if code was stored as an abstract AST and formatted on demand.
Tab indentation is theoretically superior to spaces if used correctly but in practice it's probably not worth the trouble. At least as far as I'm concerned I gave up on tabs a long time ago.
Im more explicit than most but everyone does this to some degree. The most basic example is indenting code blocks.
Generally I find it harder to debug auto formatted code. I lose information of who the author(s) were and it makes it harder to see where breaks in ideas can happen.
Back when I was in Excel every section had its own style and unwritten rules - inevitable with 30 year software. Once the code is auto formatted all information about those unwritten rules is lost.
int func( type arg,
type arg);
Specifically, the space there after ( . Syntax-aware editors already handle this formatting situation fine.The remaining use case for this elastic business is aligning reams of comments on the right side.
The best solution solution is not to write those. They are never needed in code, but sometimes they can be beneficial in data structure declarations.
The elastic effect shown can be a achieved without tabs. The text editor simply recognizes that multiple lines have aligned comments, and maintains that with spaces. However, that still leaves the version problem/infelicity that semantic changes to one line trigger surrounding whitespace changes.
The best solution is to leave enough space for the longest declaration you would ever use. Anything longer is then split into multiple lines.
struct foo {
int magic; /* magic number */
int x, y; /* coordinates in bar space */
void (*sudden_big_decl)(struct foo *, /* Phew, That was close! */
int, int, /* Good thing we left all that room. */
double);
};One of the first things I do when starting a new project is to adopt a code formatting standard enforced by automated tools like Eclipse formatting for Java or Pep8 for Python, then move on. I really don't care what the format is as long as it's consistent and readable, hence usually let the team decide in return for adhering to the choice. And I've stopped working with teams that cannot get past obsession over tabs vs. spaces. It's a warning signal that there's an unhealthy lack of focus on things that really matter to users like clean interfaces.
Edit: typo
(actually not a fan of Elastic TabStops btw)
Having to install software or set a tab-stop correctly would be just one of those things.
My original Gedit plugin (no longer compatible with Gedit's changed API)
This seems broken, not because it's not a good idea. A lot of things that I don't use and don't want are already installed (and somewhat broken.) If I use the 'cat' command on a source code file formatted this way, how is that going to work? A new shell or new Terminal program that will set a tab-stop correctly, or a set a tab-stop correctly plug-in for bash or the Terminal program? The default eight spaces in the shell for tabs isn't very pretty, so maybe you're onto something.
As I mostly blindly click "Install", libraries that had vulnerabilities are updated or maybe newly installed. I didn't ask for these libraries, I try to remove them and the package manager reports that they are not installed. I guess I've got bigger problems to deal with, and will bump along using tabs and/or spaces. There must be a better way.
I started using spaces for indentation just because I worked with people who did that. I kind of forgot I was even doing it because vim's auto-indent did a lot of the work, but after some time I realized spaces don't make sense.
The familiar arguments for tabs:
* Clean mapping between the concept of scope depth to a single character * Configurable display of indentation width, everyone can see what they want
Tabs have problems only when people use them for alignment, which breaks when tabwidth is changed.
Actually can someone try to change my mind? I know tabs-vs-spaces is an eternal war, yet I feel the problem is completely solved, hence I must be ignorant of the counter arguments.
(The only thing more logical than using tabs might be using no whitespace at all and having the editor determine presentation. But that wouldn't work well with line-oriented tools.)
The former is a pre-formatted fixed-width character document that can only be viewed one way.
The latter is a document which is easily processed; editors should show the intended logical representation of the information according to user preferences for conveying such meanings (though for terminal compatibility they often default to either 8 or 4 fixed width spaces in less powerful editors; still the meaning is conveyed clearly).
Wait it already exists... we had the answer from a long time ago! https://en.wikipedia.org/wiki/Tab-separated_values
It even avoids the comma/semicolon issue when using CSV in excel in spanish... maybe avoids some other problems in other locales.
Having written a code auto-alignment tool, I like the idea of elastic tabstops because it makes the alignment explicit. Thinking of it as a grid and having an explicit separator between columns would be nice, it gives me control over what gets aligned. Auto alignment is tricky and always has corner cases as well as missing features, things you wish it would align but it doesn’t.
I don’t buy the argument mentioned several times here that elastic tab stops have to degrade gracefully; tabs already don’t do that. From my point of view, one of the larger downsides is it wouldn’t help with situations that call for right-align, e.g. numbers. And since formatting requires look ahead, I’m curious if the command line tool (“etst”) is enough to make command line workflows good, e.g., ssh, grep, sed, perl, etc.
This only works, obviously, with a well-behaved whitespace model: pure spaces or 8-spaced tabs.
I can see a few ways to do this:
1. The editor deduces elastic tabs by observing where text lines up. 2. Augment #1 with knowledge of the lexical structure (same complexity as a syntax highlighter) to deduce where it ought to line up. 3. Use invisible characters to identify tab-stops. (Repurpose vertical tab, maybe? Or a zero-width char?) 4. Use an external file to store tab-stops. (At which point you can name your tab stops.) 5. A 'prettifier' utility can use parsing knowledge to generate 3 or 4.
I didn't make the connection with aligning source code before - but mostly because "source code" is in the "plaintext" bucket in my head, as opposed to "rich formats" - that is, I expect to read and edit streams of bytes, the way they're saved in memory, with no invisible, hidden metadata that could get silently corrupted. While the word processor style tabs seem brilliant, I have mixed feelings here, as I wouldn't want source code to stop being raw plaintext.
(On phone. Will link GitHub PR when I’m on a desktop and can use the internet with proficiency somewhat greater than that of a caveman.)
This is only one of the reasons why I use a proportional font. The other, and arguably more important reason, is that monospaced fonts make it easier to see if I've made a mistake in repetitive code because it acts like a visual "checksum" of the line.
For JavaScript, you could simply force the code to be saved as prettier formatted without any config.
If you display it, however, you can let your editor apply whatever format you like. So you would have one source of truth in the repo and whatever you like in your editor.
Using real typographic fonts, with proportional spacing, allows for more code per screen (using multiple statements per line in many cases).
monospaced typefaces are "real", and I would hate to live in a world where someone thought it was a good idea to write software, represented in columns and lines, in anything other than monospaced font
I've always assumed the obsession with monospaced fonts in coding comes from terminals, which are monospaced for traditional reasons.
I code in non-monospsaced fonts sometimes. I enjoy it. There are (for me) basically no downsides except occasional other coders wanting things "lined up" vertically, which I don't care about but enforce using a code formatter.
But I think it will simply fragment things further (if it gets traction, that is).