flowcode

module
v0.0.0-...-4916209 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: MIT

README

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.

Plataformas

Plataforma Compila Se ejecuta
Windows amd64 / arm64
Linux amd64 / arm64 No, backend de ventana pendiente
macOS amd64 / arm64 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.

Directories

Path Synopsis
cmd
flowcode command
Command flowcode es el editor.
Command flowcode es el editor.
internal
app
Package app compone las capas de FlowCode en un programa ejecutable.
Package app compone las capas de FlowCode en un programa ejecutable.
buffer
Package buffer contiene el modelo de texto de FlowCode.
Package buffer contiene el modelo de texto de FlowCode.
clock
Package clock proporciona un reloj monótono de alta resolución.
Package clock proporciona un reloj monótono de alta resolución.
command
Package command es el registro de comandos y el motor de atajos de FlowCode.
Package command es el registro de comandos y el motor de atajos de FlowCode.
editor
Package editor es la vista de código: lo que se ve y con lo que se interactúa cuando se edita un archivo.
Package editor es la vista de código: lo que se ve y con lo que se interactúa cuando se edita un archivo.
geom
Package geom define los tipos geométricos y de color compartidos por todas las capas de FlowCode.
Package geom define los tipos geométricos y de color compartidos por todas las capas de FlowCode.
gpu
Package gpu define la interfaz de dibujo de FlowCode y la aísla del API gráfico concreto que haya debajo.
Package gpu define la interfaz de dibujo de FlowCode y la aísla del API gráfico concreto que haya debajo.
gpu/gl
Package gl implementa el backend de gpu.Device sobre OpenGL 3.3 core.
Package gl implementa el backend de gpu.Device sobre OpenGL 3.3 core.
platform
Package platform aísla todo lo que depende del sistema operativo: creación de ventana, contexto OpenGL, ciclo de eventos, teclado, ratón y DPI.
Package platform aísla todo lo que depende del sistema operativo: creación de ventana, contexto OpenGL, ciclo de eventos, teclado, ratón y DPI.
syntax
Package syntax da sentido al texto: qué es palabra clave, qué es cadena, qué es comentario, y qué paréntesis cierra a cuál.
Package syntax da sentido al texto: qué es palabra clave, qué es cadena, qué es comentario, y qué paréntesis cierra a cuál.
text
Package text es el motor tipográfico de FlowCode: carga fuentes, rasteriza glifos, los cachea en un atlas de GPU y convierte cadenas en cuadriláteros listos para dibujar.
Package text es el motor tipográfico de FlowCode: carga fuentes, rasteriza glifos, los cachea en un atlas de GPU y convierte cadenas en cuadriláteros listos para dibujar.
ui
Package ui es el framework de interfaz de FlowCode: un árbol de elementos con layout de dos fases, eventos con foco y hit-testing, y una escena de primitivas que el renderer consume.
Package ui es el framework de interfaz de FlowCode: un árbol de elementos con layout de dos fases, eventos con foco y hit-testing, y una escena de primitivas que el renderer consume.
workspace
Package workspace es el proyecto: qué archivos hay, cómo encontrarlos y cómo buscar dentro de ellos.
Package workspace es el proyecto: qué archivos hay, cómo encontrarlos y cómo buscar dentro de ellos.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL