Giter VIP home page Giter VIP logo

editor's Introduction

At the moment, this is mostly a proof-of-concept, and there are few user-facing features. The program has:

  • Basic modal editing: i to enter insert mode, Esc to leave it

  • Basic navigation with the arrow keys

  • Local seeking: with s (forward) and Shift + s (backward) followed by a 2 character sequence to search for

  • Syntax highlighting: currently, there is a flatparse-based syntax highlighter for Haskell source code. However, there is no filetype detection as yet, so the parser will be applied to any file you open.

The program requires a file as an argument, and can be passed a small number of optional arguments:

  • --render INTxINT specifies the pixel dimensions of the render canvas as <height>x<width>. On a device with memory or performance constraints (e.g. if you have integrated graphics), you may wish to set this to a lower value. On a high DPI display (e.g. most modern laptops), you can likely decrease the render resolution significantly without any perceptible impact in quality.

  • --size FLOAT and --dpi FLOAT can be used to specify the font size in points and display DPI respectively. Internally, these are only ever used in tandem -- the only reason you may wish to set the DPI is to ensure that the font size (which is a physical measure) can be properly converted to a pixel size.

  • --font FILE can be used to provide a TrueType (.ttf) font source file. Note that the TTF parser is not currently complete, and likely will not be for some time. I have tested a small number of fixed-width and proportional fonts without issue, but (in particular) any font that does not provide a format 4 cmap encoding table will cause the program to crash immediately.

Building

To build from source, you can simply clone the repository recursively with

git clone --recurse-submodules https://github.com/ssddq/editor

and build with cabal build. If you have Nix installed, nix develop should provide a shell with the required dependencies.

A prebuilt binary for x86-64 Linux distributions is also automatically generated at https://github.com/ssddq/editor-release, which you can run with

nix run github:ssddq/editor-release

This is likely to require additional steps if you are not on NixOS; please visit that repository for more instructions.

Technical features

The technical points that may be of interest:

  • Files are never loaded into memory: the file state is stored as (essentially) a strict IntMap of changes -- insertions or deletions -- keyed on byte positions in the underlying file. Line positions are stored in (essentially) a red-black tree of arrays.

  • Every frame, the relevant portion of the file is read from the disk and merged with the editor state into (essentially) a Stream (Of Char) IO (). The stream is terminated as soon as the renderer has consumed enough characters to fill the given render area.

  • Syntax highlighting is provided by streaming the results of a stateful flatparse parser with signature state -> Parser e (Color, state). The parser is fed chunks of bytestrings with some minimal length (currently 512), and the results are streamed as they are produced.

    Parsers manage their own internal state (which may be trivial), and can request a limited amount of context (currently 512 bytes) so that they can begin parsing earlier than the start of the render area.

  • The file is scanned for newline characters eagerly and asynchronously on startup. Operations that depend on the array of newline positions (e.g. line traversal, inserting a newline character) only block until enough of the array has been generated that they can proceed. This means that there is no perceptible difference in opening and editing a 1 KB file and opening and editing a 1 GB file, unless you (immediately) jump to the last line in a large file.

    You can generate a 1 GB text file with:

    base64 /dev/urandom | head -c 1000000000 > sample.txt
    

Currently only UTF-8 encoded text files are supported; attempting to open a binary file will likely terminate the program immediately.

Warnings

This is very much not safe Haskell.

While I'm not aware of any serious issues in the filebuffer implementation, there is no built-in way to save changes to a file as a precaution. The internal file state is complicated -- to say the least -- and it's still entirely possible that there are subtle issues (e.g. adjacent characters get transposed) and only occur in rare cases (e.g. at the end of the file once the depth of the tree exceeds 17).

If you like to live dangerously, you can recompile it and enable saving with little effort: in main/Main.hs, simply take the stream used for rendering and provide a keybind that writes it to a handle instead.

Performance

Since this is in active development and many things (e.g. the exact content of the file being opened) can have dramatic impacts on performance, you should only use the numbers below as a general indication of performance.

On a desktop computer with an AMD 7900X and RX 6750 XT:

  • Given a 1 GB file with 70-80 characters per line, scanning for newlines takes <2 seconds on my device to produce an array just over ~100MB. Note, however, that startup is still instant: 2 seconds is only the time it takes for the scan to complete (asynchronously).

    On a 100 MB file, it completes in < 0.4s. These seemed measurably slower than e.g. helix in a terminal. However, on extremely large files (1 GB) both memory usage and the initial start up time (i.e. perceived latency) are considerably lower.

  • Frame times depend on the file, but hover somewhere around 1 ms on my desktop. On my far less capable Framework laptop, frame times were around 7 ms. These should be mostly impercetible.

    Note that frame rates are hard-limited by the poll rate of SDL.waitEvent, which is roughly ~40 ms on my device. Frames are only ever rendered in response to SDL events (see: main/Main.hs), and you should expect your device to be idle for the most part.

  • Syntax highlighting incurs a variable performance penalty, but it can be as low as 10% (i.e. frame times only increase by 10%). This seems to be largely due to flatparse generating extremely efficient parsers! Requesting the maximum context currently allowed (512 bytes) on the current Haskell parser brought the penalty up to only 20%.

    The main source of performance degradation here (other than excessive backtracking) is breaking up bytestrings: parsers that can color whole words at a time have a dramatically smaller performance impact than parsers that color letter-by-letter.

Project layout

There are two main logical components: the renderer and the filebuffer.

The filebuffer is almost entirely contained in the filebuffer package, which provides the main data structures holding the editor state along with functions to modify and traverse it, and to stream the underlying file with the edits applied.

The renderer spans a few packages

  • vma generates bindings to the Vulkan Memory Allocator library.

  • shaders generates SPIR-V bytecode for the shaders used by the renderer, and exports these as Haskell bytestrings.

  • parse-font contains a (not yet complete) parser for .ttf font files.

  • syntax contains a stateful flatparse syntax highlighter for Haskell.

  • renderer sets up the Vulkan render pipeline, and generates indirect draw calls from (essentially) a Stream (Of Char) IO ().

The common package provides shared interfaces for some types across packages, along with shared utility functions. The tools package provides the preprocessor used in renderer (see below), as well as some formatting tools.

Unsurprisingly, the executable is generated from main/Main.hs.

Syntax

There are some non-standard things about the code -- and the renderer package in particular -- that are worth mentioning.

Type parameter records. Nearly all primary modules in renderer, as well as main, are run through the preprocessor provided by tools/preprocessor. This contains a preprocessor and a Template Haskell library that parses type parameter records, transforming

f :: Vk { parameter1 = I, parameter2 = X }
  -> ...

into

f :: forall ... . Vk ... I ... X ...
  -> ...

The types I and X are substituted for the type parameters parameter1 and parameter2 named in the type declaration

data Vk ... parameter1 ... parameter2 ...

and the remaining type parameters are quantified over. This transformation is completely mechanical, though Template Haskell is used to export (and then retrieve) the type parameter names used in the original declaration.

The type of nearly every field of Vk is expressed in terms of a type family, which uses the flag I or X in the associated type parameter to determine whether the field has its 'base' type, or is empty and has type (). For fields whose 'base' type f has kind * -> *, the flag A b is used instead, and the resulting field has type f b.

Every function that is used directly in the main pipeline of the program takes the form

f ::    Vk { ... }
  -> IO Vk { ... }

and these are composed with (>>=).

Function application. Two non-standard operators for function application appear quite often -- especially when invoking Vulkan commands or creating Vulkan structs -- namely:

  • (|-) which is left-associative function application with a precedence of 3, and

  • (|*) which is left-associative function application with a precedence of 2.

If you're familiar with BlockArguments, these are used the same way do sometimes is for non-monadic arguments.

editor's People

Contributors

ssddq avatar

Stargazers

 avatar Adrian Sieber avatar Miao ZhiCheng avatar Andrejs Agejevs avatar Clemens Schmid avatar Hiromi Ishii avatar Kevin Brubeck Unhammer avatar Rodrigo Mesquita avatar  avatar Daniel Díaz Carrete avatar

Watchers

 avatar

editor's Issues

Add text color support

This requires:

  • Allowing the Stream to carry color information.
  • Updating the renderer to write color values as part of the instance data for each draw call.
  • Updating the shaders to use these colors.

Fullscreen draws degrade performance

In testing following 4316880, using fullscreen draw calls to

  • clear attachments
  • write to color attachment 3 in the render pass 0 after winding-number culling
  • draw the background color

seems to have dramatically negative impacts on frame times. These are likely to be impercetible, but I'd still rather avoid it.

Fortunately, this should be fairly straightforward. The resolve calls should simply use the same vertex shader and input buffers as the initial draw calls, and the background color should be set with a clear value in mkRenderPassBeginInfo.

Add support for coloring background regions

While b48d641 implemented text coloring (completing #3), background coloring is ironically quite a bit more complicated

It would be nice if we could simply write draw calls for whole-window rectangles to the command buffer; unfortunately, the current render pipeline makes this fundamentally impossible. The problem is that, in the first render pass, any glyph that overlaps with the rectangle (which in this case is every single rendered glyph) will overwrite the background color in the second color attachment. Consequently, when calculating the winding number to discard fragments, the background color in the second attachment will have already been overwritten with the text color -- which makes selecting the background color there impossible. This is before even factoring in the winding number issues involved with drawing glyphs that have different basepoints.

The likely solution is to simply repeat the subpasses in the first renderpass, clearing the first two color attachments but saving the third. Ideally, this would be done 4 times, for a total of 8 subpasses: background, text, overlay background, overlay text. This should be reasonably cheap, though I'd want to measure the performance impact to be sure.

Consider allowing the loading of multiple fonts

It wouldn't be terribly difficult to load multiple fonts at the same time -- we just need to carry a new vertex/index buffer for each one.

The biggest question is: what would be the use case?

  • It would be comparatively trivial to allow e.g. overlays or titlebars to use different fonts, but I'm not sure how much appeal this would have in itself.

  • It's much less clear how to deal with using multiple fonts in the same render area. One could imagine a common use case would be to add a bold variant of a fixed-width font. Should additional fonts be resized so that their advance matches the advance of the 'default' font? Is there a sensible analogue of this for proportional fonts? What should happen if fixed-width and proportional fonts are mixed?

    It would be somewhat annoying to have to treat fixed-width and proportional fonts differently (or to e.g. branch into cases depending on which came first); I'd really like a solution that makes sense for all combinations.

Use an unboxed vector to hold search skip values

Currently, the skip values for a target search string are stored in a strict IntMap because I (incorrectly) estimated that I'd need 8 bytes per unicode code point to store them in an array (just shy of 9 MB in total). On reflection, I think this is false: skips depend on bytes not code points, and I should require only 2048 bytes for the array.

Replacing the strict IntMap with an unboxed vector should speed up searching. This is a relatively easy change, but I'd also like to measure the performance impact this really has.

Add virtual linebreaks for extremely long lines in large files

Since line actions will block until (at the very least) the first linebreak is reached, a large file with no linebreaks will freeze on load (until the initial file scan is complete).

A simple workaround would be to insert a virtual line on every chunk boundary, if the chunk did not contain a newline character. This is a very simple fix, but there are some open questions about how to do this:

  • Should there be some visual distinction between a virtual and an actual linebreak? (Probably, yes.)

  • Should virtual linebreaks be discarded if an actual linebreak is inserted into the chunk? (Possibly? I have to think about this.)

Track edit history

In principle, it's easy to store the filebuffer state in a vector. Until the changes are saved to the underlying file, swapping out the current filebuffer state for some prior state is completely trivial.

However, once changes are saved, all prior states become invalid. Moreover, it's almost certainly a bad idea to try and recalculate them relative to the current file.

My tentative plan is to make all filebuffer state relative to the initial version of the file, and calculate (and record) an inverse whenever the file is saved.

Saving a file when the filebuffer has state B should generate a global B-1 that would get you back to the initial version of the file when streamed with the new on-disk file. Any filebuffer state A at any point in history can then be composed with B-1 so that AB-1 is the editor state relative to the current version of the file.

Details (e.g. whether this will be possible to implement efficiently) still need to be worked out.

Add command-line argument handling

Some of the global settings (e.g. render size, font size) should probably be taken as command-line arguments.

Eventually I'd like to have a more developed configuration solution (that doesn't involve rebuilding from source), but this could be a quick, low-effort workaround for now.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.