summaryrefslogtreecommitdiffhomepage
path: root/src/blog/nvim-ts/index.gsp
blob: 9ad8d0d51fa435a2c480caef6949e992b2febfd2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
html lang="en" {
	head { HEAD }
	body {
		header {
			div .head {
				h1 {-Learn Your Tools}
				INCLUDE(nav.gsp)
			}

			figure .quote {
				blockquote {
					p {=
						Vim is a component among other components.  Don’t turn it into a
						massive application, but have it work well together with other
						programs.
					}
				}
				figcaption {-Vim Reference Manual}
			}
		}

		main {
			h2 #rebasing {-Git Rebasing}
			p {=
				I’m always working with Git.  I use it at work, I use it for my personal
				projects, I even use it for this site.  Git has become a part of my
				everyday life.  One Git feature that I use quite often is Git-Rebase,
				which when invoked with the @code{--i} flag allows you to reorder-,
				combine-, and delete commits interactively.
			}

			p {=
				Let’s say you make two commits called ‘Fix various typos’ and ‘Add new
				blog post’ in that order.  Now imagine you found another typo that you
				forgot to fix in your first commit.  You might end up with a history
				like so:
			}

			figure { FMT_CODE(git-log) }

			p {=
				While for many people this might be fine, I personally find it much more
				clean to have the first and third commits merged into one commit, as
				they’re two parts of the same task.  This is where Git-Rebase comes in.
				We can run @code{-git rebase -i ‘‘HEAD’’~N} where @code{-N} is the
				number of commits back we want to include, which in this case would be
				3.  Running that command will open the following buffer in your text
				editor.  In my case, Neovim.
			}

			figure { FMT_CODE(git-rebase) }

			p {=
				As suggested by the comments added to the bottom of the opened
				buffer, we can combine the two typo-fixing commits by simply
				swapping the 2@sup{-nd}- and 3@sup{-rd} lines, and then changing
				the second typo-fixing commit from a @em{-pick} to a @em{-fixup}:
			}

			figure { FMT_CODE(git-rebase-2) }

			p {=
				After saving and exiting from your editor, the Git log should now only
				have two entries, which looks a lot cleaner.
			}

			figure { FMT_CODE(git-log-2) }

			h2 #problem {-The Problem}
			p {=
				This is fine and all, but it could be better.  Specifically, it would be
				nice if instead of having to navigate to the front of the line, delete
				the word, and replace it with something new (such as @em{-fixup}), you
				could just hit ‘@kbd{-f}’ on your keyboard with your cursor on the right
				line and have it edit the command for you.  Along with fixups, it would
				also be nice to be able to press ‘@kbd{-s}’ for @em{-squash}, ‘@kbd{-r}’
				for @em{-reword}, etc.
			}

			p {=
				Seeing as the Git-Rebase interface is line-based with a simple syntax,
				you could probably easily do this with a regular-expression-based
				solution.  I’m going to use Tree-Sitter though because it’s cooler, and
				I want to show off how easy it is to use.
			}

			h2 #plugin {-Writing The Plugin}
			p {=
				The first thing to figure out is where to put the plugin.  Seeing as we
				only want it active when we’re performing a rebase, we can make use of
				the @code{-after/ftplugin} directory.  Configurations placed in this
				directory will only be applied when working in a buffer whose filetype
				corresponds to the filename.  By running @code{-:set ft?} in a
				Git-Rebase buffer we can see that the filetype is ‘@code{-gitrebase}’,
				so with all that information we know that we can put our plugin in
				@code{-after/ftplugin/gitrebase.lua}.
			}

			p {=
				The basic skeleton of the plugin is going to look like so:
			}

			figure {
				figcaption {
					code {-after/ftplugin/gitrebase.lua}
				}
				FMT_CODE(skeleton.lua)
			}

			p {=
				The @code{-map} function defined here will create a normal-mode
				keybinding where pressing the key combination provided as the first
				argument will replace the Git-Rebase command of the current line with
				the string provided in the second argument.  The actual function to
				perform this replacement isn’t implemented yet, so in its current state
				it will bind these keys to an empty function.  We also pass a few
				options to @code{-vim.keymap.set}; you can read more about these in
				@code{-:help vim.keymap.set} if you’re interested.
			}

			p {=
				The first step to implementing the actual functionality of the plugin is
				to figure out where we are.  We can do this very easily with the Neovim
				Tree-Sitter API:
			}

			figure { FMT_CODE(get-cursor.diff) }

			p {-
				The @code{-ts_utils.get_node_at_cursor()} function will return to us the
				current node in the Tree-Sitter parse tree that our cursor is located
				at.  In the case that we don’t have a Tree-Sitter parser available, the
				node will be @code{-nil} and we can just issue an error.
			}

			p {=
				Before making any more progress, it’s a good idea to make sure you have
				a proper understanding of the structure of the @code{-gitrebase} AST.
				You can view the AST by opening a new @code{-gitrebase} buffer and
				running @code{-:vim.treesitter.inspect_tree()}.  I implore you to do
				this yourself, you’ll learn from it.  The AST will end up looking
				something like this, followed by a bunch of @code{-(comment)}s:
			}

			figure { FMT_CODE(ts-tree.scm) }

			p {=
				As you can see, each line is represented by an @em{-operation} node
				which has three child nodes: a @em{-command}, a @em{-label}, and a
				@em{-message}.  You can probably begin to realize now that we’re going
				to want to get- and modify the @em{-command} node on the line that our
				cursor is on.
			}

			p {=
				In the code above we got the node at our cursor, now we need to traverse
				the AST to the operation node.  We can call the @code{-:parent()} method
				on our node in a loop to traverse up the tree until we reach our target
				node.  If our cursor isn’t on a valid line such as on a comment or a
				blank line we won’t ever hit an operation node and will instead get
				@code{-nil}, so we need to handle that case too.
			}

			figure { FMT_CODE(get-parent.diff) }

			p {=
				Now that we have the operation node, we simply have to get the child
				command node (which we know is the first child node), find out where in
				the buffer it is, and replace it.  We can call the @code{-:child(0)}
				method on our node to get the first child, and then call the
				@code{-:range()} method on the child to get position in our buffer of
				the command node.  The @code{-:range()} method returns 4 values: the
				start row, start column, end row, and end column.  We can then pass
				these positions to @code{-vim.api.nvim_buf_set_text()} to set the text
				at the given position.
			}

			figure { FMT_CODE(change-command.diff) }

			p {-
				And that is the entire plugin!  In just 28 lines of code (including
				whitespace) we implemented a plugin using Tree-Sitter to allow you to
				modify a Git-Rebase command with a single keystroke.  The completed
				product looks like so:
			}

			figure {
				figcaption {
					code {-after/ftplugin/gitrebase.lua}
				}
				FMT_CODE(final.lua)
			}

			figure {
				figcaption {-Example Usage}
				video width="100%" height="720" controls {=
					@source src="final.webm" type="video/webm" {}
					Your browser does not support the video tag.
				}
			}
		}

		footer { FOOT }
	}
}