Search code examples
javascriptmonaco-editor

How do I add javascript autocomplete to markdown mode of monaco editor?


I want to be able to use the javascript-autocomplete of the monaco editor inside of markdown documents as well, because markdown documents can contain code snippets of type javascript:

```javascript
window.blah
```

I know that I could register a custom CompletionItemProvider for markdown, but I want to use the javascript one already provided by monaco. I have not found a way to receive their provider however.

Javascript syntax highlighting already works in markdown code blocks.

My idea was to somehow get their provider using something like this:

(await monaco.languages.typescript.getJavaScriptWorker()).something

For that to work I'd have to register javascript on the editor however. Even if I do that, I don't seem to be able to find anything on their worker that could help me.

While there's a method to register a CompletionItemProvider (https://microsoft.github.io/monaco-editor/api/modules/monaco.languages.html#registercompletionitemprovider), I couldn't find any that could help me get a registered provider.

How can I use the javascript-autocomplete in markdown code blocks as well?


Solution

  • You are on the right track. Here's how I use typescript/javascript worker APIs to provide their completions in a mixed language environment:

                    return new Promise((resolve) => {
                        const workerPromise = (context.language === "javascript")
                            ? languages.typescript.getJavaScriptWorker()
                            : languages.typescript.getTypeScriptWorker();
                        void workerPromise.then((worker: (...uris: Uri[]) => Promise<TypescriptWorker>) => {
                            void worker(model.uri).then((service) => {
                                const offset = model.getOffsetAt(localPosition);
                                const promise = service.getCompletionsAtPosition(model.uri.toString(), offset);
    
                                void promise.then((completions: CompletionInfo | undefined) => {
                                    if (completions) {
                                        const info = model.getWordUntilPosition(localPosition);
                                        const replaceRange: IRange = {
                                            startLineNumber: position.lineNumber,
                                            startColumn: info.startColumn,
                                            endLineNumber: position.lineNumber,
                                            endColumn: info.endColumn,
                                        };
    
                                        resolve({
                                            incomplete: false,
                                            suggestions: completions.entries.map((entry) => ({
                                                label: entry.name,
                                                kind: this.convertKind(entry.kind),
                                                range: replaceRange,
                                                insertText: entry.insertText || entry.name,
                                            } as CompletionItem)),
                                        });
                                    } else {
                                        resolve({ incomplete: false, suggestions: [] });
                                    }
                                });
                            });
                        });
                    });
    

    Important here is that you create submodels with only the content of the parts in a single language (see Monaco.createModel()) and use that to invoke the completion items code.

    In my app I split the content of an editor into blocks (each covering only full lines and which have a specific language attached to them). So they have a start line and an end line. With this information I can create sub models for each block:

        /**
         * @returns A local model which contains only the text of this block. The caller must dispose of it!
         */
        public get model(): Monaco.ITextModel {
            const editorModel = super.model; // Model for the entire editor content.
    
            if (!this.internalModel || this.internalModel.isDisposed()) {
                const localModel = Monaco.createModel("", this.language);
                // Do other required preparations of the local model, if needed.
                this.internalModel = localModel;
            }
    
            if (editorModel && this.modelNeedsUpdate) {
                this.modelNeedsUpdate = false;
                this.internalModel.setValue(editorModel.getValueInRange(
                    {
                        startLineNumber: this.startLine,
                        startColumn: 1,
                        endLineNumber: this.endLine,
                        endColumn: editorModel.getLineMaxColumn(this.endLine),
                    }, Monaco.EndOfLinePreference.LF),
                );
            }
    
            return this.internalModel;
        }