FlowCode
Un IDE escrito en Go, renderizado por GPU. Sin Electron, sin Node, sin JavaScript.
El plan completo de desarrollo está en docs/PLAN.md.
Estado
Hitos 0 a 3 completados en Windows; el 4, a medias. FlowCode ya es un
editor: se abre un archivo, se escribe con resaltado de sintaxis, se deshace, se
edita con varios cursores a la vez y se guarda en el disco. Alrededor está el
chasis completo —barra de actividad, árbol de archivos navegable, pestañas,
barra de estado y paleta de comandos—, todo dibujado con un framework de
interfaz propio sobre un atlas de glifos en GPU.
El resaltado lo hace hoy un lexer incremental propio, no Tree-sitter. La razón
está medida y contada más abajo, en «Sintaxis: lo que hay y lo que falta». De
ahí que falten las tres cosas que necesitan un árbol de verdad: plegado,
indentación automática por contexto y breadcrumb.
El buscador de archivos y la búsqueda en proyecto son el Hito 5.
Ejecutar
Necesitas Go 1.26 o superior. En Windows no hace falta nada más: no se usa cgo.
go run ./cmd/flowcode archivo.go
go run ./cmd/flowcode # sin argumento: documento generado de 200.000 líneas
O sin clonar nada, para probarlo:
go install github.com/Alexis-Reillo-Segarra/flowcode/cmd/flowcode@latest
| Tecla |
Acción |
| Flechas |
Mover el cursor |
Ctrl+←/→ |
Saltar por palabras |
Shift+cualquier movimiento |
Extender la selección |
Inicio / Fin |
Principio (alterna con el margen) y final de línea |
Ctrl+Inicio / Fin |
Principio y final del documento |
Ctrl+Z / Ctrl+Y |
Deshacer y rehacer |
Ctrl+S |
Guardar |
Ctrl+X / C / V |
Cortar, copiar y pegar |
Ctrl+Retroceso / Supr |
Borrar la palabra anterior o la siguiente |
Tab / Shift+Tab |
Indentar y desindentar la selección |
Ctrl+Alt+↑/↓ |
Añadir un cursor arriba o abajo |
Alt+clic |
Añadir un cursor donde se pulse |
Ctrl+A / Ctrl+L |
Seleccionar todo / la línea |
Alt+↑/↓ |
Expandir y contraer la selección |
Ctrl+Shift+\ |
Ir al delimitador emparejado |
Ctrl+Shift+K |
Borrar la línea |
| Clic y arrastrar |
Seleccionar; doble clic la palabra, triple la línea |
Rueda, Shift+rueda |
Desplazar en vertical y en horizontal |
Ctrl+P |
Paleta de comandos |
Ctrl+Espacio |
Alternar el banco de pruebas de scroll |
Escape |
Cerrar la paleta, dejar un solo cursor, o salir |
Todos los atajos son comandos con condición, así que la misma tecla hace cosas
distintas según dónde esté el foco: Escape cierra la paleta si está abierta,
colapsa los cursores si hay varios, y si no, sale.
En el árbol de archivos: las flechas se mueven, →/← despliegan y pliegan, y un
clic abre. También se puede soltar un archivo o una carpeta sobre la ventana.
Los archivos de más de 32 MB se abren en modo lectura, proyectados en
memoria. Editar exige el archivo entero en una estructura mutable; leer no, y
esa diferencia es lo que permite abrir un gigabyte en un milisegundo.
El cursor no parpadea, y es a propósito: parpadear obliga a repintar dos veces
por segundo para siempre, y eso rompe la propiedad de no consumir CPU en reposo.
Para medir el rendimiento sin intervención, desactivando la sincronización
vertical:
go run ./cmd/flowcode -bench -bench-duration 10s archivo.go
Para desglosar el arranque:
FLOWCODE_TRACE=1 go run ./cmd/flowcode # PowerShell: $env:FLOWCODE_TRACE=1
Resultados medidos
RTX 3060, ventana de 1280x800, texto real con Cascadia Mono, scroll continuo.
| Métrica |
Objetivo |
Medido |
| Fotogramas por segundo |
120 |
~3.500 |
| Fotograma p50 |
< 8,3 ms |
245 µs |
| Fotograma p99 |
< 8,3 ms |
635 µs |
| Fotograma p99,9 |
< 8,3 ms |
1,17 ms |
| Draw calls por fotograma |
pocos |
9 — el chasis entero |
| CPU en reposo |
~0 % |
0 % |
| Arranque hasta el primer fotograma |
< 100 ms |
215 ms |
| RAM en reposo |
< 150 MB |
70 MB |
| Tamaño del binario |
< 40 MB |
3,8 MB |
Las cifras son del chasis completo: barra de actividad, árbol de archivos,
pestañas, editor con 1.060 cuadriláteros en pantalla y barra de estado. En el
Hito 1, cuando solo había un visor sin paneles, el fotograma costaba 2 envíos a
la GPU en lugar de 9; el apartado sobre los draw calls, más abajo, explica de
dónde salen los siete restantes y por qué no crecen con el contenido.
El resaltado cuesta en torno a un 15%: sin él, las mismas cifras son 4.150
fps y 455 µs de p99. Se paga en medir la posición de cada tramo desde el
principio de la línea, que es lo correcto cuando hay tabulaciones —las paradas
son absolutas y un ancho acumulado se desviaría— y es lo que impide ir sumando.
El fotograma sigue trece veces por debajo del presupuesto de 8,3 ms.
Un archivo de un gigabyte
Es el entregable del Hito 1, así que se mide aparte. Archivo de 1,0 GB con
31.597.656 líneas:
|
|
| Abrir el archivo |
1,1 ms |
| Primer fotograma |
214 ms — el mismo que sin abrir nada |
| Indexar los 31,6 M de líneas |
203 ms, en segundo plano |
| Scroll |
4.206 fps, p99 437 µs |
| Memoria privada |
203 MB |
Abrir cuesta un milisegundo porque el archivo no se lee: se proyecta en memoria
y el sistema trae del disco solo las páginas que se miran. Antes de ese cambio,
el mismo archivo tardaba 840 ms en abrirse. El índice de líneas se construye en
paralelo mientras el usuario ya está leyendo, y hasta que termina la barra de
estado dice cuántas líneas lleva contadas en lugar de mentir con un total.
Los 203 MB de memoria privada son casi todos el índice: 31,6 millones de
desplazamientos de 4 bytes. El conjunto de trabajo que reporta Windows sube a
1,2 GB, pero eso incluye las páginas proyectadas del archivo, que son
compartidas con la caché del sistema y descartables sin coste.
Dos observaciones honestas
El arranque no cumple el objetivo y probablemente no pueda cumplirlo. El
desglose con FLOWCODE_TRACE=1 sitúa 185 de esos 214 ms en la inicialización
del driver de OpenGL de NVIDIA, que ocurre al crear el primer contexto y está
fuera de nuestro control. El código propio de FlowCode aporta unos 29 ms.
Aparece un fotograma aislado de 25-40 ms por ejecución. Con un archivo
grande ocurre al arrancar y es atribuible al indexado en segundo plano —limitar
sus trabajadores a un núcleo menos que la máquina lo bajó de 124 ms a 25 ms—.
Sin archivo grande aparece igualmente a mitad de recorrido, y ahí el trazado del
recolector lo descarta como causa: apunta a una interrupción del sistema. Con
sincronización vertical, que es el modo normal, no es perceptible.
| Plataforma |
Compila |
Se ejecuta |
| Windows amd64 / arm64 |
Sí |
Sí |
| Linux amd64 / arm64 |
Sí |
No, backend de ventana pendiente |
| macOS amd64 / arm64 |
Sí |
No, backend de ventana pendiente |
Las capas independientes del sistema —geom, clock, gpu, app— compilan y
pasan las pruebas en las seis combinaciones, y la integración continua lo
verifica. Lo que falta en Linux y macOS es únicamente la creación de ventana y
contexto, prevista con GLFW detrás de la misma interfaz platform.Window.
Arquitectura
Capas apiladas estrictamente; cada una solo depende de las inferiores.
app/ composición: chasis, comandos, banco de pruebas
├── editor/ la vista de código: cursores, edición, comandos
├── command/ registro de comandos y atajos con condición
├── syntax/ lexer incremental, resaltado, delimitadores
├── ui/ elementos, layout, eventos, tema, widgets
├── buffer/ rope, transacciones, deshacer, guardado (sin nada gráfico)
├── text/ fuentes, rasterizado, atlas de glifos
├── gpu/ interfaz de dibujo + backend OpenGL 3.3
├── platform/ ventana, entrada, contexto (syscall en Windows, sin cgo)
├── geom/ geometría y color (hoja)
└── clock/ reloj de alta resolución (hoja)
La regla no es una convención: internal/archtest la comprueba analizando las
importaciones de todos los archivos, incluidos los de plataformas que no se
estén compilando. Un paquete nuevo bajo internal/ obliga a declarar su
posición en el apilamiento, y una importación en sentido inverso rompe la
integración continua.
Por qué existe internal/clock
En Windows, time.Now tiene una resolución de unos 500 µs porque el runtime de
Go lee el contador de interrupciones del sistema. Con 8,3 ms de presupuesto por
fotograma, medir con esa granularidad hace que cualquier fotograma rápido se
registre como cero: las primeras cifras de este proyecto salieron con un p50
de 0 s, que era ruido de cuantización y no rendimiento. clock usa
QueryPerformanceCounter (100 ns) y una prueba falla si la resolución se
degrada.
Por qué hay tan pocos draw calls
Todo lo que FlowCode pinta es un cuadrilátero texturizado. Los glifos salen del
atlas, y los rectángulos —fondos, bordes, resaltado de línea, cursores— usan un
texel opaco reservado dentro de ese mismo atlas. Como el lote solo se rompe al
cambiar de textura o de recorte, una pantalla entera de código cabe en un envío.
De ahí que el número de envíos sea el número de recortes distintos: el árbol de
archivos, la barra de pestañas, el margen con los números de línea, el código y
la barra de estado. Cada panel recorta al suyo, que es justamente lo que impide
que uno invada al de al lado, y son cinco o seis paneles, no cientos de widgets.
El corolario práctico: el coste de un fotograma no depende de cuánto haya en
pantalla, sino de cuántos paneles con recorte propio haya. Añadir mil líneas de
código visibles no cuesta un envío más; añadir un panel lateral nuevo, sí.
El modelo de texto
El texto editable es un rope: un árbol equilibrado de trozos de un kilobyte
con el número de bytes y de saltos de línea cacheado en cada nodo. Sobre un
[]byte plano, insertar en medio mueve el archivo entero en cada tecla y
traducir un desplazamiento a (línea, columna) obliga a recorrerlo contando
saltos; con el árbol, las dos cosas son un descenso en O(log n). Medido sobre un
texto de 900 KB: insertar cuesta 115 ns y pedir una línea, 400 ns.
Toda edición es una transacción: una lista de ediciones ordenadas y sin
solaparse que se aplican juntas. La inversa se calcula al aplicarla, así que
deshacer es la misma operación al revés y no un segundo camino que se
desincroniza. Deshacer agrupa por cercanía en el tiempo y en el espacio: lo
tecleado del tirón se deshace de una vez, y mover el cursor abre una operación
nueva. Multi-cursor sale gratis de ese modelo: son varias ediciones en la misma
transacción.
Un rope escrito a mano tiene decenas de casos límite, así que se verifica por
diferencia: cuatro mil ediciones aleatorias con semilla fija contra una cadena
normal, comprobando tras cada una que el texto coincide y que el árbol mantiene
sus invariantes. Esa prueba encontró dos fallos reales el día que se escribió.
Al guardar se escribe un archivo temporal y se renombra encima, que dentro de un
mismo sistema de archivos es atómico: o queda la versión vieja o queda la nueva,
nunca media. Escribir directamente sobre el original deja el archivo truncado si
el proceso muere a mitad, y ese es el peor fallo que puede tener un editor.
Sintaxis: lo que hay y lo que falta
El plan preveía Tree-sitter compilado a WebAssembly y ejecutado con wazero,
para no arrastrar cgo. Esa integración no está hecha, y la razón está medida:
Los grammars que se distribuyen ya compilados —los de web-tree-sitter— no son
módulos autónomos sino módulos laterales de emscripten. Llevan una sección
dylink, importan env.memory y exportan __wasm_apply_data_relocs,
__wasm_call_ctors y tree_sitter_<lenguaje>. Instanciar uno suelto con wazero
falla con «module[env] not instantiated». Usarlos exige escribir en Go el
enlazador dinámico de emscripten —bases de memoria y de tabla, reubicaciones,
trampolines invoke_*— y además cargar el runtime de Tree-sitter, que es otro
módulo del mismo tipo. La alternativa, compilar Tree-sitter y un grammar en un
único módulo WASI autónomo, necesita clang con wasi-sdk o emscripten.
Así que la decisión sigue en pie pero su coste real es mayor del que el plan
estimaba, y lo que queda por delante es un paso de construcción, no de editor.
Lo que hay mientras tanto es un lexer incremental por tabla, y resulta cubrir
casi todo lo que se ve. Guarda un byte por línea —si empieza dentro de un
comentario de bloque, de una cadena de varias líneas o de nada— y tokeniza solo
las líneas visibles, en cada fotograma: una pantalla cuesta 26 µs. Editar
invalida desde la línea tocada hacia abajo y nada más.
Con eso salen el resaltado de diez lenguajes, el emparejado de delimitadores
—ignorando los que caen dentro de cadenas y comentarios— y la expansión de la
selección. Sin árbol, «el siguiente nivel» se resuelve con el delimitador que
envuelve: en código real coincide casi siempre con lo que haría un árbol, y
cuando no coincide se queda corto en vez de largo, que es el error que no
molesta. Lo que no se puede hacer así, y por tanto no está, es plegar código,
indentar por contexto y el breadcrumb.
Cómo se prueba una interfaz sin verla
La capa ui no pinta: emite una lista plana de rectángulos y textos —una
escena— que el renderer traduce a cuadriláteros. Eso permite componer el
chasis entero, disponerlo para un tamaño de ventana concreto y comprobar dónde
cae cada cosa sin abrir una ventana ni tener tarjeta gráfica, y comparar
volcados de texto en lugar de píxeles cuando algo cambia.
La misma frontera está en la entrada: ui no importa platform y define sus
propios eventos, así que un widget se prueba entero pasándole clics y teclas
sintéticos. Las pruebas de internal/ui e internal/app no abren nada.
Cómo se prueba lo que no se puede ver
Las máquinas de integración continua no tienen tarjeta gráfica. gpu.FakeDevice
implementa el mismo interfaz guardando texturas en memoria y registrando los
cuadriláteros que recibe, aplicando exactamente los mismos descartes que el
backend real. Con él, el atlas de glifos y la disposición del texto se verifican
comparando la lista de cuadriláteros producida, que es una descripción exacta de
lo que se habría pintado.
Desarrollo
go build ./...
go vet ./...
go test ./...
Las convenciones de commits y de ramas están en
docs/COMMITS.md. Dos reglas que conviene conocer antes de
abrir un PR: cada commit compila y pasa las pruebas por separado —de eso depende
que git bisect siga sirviendo el día que aparezca una regresión—, y cada
dependencia nueva se justifica por escrito en el PR. Hoy hay una sola,
golang.org/x/image, y esa cifra es un objetivo, no una casualidad.
Licencia
FlowCode es MIT, la misma licencia que Code-OSS.
El proyecto toma la arquitectura conceptual de VS Code como referencia y la
reimplementa en Go: ni una línea de Code-OSS se ha copiado hasta ahora. Si en
algún momento se porta lógica concreta, la licencia MIT lo permite y el commit
citará la fuente.
Lo que no cubre esa licencia y por tanto no se usa aquí: la marca «Visual
Studio Code», el icono de producto y el marketplace. Los iconos Codicon son
CC-BY 4.0 y arrastran una atribución, así que los de FlowCode se dibujan aparte.