uiwjs / react-codemirror

CodeMirror 6 component for React. @codemirror https://uiwjs.github.io/react-codemirror/
https://uiwjs.github.io/react-codemirror/
MIT License
1.61k stars 128 forks source link

Use stream language to define a simple language #287

Closed arthurmmedeiros closed 2 years ago

arthurmmedeiros commented 2 years ago

Hey,

I've trying to use react-codemirror to build a very simple text editor with some custom keywords. I basically want to let the user define a naming convention for downloaded files. The user should be able to type free text and use some pre-defined tags that would be converted in a file name later.

It'd be something like this: [Name]-[LastName]-[DateCreated]-professional.pdf

[Name], [LastName], and [DateCreated] would be keywords that are highlighted in the text area when used.

I can't manage to build a custom language using stream parser though. I went through the docs but I can't still make it work.

Any tips on things I could try here?

Thanks a lot!!

import { useState, React } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
import { StreamLanguage, StringStream } from '@codemirror/stream-parser';
import { tags, HighlightStyle } from '@codemirror/highlight';
import { EditorView, ViewUpdate } from '@codemirror/view';

export const CodeMirrorTest = () => {
    const lang = StreamLanguage.define({
        token: (stream: StringStream, state: any) => {
            // test
            if (stream.string === '[') {
                stream.skipToEnd();
            }
            return null;
        }
    });

    const myCompletions = (context: CompletionContext) => {
        const word = context.matchBefore(/\w*/);

        if (!word || (word.from === word.to && !context.explicit)) {
            return null;
        }
        return {
            from: word.from,
            options: [
                { label: '[Name]' },
                { label: '[LasName]' },
                { label: '[DateCreated]' }
            ]
        };
    };

    const theme = EditorView.theme({
        // /* Disable CodeMirror's active line highlighting. */
        '& .cm-activeLineGutter, & .cm-activeLine': {
            backgroundColor: 'transparent !important'
        },
        // /* Disable CodeMirror's focused editor outline. */
        '&.cm-editor.cm-focused': {
            outline: 'none'
        },
        '.cm-gutters': {
            display: 'none !important'
        },
        '.cm-tooltip-autocomplete': {
            left: '20px !important',
            top: '100px !important'
        }
    }, { });

    const highlightStyle = HighlightStyle.define([
        { tag: tags.keyword, color: 'red' }
    ]);

    const extensions = [
        lang,
        theme,
        autocompletion({
            override: [myCompletions],
            activateOnTyping: true
        }),
        highlightStyle
    ];

    return (
        <CodeMirror
            value=''
            extensions={extensions}
            className='form-control'
            onChange={(value: string, viewUpdate: ViewUpdate) => {
                console.log(viewUpdate);
            }}
        />

    );
};
jaywcjlove commented 2 years ago

you can refer to the example lang-package to create a extensions for yourself.

@arthurmmedeiros Below are a few examples you can refer to:

arthurmmedeiros commented 2 years ago

I appreciate the quick response @jaywcjlove

I took a look at the lang-package example and it seems like https://codemirror.net/6/docs/ref/#stream-parser is the way to go.

That requires creating an extension like you mentioned and passing a function token(stream, state) to it. I guess that's where I'll have to control the component.

I see that if I return the name of a tag then it will highlight the string. However, it checks the entire string, so I can't keep the keywords highlighted.

    const lang = StreamLanguage.define({
        token: (stream: StringStream, state: boolean) => {
            if (stream.string === 'test') {
                stream.skipToEnd();

                return 'keyword';
            }

            stream.next();
            return null;
        },
        tokenTable: {
            Name: tags.keyword
        }
    });

Is there a way to keep the keywords highlighted after moving to the next chars?

jaywcjlove commented 2 years ago

@arthurmmedeiros You need to look at the official documentation, I have no experience with this.

arthurmmedeiros commented 2 years ago

I think I found a way to output what I needed. I'll post here in case anyone is interested:

    const allowedKeywords = [
        'Name',
        'LastName',
        'Date'
    ];

    const getAllowedKeywordsExpr = () => new RegExp(`\\[\\b(?:${allowedKeywords.map(p => p).join('|')})\\b\\]`);

    const lang = StreamLanguage.define({
        token: (stream: StringStream, state: any) => {
            if (stream.match(getAllowedKeywordsExpr())) {
                return 'keyword';
            }

            stream.next();
            return null;
        }
    });
dialYun commented 1 year ago

@arthurmmedeiros Can you share the code for custom keywords? Thanks!