Skip to content

kshetline/ligatures-limited

Repository files navigation

Ligatures Limited

Code ligatures only where you want them, not where you don’t

I enjoy using ligature fonts for coding so that symbols like arrows (fat arrow no ligature) look like arrows (fat arrow ligature) and does-not-equal signs (not-equal no ligature) look like the real thing from math class (not-equal ligature). The problem is that, even with the contextual smarts built into ligature fonts like Fira Code, ligatures have a knack of popping up where you don’t want them.

Without ligature suppression


With ligature suppression

Ligatures Limited is designed to make the rendering of ligatures more context-dependent.

In the top image, you can see ligatures that don’t make sense where they are — in the regex, where three different characters are being checked (not one big two-headed arrow), and the oddly formatted asterisks in the message string.

The image below shows how those out-of-place ligatures are suppressed by Ligatures Limited, replaced with individual characters, while the triple-equals and double-ampersand ligatures are retained.

With the default settings for this extension ligatures are only rendered in three contexts: operators, punctuation, and comment markers, plus three special cases: 0x ligature when followed by a hexadecimal digit in a numeric context, rendered as (if supported by your chosen font), and a similar pattern, 0o7 for octal numbers, and 0b1 for binary numbers as well.

Also by default, the special case of x between any two decimal digits is suppressed, which would render as (for example) 2×4. If you want to see these ligatures rendered in any or all contexts, (provided, of course, that your chosen font defines them), you must expressly enable them.

Ligatures can also be suppressed (with individual characters shown instead) at the current insert cursor position (this is the default setting), for all of the current line being edited, or within all of the current text selection. This feature can be turned off entirely as well, so that the cursor position or text selection have no effect.

The ligatures ff, fi, fl, ffi, and ffl are by default rendered in all contexts. You can change your configuration suppress these ligatures, which you may wish to do when using a font which, for instance, renders fi within the width of a single character, instead of as two characters (see Disregarded Ligatures).

While the default settings should meet many users’ needs, custom settings are available to control which syntactical contexts are handled, and which particular ligatures are displayed or suppressed. Settings can be global or on a per-language basis (say, different rules for JavaScript than for Python, if you wish).

Prerequisites

In order to take advantage of Ligatures Limited, you must first have ligatures enabled in Visual Studio Code, and have a ligature font like Fira Code installed and selected (click here for instructions).

Ligatures handled by Ligatures Limited

As rendered using Fira Code:
Supported ligatures

Fira Code again, but with all ligatures suppressed
Supported ligatures as individual characters

0xF ligature represents 0x followed by any hexadecimal digit, and 9x9 ligature represents x surrounded by any decimal digits. You can specify your own additional ligatures if you need Ligatures Limited to be aware of them.

The bottom row are indefinite-width ligatures. When specifying these ligatures, three equals signs (=), dashes (-), or tildes (~) represent three or more of those signs, except for the first three ligatures, where four equals signs represent four or more. Four or more dashes, without a leading < or trailing >, is also a special case, represented by four dashes.

#### is another indefinite-width ligature, representing four or more number signs (#) in a row.

Commands

  • cycleLigatureDebug

    This allows you to see where Ligatures Limited is acting upon the styling of your code. Ligatures which have been disabled will appear in red with an inverse background, and ligatures which have been enabled will appear in green with an inverse background. Each activation of this command cycles through the three states: debug mode fully enabled, fully disabled, and debug highlighting being up to your custom settings (which can focus debugging on particular languages and/or contexts).

  • cycleSelectionMode

    Each activation of this command cycles through five different states for cursor position/text selection dependent rendering of ligatures: cursor, line, selection, off, and by-settings.

  • toggleLigatureSuppression

    This allows you to disable and reënable Ligatures Limited’s modification of ligatures. While disabled, a faint blue tint is added to the background of your code to indicate the disabled status.

  • toggleScopeHover

    This command, available as a right-click contextual menu in the text editor, turns extra hover information on and off for your code. This information shows you a Ligatures Limited token category (described below) and TextMate scope information. This information is useful for deciding upon custom settings you might wish to apply.

Custom settings

In lieu of a long-winded explanation of custom settings, for now I’ll just provide a link to the JSON schema, embedded in this extension’s package.json file, and add a few explanatory notes on top of that: https://github.com/kshetline/ligatures-limited/blob/master/package.json

Contexts

A context is either a simplified token category, from the table below, or a TextMate grammar scope, such as meta.paragraph or storage.type. Ligatures Limited works by finding possible ligature sequences in your code, using TextMate to find the types of language tokens within which these ligatures are embedded, and then applying the rules from your settings.

attribute_name  attribute_value comment         comment_marker  constant
function        identifier      invalid         keyword         number
operator        other           property_name   property_value  punctuation
regexp          scope           string          tag             text
type            variable

The rules for how Ligatures Limited handles ligatures can be summed up by the way you answer the question: “Which ligatures do I want to see, and which do I want suppressed, in which contexts and in which languages?”

When you specify a TextMate scope, only the last item listed in the hover information for that scope applies. For example:

TextMate scope

In the case above you would specify meta.paragraph.markdown to refer to this context. This can be shortened to meta.paragraph, and that would then apply to meta.paragraph in any language.

By-language and by-context settings

A language specifier can be a comma-separated list of languages, and a context specifier for by-context rules can be a comma-separated list of contexts.

Rules for a language can be inherited from rules for another language by using the inherit setting.

Rules hierarchy

  • Built-in defaults
  • The global user-provided context list, disregarded ligature list, and ligature list
  • Global by-context ligature lists
  • When language inheritance is used, the rules of the inherited language
  • Language-specific context lists and ligature lists.
  • Language-specific by-context ligature lists.

Context lists

Ligatures are not rendered unless they appear inside an explicitly listed context. These contexts are by default very limited since the value of most ligatures is their use as operators and punctuation. To add or remove contexts from the inherited rules, simply list them in a space-separated string like this:

"regexp comment.line.double-slash.ts - comment.block.ts"

Any contexts following the - sign are removed from the inherited context list. The above example enables ligatures in regular expressions and line comments, but disables them in block comments.

Ligature lists

These work much like context lists, except that an explicit + sign is needed to enable ligatures. Ligatures listed before either a plus or minus sign are simply added to the list of ligatures for which any action will be taken at all.

"+ <=> ==> - ## ###"

This example adds the first two ligatures for rendering, and causes the last two to be suppressed.

Ligature lists can also “start fresh”, clearing inherited ligatures. A leading 0 suppresses all ligatures, after which you can list just the ligatures you want to see rendered.

A leading X enables all ligatures, after which you can list just the ligatures you want to suppress.

Disregarded ligatures

The disregardedLigatures setting, available only at the global level, is a space-separated string of ligatures for Ligatures Limited to entirely ignore. If your font wants to render these ligatures, it will render them regardless of context.

Ligatures Limited disregards the following ligatures by default: ff fi fl ffi ffl

Ligatures you specify for disregardedLigatures will be added to the above default list.

If you need to undisregard (for lack of a better word) any of these by-default disregarded ligatures, simply using them in any ligature list will bring them back into consideration for all ligature processing.

(Please note that Ligatures Limited can’t force ligatures to be rendered that aren’t provided by your selected font in the first place.)

File size limits

To prevent Visual Studio Code from getting bogged down when handling very large files (way too large to be normal, proper source code files), there are default limits on the size of files which will be handled: no more than 20K lines, and no more than 35M characters. These limits can be modified using "ligaturesLimited.maxLines" and "ligaturesLimited.maxFileSize". If you set these values to zero, size checking is disabled.

Sample configuration

{
  "ligaturesLimited.languages": {
    "css, javascript": {
      "ligaturesByContext": {
        "comment.block": {
          "ligatures": "0 www"
        }
      }
    },
    "html": {
      "ligaturesByContext": {
        "attribute_value": {
          "ligatures": "0 www"
        }
      }
    },
    "typescript": {
      "inherit": "javascript",
      "ligatures": "- !="
    },
    "markdown": true,
    "plaintext, json, jsonc, json5": false
  }
}

This configuration enables www ligatures, and only www ligatures, within block comments for CSS and JavaScript.

TypeScript inherits the above behavior, and also disables the != ligature in all contexts.

For HTML, the www ligature is rendered only inside attribute values.

Markdown has all ligatures enabled, and plain text and various forms of JSON have all ligatures disabled.

Known Issues

  • Occasionally, when working with large files, a console warning UNRESPONSIVE extension host may appear, but despite the wording of the message VSCode remains responsive anyway.

Credits

This extension is partially a spin-off of two other projects, vscode-Disable-Ligatures by CoenraadS, and scope-info, by C. J. Bell.