2026-05-15 09:20:43 -07:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< link rel = "icon" type = "image/x-icon" href = "/favicon.ico" >
< title > comparing text editors | hello hello< / title >
< meta name = "description" content = "Lee Cattarin... on the internet!" >
< link rel = "alternate" href = "/feed.xml" type = "application/atom+xml" title = "hello hello" >
< meta property = "og:title" content = "comparing text editors | hello hello" >
< meta property = "og:type" content = "website" >
< meta property = "og:description" content = "Lee Cattarin... on the internet!" >
< meta property = "og:site_name" content = "hello hello" >
< meta property = "og:image" content = "/img/horsetail.jpg" >
< meta property = "og:image:alt" content = "Image unrelated to post. Close up on a horsetail plant's stem, with many small needle-like leaves emerging from all sides of the circular stem at each segmented joint." >
< meta name = "generator" content = "Eleventy v3.1.2" >
2026-05-21 23:35:46 +00:00
< meta itemprop = "ai-content-declaration:version" content = "1.0.0" >
< meta itemprop = "ai-content-declaration:level" content = "none" >
2026-05-15 09:20:43 -07:00
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin = "" >
< link href = "https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&display=swap" rel = "stylesheet" >
< script src = "https://kit.fontawesome.com/884dded219.js" crossorigin = "anonymous" > < / script >
< style > . p o s t - m e t a d a t a {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: baseline;
margin: 1.5rem 0 .5rem;
}
.post-metadata p {
font-size: .9rem;
margin: 0;
}
.post-tags {
display: flex;
flex-flow: row wrap;
gap: .5rem;
list-style: none;
margin: 0;
}
.post-tags li {
margin: 0;
}
.post-tags li a {
text-decoration: none;
color: var(--color-teal);
padding: 0 .5rem;
border-radius: 1rem;
box-shadow: .15rem .15rem var(--color-shadow);
border: .08rem solid var(--color-teal);
line-height: 2;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .1s ease-in, left .1s ease-in;
}
.post-tags li a:focus-visible {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
}
@media (forced-colors: active) {
.post-tags li a:focus-visible {
outline-offset: .08rem;
outline: .08rem solid;
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline-offset: .08rem;
outline: .08rem solid;
}
}
}
/* Click animation */
.post-tags li a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
/* Adapted from PrismJS 1.30.0 Tomorrow Night theme
https://prismjs.com/download
*/
code,
pre,
code[class*=language-],
pre[class*=language-] {
font-family: var(--font-family-code);
background-color: var(--color-bg-alt);
font-size: .9em;
text-shadow: 0 1px var(--color-shadow);
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre,
pre[class*=language-] {
margin: 1rem 0;
padding: 1rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
border-radius: .5rem;
overflow: auto;
}
:not(pre)>code,
:not(pre)>code[class*=language-] {
padding: .2rem;
border-radius: .25rem;
white-space: normal;
}
/* Selected text */
code ::-moz-selection,
code::-moz-selection,
pre ::-moz-selection,
pre::-moz-selection,
code[class*=language-] ::-moz-selection,
code[class*=language-]::-moz-selection,
pre[class*=language-] ::-moz-selection,
pre[class*=language-]::-moz-selection,
code ::selection,
code::selection,
pre ::selection,
pre::selection,
code[class*=language-] ::selection,
code[class*=language-]::selection,
pre[class*=language-] ::selection,
pre[class*=language-]::selection {
text-shadow: none;
background-color: var(--color-bg);
}
/* Syntax highlighting */
.token.namespace {
opacity: .7;
}
.token.bold,
.token.important {
font-weight:700
}
.token.italic {
font-style:italic
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation {
color: var(--color-grey);
}
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag {
color: var(--color-red);
}
.token.boolean,
.token.function,
.token.number {
color: var(--color-orange);
}
.token.class-name,
.token.constant,
.token.property,
.token.symbol {
color: var(--color-yellow);
}
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted {
color: var(--color-green);
}
.token.entity,
.token.operator,
.token.url,
.token.function-name {
color:var(--color-blue);
}
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: var(--color-purple);
}
#postlist,
#taglist {
list-style: none;
}
#postlist, .post,
#taglist, .tag {
margin: 0;
}
/* Odd-numbered posts & tag layout/coloration */
.post:nth-child(odd) .postlink,
.tag:nth-child(odd) .taglink {
grid-template-areas:
'img h2'
'img info'
'img .';
grid-template-columns: 45% auto;;
--color-primary: var(--color-teal);
--color-accent: var(--color-pink);
}
/* Even-numbered posts & tags layout/coloration */
.post:nth-child(even) .postlink,
.tag:nth-child(even) .taglink {
grid-template-areas:
'h2 img'
'info img'
'. img';
grid-template-columns: auto 45%;
--color-primary: var(--color-pink);
--color-accent: var(--color-teal);
}
/* Layout for all posts on mobile */
@media (max-width: 650px) {
.post:nth-child(n) .postlink,
.tag:nth-child(n) .taglink {
grid-template-areas:
'img'
'h2'
'info';
grid-template-columns: auto;
}
}
/* Link */
.postlink,
.taglink {
display: grid;
border: .25rem solid var(--color-primary);
border-radius: 1.25rem;
box-shadow: .35rem .35rem var(--color-shadow);
margin: 2rem 0;
text-decoration: none;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
.postlink:focus-visible,
.taglink:focus-visible {
background-color: var(--color-primary);
outline: none;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
background-color: var(--color-primary);
}
}
/* Forced colors */
@media (forced-colors: active) {
.postlink:focus-visible,
.taglink:focus-visible {
outline-offset: .25rem;
outline: .25rem solid;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
outline-offset: .25rem;
outline: .25rem solid;
}
}
}
/* Click animation */
.postlink:active,
.taglink:active {
box-shadow: none;
top: .2rem;
left: .2rem;
box-shadow: .15rem .15rem var(--color-shadow);
}
/* Post & tag elements */
.post h2, .post img,
.post ul, .post li,
.tag h2, .tag p,
.tag img {
margin: 0;
}
.post h2,
.tag h2 {
grid-area: h2;
padding: .25rem .5rem;
text-transform: uppercase;
font-size: 1.5rem;
color: var(--color-primary);
border-radius: 1rem 1rem 0 0;
border-bottom: .25rem solid var(--color-accent);
}
.post:nth-child(even) h2,
.tag:nth-child(even) h2 {
text-align: right;
}
.postlink:focus-visible h2,
.taglink:focus-visible h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
@media (any-hover: hover) {
.postlink:hover h2,
.taglink:hover h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
}
/* Images */
.post img,
.tag-imgs {
grid-area: img;
}
.blur {
filter: blur(.75rem);
clip-path: inset(0 round 1rem);
}
.tag-imgs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: .15rem;
}
.tag-imgs img {
aspect-ratio: 3 / 2;
object-fit: cover;
}
.missing-image {
width: 100%;
aspect-ratio: 3 / 2;
background-color: var(--color-bg-alt);
border-radius: 1rem;
}
.taglink:focus-visible .missing-image {
opacity: .7;
}
@media (any-hover: hover) {
.taglink:hover .missing-image {
opacity: .7;
}
}
/* Post tags */
.postlist-tags {
grid-area: info;
list-style: none;
display: flex;
flex-flow: row wrap;
gap: .5rem;
padding: .5rem;
}
.post:nth-child(odd) .postlist-tags {
justify-content: flex-end;
}
.postlist-tags li,
.tagcount {
background-color: var(--color-primary);
color: var(--color-bg);
padding: 0 .5rem;
border-radius: 1rem;
}
.postlink:focus-visible .postlist-tags li,
.taglink:focus-visible .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
@media (any-hover: hover) {
.postlink:hover .postlist-tags li,
.taglink:hover .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
}
/* Tag count */
.tag p {
grid-area: info;
padding: .5rem;
}
.tag:nth-child(odd) p {
text-align: right;
}
img[tabindex="0"] {
cursor: pointer;
}
img[tabindex="0"]:focus-visible {
outline: .15rem solid var(--color-teal);
}
dialog {
margin: auto;
flex-flow: column;
background: transparent;
border: none;
position: absolute;
}
dialog[open] {
display: flex;
}
dialog::backdrop {
background-color: rgba(from var(--color-bg) r g b / .8);
backdrop-filter: blur(4px);
}
body:has(dialog[open]) {
overflow: hidden;
}
.close-dialog {
background-color: rgba(from var(--color-teal) r g b / .35);
font-size: 1.5rem;
padding: 0 .5rem .15rem;
border-radius: 1rem;
color: var(--color-teal);
box-shadow: .15rem .15rem var(--color-shadow);
border: .08rem solid var(--color-teal);
align-self: flex-end;
justify-self: flex-start;
margin: 0 .1rem;
/* Click animation handling */
position: relative;
top: 1rem;
left: -.15rem;
transition: top .1s ease-in, left .1s ease-in;
}
.close-dialog:focus-visible {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
@media (any-hover: hover) {
.close-dialog:hover {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
}
@media (forced-colors: active) {
.close-dialog:focus-visible {
outline-offset: .08rem;
outline: .08rem solid;
}
@media (any-hover: hover) {
.close-dialog:hover {
outline-offset: .08rem;
outline: .08rem solid;
}
}
}
/* Click animation */
.close-dialog:active {
top: 1.1rem;
left: -.05rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
.modal-img {
max-height: calc(90vh - 2rem);
object-fit: contain;
width: auto;
max-width: 90%;
margin: 0 auto;
}
:root {
color-scheme: light dark;
--font-family: 'Atkinson Hyperlegible Next', sans-serif;
--font-family-code: 'Atkinson Hyperlegible Mono', monospace;
--color-dark: #2e303e;
--color-dark-alt: #3c3f52;
--color-light: #ebeeef;
--color-light-alt: #dbe1e3;
--color-teal-dark: #18737b;
--color-teal-light: #25b0bc;
--color-pink-dark: #94195d;
--color-pink-light: #ee9fcb;
--color-shadow: rgba(2, 10, 40, .25);
/* Used for syntax highlighting */
--color-red-light: #f195aa;
--color-orange-light: #fab795;
--color-yellow-light: #fbe6bc;
--color-green-light: #29d398;
--color-blue-light: #26bbd9;
--color-purple-light: #ddaeea;
--color-grey-light: #b9c3c6;
--color-red-dark: #991433;
--color-orange-dark: #883206;
--color-yellow-dark: #6a4906;
--color-green-dark: #125940;
--color-blue-dark: #125663;
--color-purple-dark: #722999;
--color-grey-dark: #4a4b64;
--color-text: light-dark(var(--color-dark), var(--color-light));
--color-bg: light-dark(var(--color-light), var(--color-dark));
--color-text-alt: light-dark(var(--color-dark-alt), var(--color-light-alt));
--color-bg-alt: light-dark(var(--color-light-alt), var(--color-dark-alt));
--color-teal: light-dark(var(--color-teal-dark), var(--color-teal-light));
--color-pink: light-dark(var(--color-pink-dark), var(--color-pink-light));
--color-red: light-dark(var(--color-red-dark), var(--color-red-light));
--color-orange: light-dark(var(--color-orange-dark), var(--color-orange-light));
--color-yellow: light-dark(var(--color-yellow-dark), var(--color-yellow-light));
--color-green: light-dark(var(--color-green-dark), var(--color-green-light));
--color-blue: light-dark(var(--color-blue-dark), var(--color-blue-light));
--color-purple: light-dark(var(--color-purple-dark), var(--color-purple-light));
--color-grey: light-dark(var(--color-grey-dark), var(--color-grey-light));
}
/* Base */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
color: var(--color-text);
background-color: var(--color-bg);
}
main {
width: 60vw;
max-width: 1000px;
margin: 0 auto;
scroll-margin-top: 7rem;
}
@media (max-width: 1050px) {
main {
width: 75vw;
}
}
@media (max-width: 650px) {
main {
width: 92vw;
}
}
/* Headers */
h1, h2, h3, h4, h5, h6 {
line-height: 1.25;
color: var(--color-teal);
}
h1 {
margin-top: 3rem;
font-size: 3.5rem;
text-align: center;
}
h2, h3, h4, h5, h6 {
scroll-margin-top: 5rem;
}
h2 {
margin-top: 2rem;
font-size: 2.2rem;
}
h3 {
margin-top: 1.5rem;
font-size: 1.6rem;
}
@media (max-width: 650px) {
h1 { font-size: 2.8rem; }
h2 { font-size: 1.8rem; }
h3 { font-size: 1.35rem; }
}
h4, h5, h6 {
margin-top: 1rem;
font-size: 1.2rem;
}
/* Images */
img {
display: block;
max-width: 100%;
height: auto;
border-radius: 1rem;
margin: 0 auto;
}
/* Paragraphs */
p {
margin: 1.25rem 0;
line-height: 1.4;
}
strong,
b {
font-weight: 900;
}
/* Links */
a {
color: var(--color-text);
border-radius: .25rem;
text-decoration: underline;
text-decoration-style: solid;
text-decoration-thickness: .2em;
text-decoration-color: var(--color-teal);
transition: text-decoration-thickness .5s;
}
a:focus-visible {
text-decoration: none;
outline: .15rem solid var(--color-teal);
}
@media (any-hover: hover) {
a:hover {
text-decoration-thickness: .4em;
}
}
a:active {
text-decoration-thickness: .4em;
}
/* Heading anchors */
a.ha,
span.ha-placeholder {
color: var(--color-pink);
font-size: .8em;
vertical-align: .1em;
}
span.ha-placeholder {
opacity: .55;
}
/* Lists */
::marker {
color: var(--color-pink);
}
ul, ol, dl, li {
margin-left: 1rem;
}
@media (max-width: 650px) {
ul, ol, dl {
margin-left: 0;
}
}
li {
line-height: 1.2 5;
margin-top: .65rem;
margin-bottom: .65rem;
}
li ul, li ol {
margin: .5rem 0;
}
dt {
font-weight: 900;
margin-top: .5rem;
}
dd {
margin-left: 2rem;
margin-bottom: .75rem;
}
/* Blockquotes */
blockquote {
margin: .5rem 0;
padding: 0 1rem;
border-radius: .25rem 1rem 1rem .25rem;
line-height: 1.25;
border-left: .5rem solid var(--color-pink);
}
blockquote,
blockquote p,
blockquote ol,
blockquote ul {
background-color: var(--color-bg-alt);
padding: .5rem;
}
blockquote p {
margin: 0;
}
/* Tables */
table {
width: 100%;
border-spacing: 0; /* border collapse doesn't play nice with radii */
border-radius: .3rem;
border: thin solid var(--color-pink);
}
th {
color: var(--color-bg);
background-color: var(--color-pink);
}
th code {
color: var(--color-text); /* Yes, I actually do this somewhere */
}
th, td {
padding: .5rem;
text-align: left;
}
tr:nth-child(even) { background-color: var(--color-bg-alt); }
th:not(:first-child) { border-left: thin solid var(--color-bg); }
th:first-child { border-top-left-radius: .25rem; }
th:last-child { border-top-right-radius: .25rem; }
td:not(:first-child) { border-left: thin solid var(--color-pink); }
/* Times */
time {
color: var(--color-grey);
}
/* Horizontal rules */
hr {
border: .25rem solid var(--color-pink);
margin: 2rem 0;
}
/* Used on home, reference, gallery pages */
.centered {
text-align: center;
}
/* Currently only used for resume, but it's generalizable */
.upper {
text-transform: uppercase;
}
/* Header */
header {
position: sticky;
top: 0;
background-color: var(--color-bg);
padding: .75rem 0;
z-index: 10;
border-bottom: thick solid var(--color-teal);
box-shadow: 0 .25rem .15rem var(--color-shadow);
}
/* Header links, pagination links */
header a,
.pagination a,
.webring ul a {
border-radius: 1rem;
border: .125rem solid var(--color-pink);
color: var(--color-pink);
text-decoration: none;
padding: 0 .25rem;
box-shadow: .15rem .15rem var(--color-shadow);
font-size: 1.2rem;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
header a,
.pagination .older a,
.webring .prev a,
.webring .rand a {
padding-right: .35rem;
}
.pagination .newer a,
.webring .next a,
.webring .rand a {
padding-left: .35rem;
}
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
outline: none;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
}
}
@media (forced-colors: active) {
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
outline-offset: .125rem;
outline: .125rem solid;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
outline-offset: .125rem;
outline: .125rem solid;
}
}
}
/* Click animation */
header a:active,
.pagination a:active,
.webring ul a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
/* Current page */
header a[aria-current="page"] {
border-color: var(--color-teal);
color: var(--color-teal);
}
header a[aria-current="page"]:focus-visible {
color: var(--color-bg);
border-color: var(--color-teal);
background-color: var(--color-teal);
}
@media (any-hover: hover) {
header a[aria-current="page"]:hover {
color: var(--color-bg);
background-color: var(--color-teal);
border-color: var(--color-teal);
}
}
/* Header link icons, pagination icons */
header i,
.pagination i,
.webring ul i {
color: var(--color-teal);
}
header i,
.pagination .older i,
.webring .prev i,
.webring .rand i:nth-child(1) {
padding-left: .25rem;
}
.pagination .newer i,
.webring .next i,
.webring .rand i:nth-child(2) {
padding-right: .25rem;
}
header a[aria-current="page"] i {
color: var(--color-pink);
}
header a:focus-visible i,
a[aria-current="page"] a:focus-visible i,
.pagination a:focus-visible i,
.webring ul a:focus-visible i {
color: var(--color-bg);
}
@media (any-hover: hover) {
header a:hover i,
header a[aria-current="page"]:hover i,
.pagination a:hover i,
.webring ul a:hover i {
color: var(--color-bg);
}
}
/* Skip link */
#skip {
left: -999px;
position: absolute;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
z-index: -99;
}
#skip:focus-visible {
display: inline-block;
left: auto;
top: auto;
width: auto;
height: auto;
overflow: auto;
margin: 0 10%;
z-index: 999;
}
/* Nav */
header ul {
display: flex;
list-style: none;
gap: 1rem;
justify-content: center;
}
header ul,
header li {
margin: 0;
}
@media (max-width: 650px) {
.menu-text {
display: none; /* Icons only on small screens */
}
header a {
padding: .15rem .5rem;
}
header i {
padding: 0;
}
}
/* Footer */
footer {
padding: 1rem 0;
font-size: .9rem;
border-top: thick solid var(--color-pink);
margin-top: 2rem;
}
footer ul {
display: flex;
list-style: none;
gap: .5rem;
justify-content: center;
margin: 0;
}
footer li {
margin: 0;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: " ● " / "";
color: var(--color-teal);
}
@media (max-width: 650px) {
footer ul {
flex-flow: column;
text-align: center;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: none;
}
}
footer a {
text-decoration-color: var(--color-pink);
}
footer a:focus-visible {
outline-color: var(--color-pink);
}
/* Pagination */
.pagination,
.pagination li {
margin: 0;
}
.pagination {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-areas: "older newer";
list-style: none;
margin-top: 3rem;
}
@media (max-width: 650px) {
.post-pagination {
grid-template-columns: 1fr;
grid-template-areas:
"older"
"newer";
gap: .75rem;
}
}
.pagination .older {
grid-area: older;
}
.pagination .newer {
grid-area: newer;
text-align: right;
}
/* webring navigation */
.webring {
margin-bottom: 3rem;
}
.webring ul {
display: flex;
flex-flow: row wrap;
list-style: none;
justify-content: space-around;
gap: .35rem;
}
.webring ul,
.webring li {
margin-left: 0;
}
@media print {
/* Nav elements */
header,
footer,
nav {
display: none !important;
}
/* Base */
body {
background-color: #fff;
color: #000;
}
main {
width: 95vw;
}
h1,h2,h3,h4,h5,h6 {
color: #000;
}
/* Links */
/* Hover is not really necessary, but it's annoying when testing otherwise */
a,
a:hover {
text-decoration-style: dotted;
text-decoration-thickness: .1rem;
text-decoration-color: #000;
}
a::after{
content: " (" attr(href) ")";
}
/* Code */
code,
pre,
code[class*=language-],
pre[class*=language-] {
text-shadow: none;
background-color: var(--color-light);
color: #000 !important;
}
.token.namespace {
opacity: 1;
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation,
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag,
.token.boolean,
.token.function,
.token.number,
.token.class-name,
.token.constant,
.token.property,
.token.symbol,
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted,
.token.entity,
.token.operator,
.token.url,
.token.function-name,
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: #000;
}
}< / style >
< script type = "module" > / / T h a n k y o u t o h t t p s : / / g i t h u b . c o m / d a v i d d a r n e s / h e a d i n g - a n c h o r s
// Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
let globalInstanceIndex = 0;
class HeadingAnchors extends HTMLElement {
static register(tagName = "heading-anchors", registry = window.customElements) {
if(registry & & !registry.get(tagName)) {
registry.define(tagName, this);
}
}
static attributes = {
exclude: "data-ha-exclude",
prefix: "prefix",
content: "content",
}
static classes = {
anchor: "ha",
placeholder: "ha-placeholder",
srOnly: "ha-visualhide",
}
static defaultSelector = "h2,h3,h4,h5,h6";
static css = `
.${HeadingAnchors.classes.srOnly} {
clip: rect(0 0 0 0);
height: 1px;
overflow: hidden;
position: absolute;
width: 1px;
}
.${HeadingAnchors.classes.anchor} {
position: absolute;
left: var(--ha_offsetx);
top: var(--ha_offsety);
text-decoration: none;
opacity: 0;
}
.${HeadingAnchors.classes.placeholder} {
opacity: .3;
}
.${HeadingAnchors.classes.anchor}:is(:focus-within, :hover) {
opacity: 1;
}
.${HeadingAnchors.classes.anchor},
.${HeadingAnchors.classes.placeholder} {
display: inline-block;
padding: 0 .25em;
/* Disable selection of visually hidden label */
-webkit-user-select: none;
user-select: none;
}
@supports (anchor-name: none) {
.${HeadingAnchors.classes.anchor} {
position: absolute;
left: anchor(left);
top: anchor(top);
}
}`;
get supports() {
return "replaceSync" in CSSStyleSheet.prototype;
}
get supportsAnchorPosition() {
return CSS.supports("anchor-name: none");
}
constructor() {
super();
if(!this.supports) {
return;
}
let sheet = new CSSStyleSheet();
sheet.replaceSync(HeadingAnchors.css);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
this.headingStyles = {};
this.instanceIndex = globalInstanceIndex++;
}
connectedCallback() {
if (!this.supports) {
return;
}
this.headings.forEach((heading, index) => {
if(!heading.hasAttribute(HeadingAnchors.attributes.exclude)) {
let anchor = this.getAnchorElement(heading);
let placeholder = this.getPlaceholderElement();
// Prefers anchor position approach for better accessibility
// https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
if(this.supportsAnchorPosition) {
let anchorName = `--ha_${this.instanceIndex}_${index}`;
placeholder.style.setProperty("anchor-name", anchorName);
anchor.style.positionAnchor = anchorName;
}
heading.appendChild(placeholder);
heading.after(anchor);
}
});
}
// Polyfill-only
positionAnchorFromPlaceholder(placeholder) {
if(!placeholder) {
return;
}
let heading = placeholder.closest("h1,h2,h3,h4,h5,h6");
if(!heading.nextElementSibling) {
return;
}
// TODO next element could be more defensive
this.positionAnchor(heading.nextElementSibling);
}
// Polyfill-only
positionAnchor(anchor) {
if(!anchor || !anchor.previousElementSibling) {
return;
}
// TODO previous element could be more defensive
let heading = anchor.previousElementSibling;
this.setFontProp(heading, anchor);
if(this.supportsAnchorPosition) {
// quit early
return;
}
let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
anchor.style.setProperty("--ha_offsetx", `${placeholder.offsetLeft}px`);
anchor.style.setProperty("--ha_offsety", `${placeholder.offsetTop}px`);
}
}
setFontProp(heading, anchor) {
let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
let style = getComputedStyle(placeholder);
let props = ["font-weight", "font-size", "line-height", "font-family"];
let [weight, size, lh, family] = props.map(name => style.getPropertyValue(name));
anchor.style.setProperty("font", `${weight} ${size}/${lh} ${family}`);
let vars = style.getPropertyValue("font-variation-settings");
if(vars) {
anchor.style.setProperty("font-variation-settings", vars);
}
}
}
/* no longer used, using aria-labelledby instead - see getAnchorElement
getAccessibleTextPrefix() {
// Useful for i18n
return this.getAttribute(HeadingAnchors.attributes.prefix) || "Jump to section titled";
}
*/
getContent() {
if(this.hasAttribute(HeadingAnchors.attributes.content)) {
return this.getAttribute(HeadingAnchors.attributes.content);
}
return "#";
}
// Placeholder nests inside of heading
getPlaceholderElement() {
let ph = document.createElement("span");
ph.setAttribute("aria-hidden", true);
ph.classList.add(HeadingAnchors.classes.placeholder);
let content = this.getContent();
if(content) {
ph.innerHTML = content; // CHANGED HERE
}
ph.addEventListener("mouseover", (e) => {
let placeholder = e.target.closest(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
this.positionAnchorFromPlaceholder(placeholder);
}
});
return ph;
}
getAnchorElement(heading) {
let anchor = document.createElement("a");
anchor.href = `#${heading.id}`;
anchor.classList.add(HeadingAnchors.classes.anchor);
let content = this.getContent();
anchor.innerHTML = `< span aria-labelledby = ${heading.id} > ${content}< / span > `; // CHANGED HERE
anchor.addEventListener("focus", e => {
let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`);
if(anchor) {
this.positionAnchor(anchor);
}
});
anchor.addEventListener("mouseover", (e) => {
// when CSS anchor positioning is supported, this is only used to set the font
let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`);
this.positionAnchor(anchor);
});
return anchor;
}
get headings() {
return this.querySelectorAll(this.selector.split(",").map(entry => `${entry.trim()}[id]`));
}
get selector() {
return this.getAttribute("selector") || HeadingAnchors.defaultSelector;
}
}
HeadingAnchors.register();
export { HeadingAnchors }
/* don't even bother on mobile */
if (window.innerWidth > 650) {
const dialog = document.querySelector("dialog");
const closeButton = document.querySelector(".close-dialog");
const hero = document.querySelector(".hero");
hero.tabIndex = 0;
hero.addEventListener("click", (e) => dialog.showModal());
hero.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
dialog.showModal();
}
});
closeButton.addEventListener("click", (e) => dialog.close());
closeButton.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
dialog.close();
}
});
}< / script >
< / head >
< body >
< header >
< a href = "#main" id = "skip" title = "skip to main content" >
< i class = "fa-solid fa-forward" aria-hidden = "true" > < / i > skip
< / a >
< nav aria-label = "main navigation" >
< ul >
< li >
< a href = "/reference/" title = "read reference posts" >
< i class = "fa-regular fa-folder-open" aria-hidden = "true" > < / i >
< span class = "menu-text" > reference< / span >
< / a >
< / li >
< li >
< a href = "/gallery/" title = "view the gallery" >
< i class = "fa-regular fa-images" aria-hidden = "true" > < / i >
< span class = "menu-text" > gallery< / span >
< / a >
< / li >
< li >
< a href = "/" title = "home" >
< i class = "fa fa-solid fa-crow" aria-hidden = "true" > < / i >
< span class = "menu-text" > home< / span >
< / a >
< / li >
< li >
< a href = "/about/" title = "about Lee" >
< i class = "fa-regular fa-user" aria-hidden = "true" > < / i >
< span class = "menu-text" > about< / span >
< / a >
< / li >
< li >
< a href = "/contact/" title = "contact Lee" >
< i class = "fa-solid fa-envelope-open-text" aria-hidden = "true" > < / i >
< span class = "menu-text" > contact< / span >
< / a >
< / li >
< / ul >
< / nav >
< / header >
< main id = "main" >
< heading-anchors content = "<i class='fa-solid fa-anchor'></i>" >
< article >
< h1 id = "comparing-text-editors" > comparing text editors< / h1 >
< div class = "post-metadata" >
< p >
posted on
< time datetime = "2026-01-08" > January 8, 2026< / time >
by Lee Cattarin
< / p >
< ul class = "post-tags" >
< li >
< a href = "/tags/software/" > software< / a >
< / li >
< / ul >
< / div >
< dialog closedby = "any" aria-label = "image modal" tabindex = "-1" >
< button class = "close-dialog" autofocus = "" aria-label = "close the image modal" > × < / button >
< img class = "modal-img" src = "/img/horsetail.jpg" alt = "Image unrelated to post. Close up on a horsetail plant's stem, with many small needle-like leaves emerging from all sides of the circular stem at each segmented joint." loading = "lazy" decoding = "async" width = "1000" height = "666" >
< / dialog >
< img class = "hero" src = "/img/horsetail.jpg" alt = "Image unrelated to post. Close up on a horsetail plant's stem, with many small needle-like leaves emerging from all sides of the circular stem at each segmented joint." loading = "lazy" decoding = "async" width = "1000" height = "666" >
< p > I'm fucking depressed. No, not like mental health depressed (okay, look, that too, but that's not relevant here). Looking to switch text editors, I reviewed 6 different options...and what I found didn't thrill me.< / p >
< blockquote >
< p > this post contains comparison tables that are far more viewable on desktop/tablet< / p >
< / blockquote >
< h2 id = "the-editors" > the editors< / h2 >
< p > the editors I reviewed, in no particular order, are:< / p >
< ul >
< li > < a href = "https://code.visualstudio.com/" target = "_blank" rel = "external" > VSCode< / a > (my current daily driver which I've been wanting to leave behind for a while now)< / li >
< li > < a href = "https://zed.dev/" target = "_blank" rel = "external" > Zed< / a > (an editor I've been tentatively interested in)< / li >
< li > < a href = "https://kate-editor.org/" target = "_blank" rel = "external" > Kate< / a > (recommended to me semi-recently on the fediverse, but one I quickly wrote off at the time because of clunkiness)< / li >
< li > < a href = "https://lap.dev/lapce/" target = "_blank" rel = "external" > Lapce< / a > (recommended via fedi)< / li >
< li > < a href = "https://pulsar-edit.dev/" target = "_blank" rel = "external" > Pulsar< / a > (a fork of the discontinued Atom editor, recommended again by fedi, in the course of this review)< / li >
< li > and finally, < a href = "https://www.sublimetext.com/" target = "_blank" rel = "external" > Sublime Text< / a > (my daily driver back in college)< / li >
< / ul >
< h2 id = "the-qualities" > the qualities< / h2 >
< p > I reviewed looking for 5 major < strong > functional< / strong > qualities that I considered to be my most useful or heavily-used features:< / p >
< ul >
< li > < code > .editorconfig< / code > support< / li >
< li > find and replace with regex support< / li >
< li > WSL support (including, because apparently this is an issue, the ability to delete files from inside the editor) (this is the highest priority - I do < em > all< / em > work in WSL)< / li >
< li > multi-edit (e.g. ability to select multiple instances of a word and edit them all) (this is the most discardable functionality, as it can be replaced with find-and-replace)< / li >
< li > markdown preview< / li >
< / ul >
< p > 3 less important, but preferred, < strong > aesthetic< / strong > qualities:< / p >
< ul >
< li > clean, modern UI< / li >
< li > file type icons< / li >
< li > ability to customize the color scheme (ideally per-color settings, but that's uncommon)< / li >
< / ul >
< p > and finally, 3 < strong > ethical and trustworthiness< / strong > qualities:< / p >
< ul >
< li > no LLM features< / li >
< li > for products with LLM features: single setting to turn off LLM features< / li >
< li > trust that product longevity will not be affected by LLM-generated code< / li >
< / ul >
< p > (in other words: no LLMs).< / p >
< h2 id = "the-comparison-charts" > the comparison charts< / h2 >
< h3 id = "functional-qualities" > functional qualities< / h3 >
< table >
< thead >
< tr >
< th > < / th >
< th > < code > .editorconfig< / code > < / th >
< th > find-and-replace< / th >
< th > WSL< / th >
< th > multi-edit< / th >
< th > < code > .md< / code > preview< / th >
< / tr >
< / thead >
< tbody >
< tr >
< td > VSCode< / td >
< td > < strong > yes< / strong > [1]< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > [1]< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< / tr >
< tr >
< td > Zed< / td >
< td > < strong > yes< / strong > [1]< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< / tr >
< tr >
< td > Kate< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< td > no[2]< / td >
< td > < strong > yes< / strong > < / td >
< td > no< / td >
< / tr >
< tr >
< td > Lapce< / td >
< td > no< / td >
< td > < strong > yes< / strong > < / td >
< td > no[3]< / td >
< td > < strong > yes< / strong > < / td >
< td > no< / td >
< / tr >
< tr >
< td > Pulsar< / td >
< td > no< / td >
< td > < strong > yes< / strong > < / td >
< td > no[3]< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< / tr >
< tr >
< td > Sublime Text< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< td > no[3][4]< / td >
< td > < strong > yes< / strong > < / td >
< td > no[5]< / td >
< / tr >
< / tbody >
< / table >
< p > [1] extension needed: < a href = "https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig" target = "_blank" rel = "external" > VSCode < code > .editorconfig< / code > < / a > , < a href = "https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack" :target = "_blank" > VSCode WSL< / a > , < a href = "https://zed.dev/extensions/editorconfig" :target = "_blank" > Zed < code > .editorconfig< / code > < / a > < / p >
< p > [2] I could open a WSL directory in Kate, but couldn't see any files. I confirmed that opening a Windows directory worked as expected.< / p >
< p > [3] opening the project worked fine, but I couldn't delete files. In Sublime Text's case, they were deleted but still shown in the file view.< / p >
< p > [4] saving a new file opens the save menu in the Windows File Explorer, which frankly makes me a bit afraid. Touching your WSL files from Windows is generally a bad idea.< / p >
< p > [5] I explored two different add-on packages for Markdown preview support. < a href = "https://packagecontrol.io/packages/MarkdownLivePreview" target = "_blank" rel = "external" > Markdown Live Preview< / a > and < a href = "https://packagecontrol.io/packages/MarkdownPreview" :target = "_blank" > MarkdownPreview< / a > . Markdown Live Preview opened a whole new window scoped only to the specific < code > .md< / code > file. MarkdownPreview previewed in browser. Neither of these match the behavior I am looking for.< / p >
< h3 id = "aesthetic-qualities" > aesthetic qualities< / h3 >
< table >
< thead >
< tr >
< th > < / th >
< th > UI< / th >
< th > file icons< / th >
< th > color scheme< / th >
< / tr >
< / thead >
< tbody >
< tr >
< td > VSCode< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > [1]< / td >
< / tr >
< tr >
< td > Zed< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > [2]< / td >
< / tr >
< tr >
< td > Kate< / td >
< td > no< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > [2]< / td >
< / tr >
< tr >
< td > Lapce< / td >
< td > < strong > yes< / strong > < / td >
< td > no< / td >
< td > < strong > yes< / strong > [3]< / td >
< / tr >
< tr >
< td > Pulsar< / td >
< td > < strong > yes< / strong > < / td >
< td > no< / td >
< td > < strong > yes< / strong > [1]< / td >
< / tr >
< tr >
< td > Sublime Text< / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< td > < strong > yes< / strong > < / td >
< / tr >
< / tbody >
< / table >
< p > [1] several color schemes available. Further extensions available.< / p >
< p > [2] several color schemes available.< / p >
< p > [3] only light and dark schemes available. Further plugins available.< / p >
< h3 id = "ethical-qualities" > ethical qualities< / h3 >
< p > this is my best guess based on searching online and reviewing the settings; it's kind of hard to really confirm these things. I am judging the last quality - whether or not the devs are using LLMs - based on the existence of < code > prompts< / code > directories in the projects' repositories.< / p >
< table >
< thead >
< tr >
< th > < / th >
< th > no LLM features< / th >
< th > LLM kill switch< / th >
< th > no LLM code< / th >
< / tr >
< / thead >
< tbody >
< tr >
< td > VSCode< / td >
< td > no< / td >
< td > no< / td >
< td > no[1]< / td >
< / tr >
< tr >
< td > Zed< / td >
< td > no< / td >
< td > < strong > yes< / strong > < / td >
< td > no[2]< / td >
< / tr >
< tr >
< td > Kate< / td >
< td > < strong > yes< / strong > < / td >
< td > n/a< / td >
< td > < strong > yes< / strong > < / td >
< / tr >
< tr >
< td > Lapce< / td >
< td > < strong > yes< / strong > < / td >
< td > n/a< / td >
< td > < strong > yes< / strong > < / td >
< / tr >
< tr >
< td > Pulsar< / td >
< td > < strong > yes< / strong > < / td >
< td > n/a< / td >
< td > < strong > yes< / strong > [3]< / td >
< / tr >
< tr >
< td > Sublime Text< / td >
< td > < strong > yes< / strong > < / td >
< td > n/a< / td >
< td > unknown[4]< / td >
< / tr >
< / tbody >
< / table >
< p > [1] < a href = "https://github.com/microsoft/vscode/tree/main/.github/prompts" target = "_blank" rel = "external" > prompts directory< / a > < / p >
< p > [2] looks like at current LLMs are only used for < a href = "https://github.com/zed-industries/zed/tree/main/.factory/prompts" target = "_blank" rel = "external" > documentation< / a > < / p >
< p > [3] it's been discussed and sounds currently up for debate: < a href = "https://github.com/pulsar-edit/pulsar/issues/792" target = "_blank" rel = "external" > Pulsar< / a > , < a href = "https://forum.sublimetext.com/t/implement-ai-into-sublime-text/68411" :target = "_blank" > Sublime Text< / a > < / p >
< p > [4] I couldn't find the source code for Sublime Text online; I assume it's not OSS. If you know where it is, point me in that direction.< / p >
< h2 id = "summary" > summary< / h2 >
< p > I'll be honest, I just don't know. The functionality is not something I can easily compromise on. TBH, I figured I had pretty basic needs as a developer, but it seems that's not the case! The only editors that meet my functionality needs across the board are < em > also< / em > the worst offenders on the LLM front.< / p >
< p > at the end of the day, I might just have to keep looking... but regardless, I wanted to publish what I found to help anyone else with similar needs.< / p >
< h2 id = "corrections-and-edits" > corrections and edits< / h2 >
< ul >
< li > Kate < em > does< / em > have multi-edit< / li >
< li > Lapce has plugins for further color schemes< / li >
< li > VSCode WSL support requires an extension< / li >
< li > Zed < code > .editorconfig< / code > support requires an extension< / li >
< / ul >
< h3 id = "wsl2-and-linux-guis" > WSL2 and Linux GUIs< / h3 >
< p > I learned that < a href = "https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps" target = "_blank" rel = "external" > WSL2 can support GUI apps< / a > so I tried this for a few. Pulsar did not work; Sublime Text worked but the UI scale was < em > teensy< / em > and was not affected by the < code > ui_scale< / code > setting.< / p >
< / article >
< nav aria-label = "pagination" >
< ol class = "pagination post-pagination" >
< li class = "older" >
< a href = "/an-intro-to-git/" >
< i class = "fa-solid fa-hand-point-left" aria-hidden = "true" > < / i > an intro to git
< / a >
< / li >
< li class = "newer" >
< a href = "/spinning-plants/" >
spinning plants < i class = "fa-solid fa-hand-point-right" aria-hidden = "true" > < / i >
< / a >
< / li >
< / ol >
< / nav >
< hr >
< section class = "related-posts" >
< h2 data-ha-exclude = "" id = "related-posts" > related posts< / h2 >
< ol id = "postlist" >
< li class = "post" >
2026-07-26 22:09:10 +00:00
< a class = "postlink" href = "/my-favorite-git-flag/" >
< h2 data-ha-exclude = "" id = "my-favorite-git-flag" > my favorite git flag < / h2 >
2026-05-15 09:20:43 -07:00
< ul class = "postlist-tags" >
< li > software< / li >
< / ul >
2026-07-26 22:09:10 +00:00
< img src = "/img/shelf-mushrooms.jpg" alt = "Picture unrelated to post. Creamy beige shelf mushrooms on a mossy tree trunk." loading = "lazy" decoding = "async" width = "1000" height = "666" >
2026-05-15 09:20:43 -07:00
< / a >
< / li >
< li class = "post" >
2026-07-26 22:09:10 +00:00
< a class = "postlink" href = "/core-css/" >
< h2 data-ha-exclude = "" id = "core-css" > core CSS < / h2 >
2026-05-15 09:20:43 -07:00
< ul class = "postlist-tags" >
< li > software< / li >
< / ul >
2026-07-26 22:09:10 +00:00
< img src = "/img/core-css.png" alt = "A website mockup in dark mode with light pink accents. Everything is filler text such as the title 'This is my cool website', headers reading things like 'This is an h1', nav items 'Nav item 1', etc. There is a paragraph block - the opening of the Bee Movie - and part of an image visible - a screencap of the music video for Never Gonna Give You Up by Rick Astley." loading = "lazy" decoding = "async" width = "1000" height = "447" >
2026-05-15 09:20:43 -07:00
< / a >
< / li >
< li class = "post" >
2026-07-26 22:09:10 +00:00
< a class = "postlink" href = "/domain-and-site-setup/" >
< h2 data-ha-exclude = "" id = "domain-and-site-setup" > domain and site setup < / h2 >
2026-05-15 09:20:43 -07:00
< ul class = "postlist-tags" >
< li > software< / li >
< / ul >
2026-07-26 22:09:10 +00:00
< img src = "/img/crinkly-mushrooms.jpg" alt = "Picture unrelated to post. Some crinkly brown-orange mushrooms in vibrant green grass." loading = "lazy" decoding = "async" width = "1000" height = "750" >
2026-05-15 09:20:43 -07:00
< / a >
< / li >
< / ol >
< / section >
< / heading-anchors >
< / main >
< footer >
< ul >
< li >
< a href = "/colophon/" >
colophon
< / a >
< / li >
< li >
< a href = "/" title = "go home" aria-label = "go home | hello hello from Lee Cattarin, copyright 2022-2026" >
hello hello from Lee Cattarin, © 2022-2026< / a >
< / li >
< li >
< a href = "https://heckin.technology/inherentlee/leecat.art" title = "source code" aria-label = "source code" target = "_blank" rel = "external" >
src
< / a >
< / li >
< / ul >
< / footer >
2026-07-26 22:09:10 +00:00
<!-- This page `/comparing - text - editors/` was built on 2026 - 07 - 26T22:08:52.664Z -->
2026-05-15 09:20:43 -07:00
< / body >
< / html >