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

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

1 Like

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

Thanks for the catch! I have updated the post

1 Like