Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple editor instances with tabs in React using @typefox/monaco-editor-react #850

Open
sten-code opened this issue Feb 10, 2025 · 5 comments

Comments

@sten-code
Copy link

I'm trying to create a tab system where each tab has it's own Monaco editor. I have seen examples where others have done this without this wrapper, but there are no examples in this repository that explain exactly how to do this.

The issue is that when I switch tabs, it still has the same editor and doesn't actually change to the content of the corresponding tab. This shouldn't really be possible since each tab has it's own instance.

This is what I have tried, it's a bit simplified compared to what I actually have:

import * as vscode from "vscode";
import { MonacoEditorReactComp } from "@typefox/monaco-editor-react";
import { LogLevel } from "@codingame/monaco-vscode-api";
import { useEffect, useRef, useState } from "react";
import { type WrapperConfig } from "monaco-editor-wrapper";
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from "vscode-ws-jsonrpc";
import { createUrl, Logger } from "monaco-languageclient/tools";
import { useWorkerFactory } from "monaco-languageclient/workerFactory";

export const Monaco = () => {
    const editorContainerRef = useRef<HTMLDivElement | null>(null);
    const [wrapperConfig, setWrapperConfig] = useState<WrapperConfig | null>(null);

    const webSocket = new WebSocket(createUrl({
        secured: false,
        host: "localhost",
        port: 8080,
        extraParams: {
            authorization: "UserAuth"
        }
    }));
    const socket = toSocket(webSocket);
    const reader = new WebSocketMessageReader(socket);
    const writer = new WebSocketMessageWriter(socket);

    useEffect(() => {
        if (editorContainerRef.current) {
            // Create the wrapperConfig only after the DOM element is available
            setWrapperConfig({
                $type: "extended",
                htmlContainer: editorContainerRef.current,
                logLevel: LogLevel.Debug,
                languageClientConfigs: {
                    luau: {
                        name: "Luau Language Server",
                        connection: {
                            options: {
                                $type: "WebSocketDirect",
                                webSocket
                            },
                            messageTransports: { reader, writer }
                        },
                        clientOptions: {
                            documentSelector: ["lua", "luau"],
                            workspaceFolder: {
                                index: 0,
                                name: "workspace",
                                uri: vscode.Uri.parse("/workspace")
                            },
                        }
                    }
                },
                vscodeApiConfig: {
                    userConfiguration: {
                        json: JSON.stringify({
                            "workbench.colorTheme": "Default Dark Modern",
                            "editor.guides.bracketPairsHorizontal": "active",
                            "editor.wordBasedSuggestions": "off",
                            "editor.experimental.asyncTokenization": true
                        })
                    }
                },
                editorAppConfig: {
                    codeResources: {
                        modified: {
                            text: "",
                            fileExt: "lua",
                        }
                    },
                    monacoWorkerFactory: (logger?: Logger) => {
                        useWorkerFactory({
                            workerLoaders: {
                                TextEditorWorker: () => new Worker(
                                    new URL("@codingame/monaco-vscode-editor-api/esm/vs/editor/editor.worker.js", import.meta.url),
                                    { type: "module" }
                                ),
                                TextMateWorker: () => new Worker(
                                    new URL("@codingame/monaco-vscode-textmate-service-override/worker", import.meta.url),
                                    { type: "module" }
                                ),
                                // these are other possible workers not configured by default
                                OutputLinkDetectionWorker: undefined,
                                LanguageDetectionWorker: undefined,
                                NotebookEditorWorker: undefined,
                                LocalFileSearchWorker: undefined
                            },
                            logger
                        });
                    }
                }
                // All the other configurations...
            });
        }
    }, []);


    return (
        <div ref={editorContainerRef} className="w-full h-full flex-grow bg-zinc-950 text-zinc-100">
            {wrapperConfig && (
                <MonacoEditorReactComp
                    wrapperConfig={wrapperConfig}
                    className="h-full"
                />
            )}
        </div>
    );
};

type Tab = {
    id: number,
    title: string,
    editor: ReturnType<typeof Monaco>,
};

export const TabSystem = () => {
    const [tabs, setTabs] = useState<Tab[]>([
        {
            id: 1,
            title: "Untitled-1",
            editor: <Monaco />
        }
    ]);
    const [activeTab, setActiveTab] = useState(1);

    const addTab = () => {
        let newTabId = 1;
        const ids = tabs.map(tab => tab.id);
        while (ids.includes(newTabId))
            newTabId++;

        const newTab: Tab = { id: newTabId, title: `Untitled-${newTabId}`, editor: <Monaco /> };
        setTabs([...tabs, newTab]);
        setActiveTab(newTabId);
    };

    const closeTab = (id: number) => {
        const newTabs = tabs.filter((tab) => tab.id !== id);
        setTabs(newTabs);
        if (activeTab === id && newTabs.length) {
            setActiveTab(newTabs[0].id);
        } else if (!newTabs.length) {
            setActiveTab(0);
        }
    };

    return (
        <div className="flex flex-col h-full select-none">
            {/* Tab Bar */}
            <div className="flex">
                {tabs.map((tab) => (
                    <div key={tab.id} onClick={() => setActiveTab(tab.id)} className={`cursor-pointer ${activeTab === tab.id ? "bg-zinc-800 border-green-500" : "border-transparent"}`}>
                        <span className="pl-1">{tab.title}</span>
                        <button
                            className="px-2"
                            onClick={(e) => {
                                e.stopPropagation();
                                closeTab(tab.id);
                            }}>
                            X
                        </button>
                    </div>
                ))}
                <button onClick={addTab} className="pl-3">+</button>
            </div>

            {/* Tab Content */}
            {tabs.length > 0 ? tabs.find((tab) => tab.id === activeTab)?.editor : <></>}
        </div>
    );
}

Thanks in advance to anyone who might be able to help.

@kaisalmen
Copy link
Collaborator

Hi @sten-code we have something like that without the need for extra code:
https://typefox.github.io/monaco-languageclient/ghp_appPlayground.html (source)

monaco-vscode-api provides the possibility to build "partial" VSCode applications. In the above example the "ViewsService" is used instead of the "EditorService" that is used in the "simpler" examples. When the view service is active the monaco-editor-wrapper does not instantiate its own monaco-editor, but the view service does that. You control use such calls to to open or show files in the editor:
https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/appPlayground/reactMain.tsx#L27 => https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/appPlayground/common.ts#L18-L25
The views service brings full layout support (dragging/re-arranging) as you know from VSCode.

@sten-code
Copy link
Author

I have tested that example pretty thoroughly, but I couldn't find a way to style the tabs myself. I also need the ability to extract the text from a specific tab. The best way to do any of these is to make my own tab system. I would much prefer to use the view service, but there just isn't any documentation on it and I don't want to spend all this time trying to reverse engineer something that I don't know with 100% certainty that it supports everything in my use case. There are only some vague examples that only cover very specific use cases.

@kaisalmen
Copy link
Collaborator

@sten-code what you want to achieve goes beyond what we can offer/support here.
Regarding the api / capabilities you can check VSCode documentation. monaco-vscode-api comes close to be a modularized VSCode web. You can use all features and styling possibilities VSCode offers.

@sten-code
Copy link
Author

Alright, thanks for letting me know. I will implement a custom solution without using the MonacoEditorReactComp wrapper. I will post my solution here when I implement it in the case anyone else has the same requirements as I do.

@kaisalmen
Copy link
Collaborator

@sten-code thank you. If you have also ideas for enhancements, please feel free to open an enhancement issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants