Una Q de Quake en mi editor

Cómo hice Quake Q, una extensión de Visual Studio Code que dibuja la letra Q como el logo de Quake 1 sin cambiar ni un carácter del texto.

https://github.com/cefaloide/quake-vscode


La idea

Quake (1996) es uno de esos juegos que se te quedan grabados, y su logo también: un anillo abierto por arriba atravesado por un clavo. Un día pensé que me gustaría ver ese logo cada vez que escribo una Q en el editor. Solo esa letra; el resto del código tenía que seguir igual.

Parecía tan fácil como cambiar una fuente, pero no lo era.

El problema: VS Code no deja cambiar la fuente de una sola letra

En VS Code, editor.fontFamily se aplica a todo el editor, y la API de extensiones no tiene nada parecido a «esta letra, con esta fuente».

Lo que sí ofrece son las decoraciones (createTextEditorDecorationType). Son las mismas que usan los linters para subrayar errores o GitLens para anotar líneas. Con ellas puedes cambiar el color, el fondo o el borde de un trozo de texto, y añadir contenido antes o después de él (before y after), incluida una imagen.

Con eso había dos caminos:

  1. Inyectar CSS. El valor de la propiedad textDecoration acaba tal cual en el CSS del editor. Por eso, si escribes none; font-family: 'DpQuake', se cuela una regla font-family. Funciona, pero es un truco no documentado y solo sirve si tienes una fuente de Quake instalada. Lo dejé como modo opcional (quakeQ.fontFamily).
  2. Dibujar encima. Hacer la letra transparente y pintar una imagen en su sitio. No depende de ninguna fuente instalada, así que es el modo por defecto.

Cómo funciona por dentro

1. Una decoración que esconde la letra

El primer paso es una decoración que vuelve transparente la letra:

vscode.window.createTextEditorDecorationType({
  color: 'transparent',
  rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed
});

El carácter sigue ahí. El cursor, la selección, la búsqueda y el copiar y pegar funcionan exactamente igual; la letra solo deja de verse.

La q y la Q comparten el mismo tipo de decoración. La imagen de cada una se elige por rango con renderOptions.

2. Una imagen que no ocupa sitio

Encima de la letra transparente se pinta la imagen con un before:

before: {
  contentIconPath: svgUri,
  width: `${width}px`,
  height: `${lineHeight}px`,
  margin: `0 -${width - overhang}px 0 -${overhang}px`,
  textDecoration: 'none; vertical-align: top; pointer-events: none'
}

Un before normal empujaría el texto hacia la derecha. El truco está en los márgenes negativos:

  • El margen izquierdo retrocede lo que la imagen sobresale por la izquierda (overhang).
  • El margen derecho retrocede el resto.
  • Entre los dos suman exactamente el ancho de la imagen, así que su ancho neto es cero.

Así la imagen se superpone a la letra transparente y las columnas no se mueven ni un píxel.

vertical-align: top alinea la imagen con la parte de arriba de la línea, y pointer-events: none deja que los clics pasen al texto que hay debajo.

3. De una cuadrícula de texto a un SVG

Cada letra está dibujada como una cuadrícula de texto, donde # es un píxel encendido y . uno apagado. Esta es la Q:

.#...#.
#.....#
#..#..#
#..#..#
#..#..#
##.#.##
.#####.
...#...
...#...
...#...

Cada glifo tiene además un ascent: el número de filas que quedan por encima de la línea base. Las demás cuelgan por debajo, como el rabito de la q, y aquí ese rabito es la punta del clavo.

La función buildSvg convierte la cuadrícula en un SVG:

  • Junta los píxeles seguidos de cada fila en un solo rectángulo, para que el SVG sea pequeño.
  • Lo rellena con un degradado vertical de bronce, de #d8b070 a #7a4a20, los dos colores configurables.
  • Usa shape-rendering="crispEdges" para que los bordes salgan nítidos, como en un juego de los 90.

4. Medir sin poder medir

La API no da las medidas reales de la fuente del editor, así que las estimo a partir de la configuración:

  • Ancho de un carácter: unos 0,6 × fontSize + letterSpacing. Casi todas las fuentes monoespaciadas rondan las 0,6 em de ancho.
  • Alto de línea: si editor.lineHeight vale 0, VS Code usa 1,35 × fontSize (1,5 en macOS). Los valores menores que 8 son multiplicadores del tamaño de letra.
  • Línea base: más o menos a mitad de la línea más 0,33 × fontSize.

Como la imagen se superpone a la letra en lugar de sustituirla, un error de un píxel en estas cuentas no descoloca nada. Y si con tu fuente la letra queda un poco alta o baja, se corrige con quakeQ.verticalOffset.

5. El tamaño del píxel

El tamaño de cada «píxel» del dibujo es el menor de tres límites:

const pixel = Math.min(
  width / (COLUMNS + 1),          // que quepa a lo ancho
  baseline / glyph.ascent,        // que no se salga por arriba
  (lineHeight - baseline) / descent // ni por abajo
);

El ancho es el de un carácter multiplicado por quakeQ.scale, que por defecto vale 1,5. Así el logo es algo más grande que una letra y se mete un poco en las vecinas, pero nunca se sale de la línea.

6. Archivos SVG con nombre de hash

contentIconPath necesita la URI de un archivo. Por eso cada SVG se guarda en la carpeta de almacenamiento de la extensión (globalStorageUri), con un nombre sacado de su contenido: los primeros 12 caracteres de su hash SHA-1.

Si el SVG no cambia, el archivo tampoco, y un mapa en memoria evita volver a escribirlo.

7. Solo lo que se ve

La extensión solo busca la Q con una expresión regular en las líneas visibles, más 20 de margen por arriba y por abajo para que al hacer scroll rápido no aparezcan letras sin decorar.

El recálculo se hace con un retraso (debounce) de 50 ms cada vez que:

  • cambian los editores visibles,
  • haces scroll,
  • editas el documento.

Así un archivo de 10.000 líneas cuesta lo mismo que uno de 50.

Cuando cambias cualquier ajuste de la extensión (quakeQ.*) o del editor (editor.*), como el tamaño de letra, todo se reconstruye desde cero.

El diseño de la letra

La letra fue lo que más vueltas dio:

  1. Una q de consola. La primera versión era una q gruesa inspirada en la fuente de consola de Quake.
  2. El logo vectorial. Probé a usar el logo oficial en SVG. Al final preferí volver al pixel art, que encaja mejor con el estilo retro (además, el logo es una marca registrada de id Software).
  3. El logo en píxeles. Redibujé la letra con la forma del logo: un anillo y un clavo con cruceta.
  4. Más grande. A tamaño de editor se veía demasiado fina, así que añadí el ajuste quakeQ.scale para que ocupe algo más que una letra.
  5. Sin cruz. Quité la cruceta del clavo, que a ese tamaño ensuciaba el dibujo.
  6. Anillo redondo. Curvé las puntas hacia dentro y ensanché la base para que pareciera un anillo y no un tridente.

Por último añadí el ajuste quakeQ.letters, para elegir si se sustituye la Q mayúscula (por defecto), la q minúscula o las dos.

Ajustes

AjustePor defectoPara qué sirve
quakeQ.enabledtrueActiva o desactiva el efecto
quakeQ.lettersuppercaseuppercase, lowercase o both
quakeQ.scale1.5Tamaño del logo respecto a una letra
quakeQ.color / quakeQ.shadowColor#d8b070 / #7a4a20Colores del degradado
quakeQ.verticalOffset0Ajuste vertical en píxeles
quakeQ.fontFamily""Usar una fuente de Quake instalada en lugar del dibujo

También hay un comando, Quake Q: Toggle, para activarla y desactivarla.

Herramientas

  • JavaScript sin compilar: sin paso de build, sin TypeScript y sin dependencias. Son dos archivos: extension.js, con las decoraciones y los ajustes, y glyph.js, con los dibujos.
  • Icono y captura generados con código: se crean con Playwright a partir de la misma función buildSvg que usa la extensión, así que siempre coinciden con lo que se ve en el editor.
  • Empaquetado: con vsce.
  • Claude Code: desarrollé la extensión con Claude Code. Yo le explicaba en español lo que quería ver, revisaba las capturas y le pedía ajustes. En el repositorio hay un CLAUDE.md con las convenciones del proyecto: commits en formato Conventional Commits y todo el código y la documentación en inglés.

Limitaciones

  • Medidas estimadas: no son las reales de la fuente, así que con algunas fuentes hay que retocar verticalOffset.
  • Colores fijos: no cambian con el tema de VS Code, aunque el bronce se ve bien tanto en temas claros como oscuros.
  • Solapamiento: con scale por encima de 1, el logo se monta un poco sobre las letras de al lado.
  • Modo fuente: depende de un truco de CSS que una futura versión de VS Code podría romper.
  • Sin tests: de momento no hay tests automáticos.

Pruébala

El código está en github.com/cefaloide/quake-vscode. Para probarla, abre la carpeta en VS Code y pulsa F5. Para instalarla:

npx @vscode/vsce package
code --install-extension quake-q-0.1.0.vsix

Y a partir de ahí, cada Q de tu código será un pequeño homenaje a Quake.