Concealing syntax

Hey,
Is it possible to hide some parts of the syntax when a line is not in focus?
For example:

  • display the string ‘lambda’ as ‘λ’ or
  • “!=” as “≠”

This is similar to Vim’s conceal feature.

I have had some luck doing this with replace widgets with but I am not able to remove the decoration when the line is in focus. Also, it feels like I’m not approaching it correct.

Any guidance will be greatly appreciated :pray:

Thank you so much

1 Like

Replacing widget decorations would be the way to go for this, yes. Make sure you skip the line with the selection head in it, and update (probably a full rebuild of the widgets inside the viewport is fine) when the selection, document, or viewport changes.

Thank you so much it worked! :smile:

Instead of skipping the complete line, I am skipping any range that overlaps with the selection range.

For anybody who finds this post, this is how I implemented the feature

class ConcealWidget extends WidgetType {

    constructor(readonly symbol: string) {
        super()
    }

    eq(other: ConcealWidget) {
        return (other.symbol == this.symbol)
    }

    toDOM() {
        let span = document.createElement("span")
        span.className = "cm-concealed-sym" /* Formatting to be taken care of*/
        span.textContent = this.symbol
        return span;
    }

    ignoreEvent() {
        return false
    }
}

function selectionAndRangeOverlap(selection: EditorSelection, rangeFrom:
    number, rangeTo: number) {
    return (selection.main.from <= rangeTo) &&
        (selection.main.to) >= rangeFrom;
}

function conceal(view: EditorView) {

    const concealMap = {
        "!=": "≠",
        "<=": "≤",
        ">=": "≥",
        // and so on...
    }

    let widgets: any = []
    for (let { from, to } of view.visibleRanges) {
        syntaxTree(view.state).iterate({
            from, to, enter: (type, from, to) => {
                const toSkip: Boolean = selectionAndRangeOverlap(
                    view.state.selection, from, to)
                    if (
                        (type.name == "CompareOp" || type.name = "LogicOp") 
                        && !toSkip
                    ) {
                        const s: string = view.state.doc.sliceString(from, to)
                        if ( !concealMap.hasOwnProperty(s) ) {
                            return
                        }
                        widgets.push(Decoration.replace({
                            widget: new ConcealWidget(),
                            inclusive: false,
                            block: false,
                        }).range(from, to))
                    }
            }
        })
    }
    return Decoration.set(widgets, true)
}


export const concealPlugin = ViewPlugin.fromClass(class {
    decorations: DecorationSet
    constructor(view: EditorView) {
        this.decorations = conceal(view)
    }
    update(update: ViewUpdate) {
        if (update.docChanged || update.viewportChanged || update.selectionSet)
            this.decorations = conceal(update.view)
    }
}, { decorations: v => v.decorations, })

Will check if the code has any issues but looks good so far

2 Likes

Nice work. A small typo, in concealPlugin, images should be conceal instead.

Thanks for the catch! I have updated the post

1 Like