├── .github ├── FUNDING.yml └── workflows │ ├── publish.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── comfy ├── codicon.css ├── codicon.ttf ├── danbooru.csv ├── editor.worker.mjs ├── extra-quality-tags.csv ├── main.bundle.js └── main.bundle.js.LICENSE.txt ├── csv ├── danbooru.csv └── extra-quality-tags.csv ├── editor_check.html ├── extension.json ├── javascript ├── codicon.ttf ├── editor.worker.js ├── main.bundle.js └── main.bundle.js.LICENSE.txt ├── package-lock.json ├── package.json ├── pyproject.toml ├── scripts └── api.py ├── settings └── .gitkeep ├── setup.cfg ├── snippets.py ├── snippets └── .gitkeep ├── src ├── @types │ ├── gradio.d.ts │ └── monaco-vim.d.ts ├── comfyui │ ├── api.ts │ ├── index.ts │ ├── languages │ │ ├── comfy-dynamic-prompt.ts │ │ ├── comfy-prompt.ts │ │ └── index.ts │ ├── link.ts │ ├── settings.ts │ ├── static │ │ └── codicon.css │ ├── types.ts │ ├── utils.ts │ └── widget │ │ ├── find_widget.ts │ │ ├── index.css │ │ ├── index.ts │ │ └── replace_widget.ts ├── completion.ts ├── index.ts ├── languages │ ├── index.ts │ ├── sd-dynamic-prompt.ts │ └── sd-prompt.ts ├── main.ts ├── monaco_utils.ts ├── styles │ ├── index.css │ └── index.d.ts └── utils.ts ├── tsconfig.json ├── tsconfig.test.json ├── webpack.comfy.dev.js ├── webpack.comfy.js ├── webpack.common.js ├── webpack.dev.js ├── webpack.editor.js └── webpack.prod.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Taremin] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - "v[0-9]+\\.[0-9]+\\.[0-9]+" 7 | paths: 8 | - "pyproject.toml" 9 | 10 | jobs: 11 | publish-node: 12 | name: Publish Custom Node to registry 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Publish Custom Node 18 | uses: Comfy-Org/publish-node-action@main 19 | with: 20 | ## Add your own personal access token to your Github Repository secrets and reference it here. 21 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+\\.[0-9]+\\.[0-9]+" 7 | 8 | jobs: 9 | create_release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: version 16 | id: version 17 | run: | 18 | REPOSITORY=$(echo ${{ github.event.repository.name }}) 19 | echo "repository=$REPOSITORY" >> "$GITHUB_OUTPUT" 20 | VERSION=$(basename ${{ github.ref }}) 21 | echo "version=$VERSION" >> "$GITHUB_OUTPUT" 22 | VERSION_STRING=$(echo $VERSION | sed -e "s#\.#_#g") 23 | echo "version_string=$VERSION_STRING" >> "$GITHUB_OUTPUT" 24 | - name: Zip output 25 | id: create_zip 26 | run: | 27 | ARCHIVE_BASENAME=$(echo ${{steps.version.outputs.repository}}-${{steps.version.outputs.version_string}}) 28 | ARCHIVE_FILENAME=$(echo $ARCHIVE_BASENAME.zip) 29 | 30 | echo "filename=$ARCHIVE_FILENAME" >> "$GITHUB_OUTPUT" 31 | 32 | mkdir $ARCHIVE_BASENAME 33 | rsync -av ./* $ARCHIVE_BASENAME --exclude $ARCHIVE_BASENAME 34 | zip -r $ARCHIVE_FILENAME $ARCHIVE_BASENAME -x ".git" 35 | - name: Create Release 36 | id: create-release 37 | uses: softprops/action-gh-release@v1 38 | with: 39 | name: Release ${{ steps.version.outputs.version }} 40 | tag_name: ${{ steps.version.outputs.version }} 41 | draft: true 42 | files: | 43 | ${{ steps.create_zip.outputs.filename}} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .gitignore 3 | settings/* 4 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Taremin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebUI Monaco Prompt 2 | 3 | これは AUTOMATIC1111 氏の [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) と [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 用の拡張です。 4 | 5 | プロンプトの編集をVSCodeでも使用されているエディタ実装 [Monaco Editor](https://microsoft.github.io/monaco-editor/) で行えるようにします。 6 | 7 | ## インストール 8 | 9 | ### AUTOMATIC1111 Stable Diffusion WebUI 10 | 11 | `stable-diffusion-webui` の `Install from URL` からこのリポジトリのURL `https://github.com/Taremin/webui-monaco-prompt` を入力してインストールしてください。 12 | 13 | ### ComfyUI (Experimental) 14 | 15 | 下記の二通りからお好きな方法でインストールしてください。 16 | 17 | 1. `custom_nodes` にこのリポジトリを clone する 18 | 2. `ComfyUI Manager Menu` の `Install via Git URL` にこのリポジトリのURLを入力してインストールする 19 | 20 | #### 以前のインストール方法 21 | 22 | ~~[Releases](https://github.com/Taremin/webui-monaco-prompt/releases) からzipファイルをダウンロードして `web/extensions` に展開してください。~~ 23 | v0.1.2からはこの方法ではインストールできなくなりました。 24 | 25 | ## 機能 26 | 27 | - VIM キーバインディング対応 ([monaco-vim](https://github.com/brijeshb42/monaco-vim)) 28 | - 色付け機能 29 | - 標準表記 30 | - Dynamic Prompts拡張表記 31 | - オートコンプリート対応 32 | - デフォルトでは `danbooru.csv`, `extra-quality-tags.csv` を読み込んでいるので既に `a1111-sd-webui-tagcompete` を使用している方は違和感なく使えます 33 | - `<` を入力すると Extra Networks (HN/LoRA/LyCORIS) のみの候補を出せます 34 | - LyCORISは[a1111-sd-webui-lycoris](https://github.com/KohakuBlueleaf/ 35 | a1111-sd-webui-lycoris)拡張導入時に使用可能です 36 | - **この機能は Deprecated (非推奨)になりました** 37 | - 今後のアップデートで削除される予定です 38 | - スニペットとモデル名サジェストを使用してください 39 | - モデル名サジェスト 40 | - `Ctrl-M` から始まるショートカットキーでモデル名の挿入が行えます 41 | 42 | | モデル | ショートカットキー | 43 | |--------------|----------------------| 44 | | Checkpoint | `Ctrl-M` -> `Ctrl-M` | 45 | | LoRA | `Ctrl-M` -> `Ctrl-L` | 46 | | Embedding | `Ctrl-M` -> `Ctrl-E` | 47 | | Hypernetwork | `Ctrl-M` -> `Ctrl-H` | 48 | | VAE | `Ctrl-M` -> `Ctrl-A` | 49 | 50 | - スニペット 51 | - `Ctrl-M` -> `Ctrl-S` で挿入可能 52 | - 詳細は後述 53 | 54 | また、他にも Monaco に備わっている VSCode 互換のショートカットキーなども使用可能です。 55 | 56 | ### スニペット 57 | 58 | スニペット(断片)はよく使う入力をテンプレートで行えるようにする機能です。 59 | 60 | #### 追加方法 61 | 62 | スニペットはこの拡張の `snippets` ディレクトリか、各カスタムノード/拡張機能以下の `snippets` ディレクトリに含まれる `.json` を読み込みます。 63 | 64 | JSONのフォーマットは `{"label": string, "insertText": string, documentation: string}` か、その配列です。 65 | `insertText` では下記のスニペット構文が使用可能です。 66 | `documentation` では `Markdown` 及び一部の HTML タグが使用可能です。 67 | 68 | #### 構文 69 | 70 | スニペット構文は VSCode 互換のものが使用できます。 71 | https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax 72 | 73 | 74 | ## 注意 75 | 76 | ### AUTOMATIC1111 Stable Diffusion WebUI 77 | 78 | この拡張では標準のプロンプト編集で使用するtextareaを差し替えたり Extra Networks のリフレッシュへの対応などで、特定のHTML要素に依存したあまり汎用的でない手段を用いています。 79 | そのため、HTML構造が変化したり既存機能の変更が行われた場合、利用できなくなることがあるかもしれません。 80 | その場合は一時的に利用を中止することをおすすめします。 81 | 82 | ## その他 83 | 84 | ### 共通 85 | 86 | ヘッダが邪魔な場合はエディタのコンテキストメニューから非表示にできます。(ヘッダで行える設定もコンテキストメニューから行えます。) 87 | 88 | ### AUTOMATIC1111 Stable Diffusion WebUI 89 | 90 | 設定はこの拡張のあるディレクトリの `settings` 内に保存されます。 91 | 認証未設定時は `global.json` 認証設定時は `user_[username].json` というファイル名です。 92 | 93 | ### オートコンプリート 94 | 95 | `Language` が `plaintext` 以外の場合にCSVによる自動補完が有効になります。 96 | 97 | #### CSV 追加方法 98 | 99 | この拡張の `csv` ディレクトリにファイルを追加します。 100 | A1111の場合は再読み込み、ComfyUIの場合は `Refresh` で使用できるようになります。 101 | 102 | ## クレジット 103 | 104 | この拡張には [a1111-sd-webui-tagcomplete 105 | ](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete) のタグデータ(danbooru.csv, extra-quality-tags.csv)を同梱しています。 106 | 107 | ## ライセンス 108 | 109 | [MIT](./LICENSE) 110 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is the entry point for ComfyUI 3 | # 4 | import os 5 | import server 6 | import folder_paths 7 | from aiohttp import web 8 | import glob 9 | import json 10 | import shutil 11 | from . import snippets 12 | 13 | WEB_DIRECTORY = "./comfy" 14 | 15 | extension_root_path = os.path.dirname(__file__) 16 | custom_nodes_path = folder_paths.get_folder_paths("custom_nodes")[0] 17 | 18 | 19 | @server.PromptServer.instance.routes.get("/webui-monaco-prompt/csv") 20 | async def get_csv_fils(request): 21 | for path in glob.glob(os.path.join(extension_root_path, "csv", "*.csv"), recursive=True): 22 | basename = os.path.basename(path) 23 | comfy_path = os.path.join(extension_root_path, "comfy", basename) 24 | 25 | if not os.path.isfile(comfy_path): 26 | shutil.copy2(path, comfy_path) 27 | 28 | files = list(map( 29 | lambda x: os.path.basename(x), 30 | glob.glob(extension_root_path + "/comfy/*.csv", recursive=True) 31 | )) 32 | 33 | return web.Response(text=json.dumps(files), content_type='application/json') 34 | 35 | 36 | @server.PromptServer.instance.routes.get("/webui-monaco-prompt/snippet") 37 | async def get_snippets(request): 38 | if (snippets.get_snippets() is None): 39 | snippets.load_snippets(custom_nodes_path) 40 | return web.Response(text=json.dumps(snippets.get_snippets()), content_type='application/json') 41 | 42 | 43 | @server.PromptServer.instance.routes.get("/webui-monaco-prompt/snippet-refresh") 44 | async def reload_snippets(request): 45 | snippets.load_snippets(custom_nodes_path) 46 | 47 | return web.Response(text=json.dumps(snippets.get_snippets()), content_type='application/json') 48 | 49 | 50 | class WebuiMonacoPromptFind: 51 | def __init__(self): 52 | pass 53 | 54 | @classmethod 55 | def INPUT_TYPES(s): 56 | return {} 57 | 58 | RETURN_TYPES = () 59 | RETURN_NAMES = () 60 | FUNCTION = "process" 61 | CATEGORY = "WebuiMonacoPrompt" 62 | 63 | def process(self, *args, **kwargs): 64 | return () 65 | 66 | 67 | class WebuiMonacoPromptReplace(WebuiMonacoPromptFind): 68 | pass 69 | 70 | 71 | NODE_CLASS_MAPPINGS = { 72 | "WebuiMonacoPromptFind": WebuiMonacoPromptFind, 73 | "WebuiMonacoPromptReplace": WebuiMonacoPromptReplace, 74 | } 75 | -------------------------------------------------------------------------------- /comfy/codicon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "codicon"; 3 | src: 4 | url("./codicon.ttf") format("truetype"); 5 | } -------------------------------------------------------------------------------- /comfy/codicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taremin/webui-monaco-prompt/88823f0d5a942aa89457a26a6b889e6c380235bd/comfy/codicon.ttf -------------------------------------------------------------------------------- /comfy/extra-quality-tags.csv: -------------------------------------------------------------------------------- 1 | masterpiece,5,Quality tag, 2 | best_quality,5,Quality tag, 3 | high_quality,5,Quality tag, 4 | normal_quality,5,Quality tag, 5 | low_quality,5,Quality tag, 6 | worst_quality,5,Quality tag, 7 | -------------------------------------------------------------------------------- /comfy/main.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/LICENSE */ 2 | 3 | /*! @license DOMPurify 3.1.7 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.7/LICENSE */ 4 | -------------------------------------------------------------------------------- /csv/extra-quality-tags.csv: -------------------------------------------------------------------------------- 1 | masterpiece,5,Quality tag, 2 | best_quality,5,Quality tag, 3 | high_quality,5,Quality tag, 4 | normal_quality,5,Quality tag, 5 | low_quality,5,Quality tag, 6 | worst_quality,5,Quality tag, 7 | -------------------------------------------------------------------------------- /editor_check.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "EndPoint": "/webui-monaco-prompt", 3 | "GetEmbeddings": "/webui-monaco-prompt-embeddings", 4 | "GetSnippets": "/webui-monaco-prompt-snippets", 5 | "CSV": "/webui-monaco-prompt-csv" 6 | } -------------------------------------------------------------------------------- /javascript/codicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taremin/webui-monaco-prompt/88823f0d5a942aa89457a26a6b889e6c380235bd/javascript/codicon.ttf -------------------------------------------------------------------------------- /javascript/main.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/LICENSE */ 2 | 3 | /*! @license DOMPurify 3.1.7 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.7/LICENSE */ 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webui-monaco-prompt", 3 | "version": "0.2.6", 4 | "description": "Prompt Editor Extension for AUTOMATIC1111 stable-diffusion-webui and ComfyUI", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "postinstall": "cd node_modules/monaco-vim/ && npm install && npm run babel", 8 | "clean": "rimraf ./dist ./javascript ./comfy", 9 | "build": "npm run clean && webpack --config webpack.prod.js && webpack --config webpack.comfy.js", 10 | "build:vim": "npm run clean && npm run postinstall && webpack --config webpack.prod.js", 11 | "watch": "webpack watch --config webpack.dev.js", 12 | "watch:editor": "webpack watch --config webpack.editor.js", 13 | "watch:comfy": "webpack watch --config webpack.comfy.dev.js", 14 | "checkupdate": "npm-check-updates --target minor", 15 | "update-deps": "npm-check-updates --target minor -u && npm install" 16 | }, 17 | "author": "Taremin", 18 | "license": "MIT", 19 | "dependencies": { 20 | "csv-parse": "^5.6.0", 21 | "fast-equals": "^5.0.1", 22 | "monaco-editor": "^0.52.2", 23 | "monaco-vim": "github:Taremin/monaco-vim", 24 | "multiple-select-vanilla": "^3.4.4" 25 | }, 26 | "devDependencies": { 27 | "copy-webpack-plugin": "^12.0.2", 28 | "css-loader": "^6.11.0", 29 | "file-loader": "^6.2.0", 30 | "monaco-editor-webpack-plugin": "^7.1.0", 31 | "npm-check-updates": "^16.14.20", 32 | "rimraf": "^5.0.10", 33 | "style-loader": "^3.3.4", 34 | "ts-loader": "^9.5.1", 35 | "typescript": "^5.7.2", 36 | "webpack": ">=5.97.1", 37 | "webpack-cli": "^5.1.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "webui-monaco-prompt" 3 | description = "Make it possible to edit the prompt using the Monaco Editor, an editor implementation used in VSCode.\nNOTE: This extension supports both ComfyUI and A1111 simultaneously." 4 | version = "0.2.6" 5 | license = { text = "MIT License" } 6 | 7 | [project.urls] 8 | Repository = "https://github.com/Taremin/webui-monaco-prompt" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "taremin" 13 | DisplayName = "webui-monaco-prompt" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /scripts/api.py: -------------------------------------------------------------------------------- 1 | from modules import script_callbacks 2 | from typing import Optional 3 | import json 4 | import glob 5 | 6 | from gradio import Blocks 7 | import fastapi 8 | from fastapi import FastAPI, HTTPException, status 9 | from modules.api.api import Api 10 | import snippets 11 | 12 | import os 13 | extension_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 14 | with open(os.path.join(extension_base_dir, "extension.json"), mode="r") as f: 15 | try: 16 | extension_settings = json.load(f) 17 | except Exception: 18 | print(__file__, "can't load extension settings") 19 | extension_settings = {} 20 | 21 | 22 | def on_app_started(demo: Optional[Blocks], app: FastAPI): 23 | def get_settings_path(auth, user): 24 | return os.path.join( 25 | extension_base_dir, 26 | f"settings/{'user_' + user if auth is not None else 'global'}.json" 27 | ) 28 | 29 | def get_current_user(request: fastapi.Request) -> Optional[str]: 30 | token = request.cookies.get("access-token") or request.cookies.get( 31 | "access-token-unsecure" 32 | ) 33 | return app.tokens.get(token) 34 | 35 | def get(request: fastapi.Request): 36 | user = get_current_user(request) 37 | if app.auth is None or user is not None: 38 | settings_path = get_settings_path(app.auth, user) 39 | if not os.path.isfile(settings_path): 40 | return {} 41 | 42 | with open(settings_path, mode="r") as f: 43 | try: 44 | return json.load(f) 45 | except Exception: 46 | print(__file__, "can't load JSON") 47 | return {} 48 | raise HTTPException( 49 | status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated" 50 | ) 51 | 52 | async def post(request: fastapi.Request): 53 | user = get_current_user(request) 54 | if app.auth is None or user is not None: 55 | with open(get_settings_path(app.auth, user), mode="w") as f: 56 | try: 57 | settings = await request.json() # json check 58 | json.dump(settings, f) 59 | return {"success": True} 60 | except Exception as e: 61 | return {"success": False, "error": e} 62 | raise HTTPException( 63 | status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated" 64 | ) 65 | 66 | def get_embeddings(request: fastapi.Request): 67 | return Api.get_embeddings(None) 68 | 69 | def get_csvs(request: fastapi.Request): 70 | paths = glob.glob( 71 | pathname=os.path.join(extension_base_dir, "csv", "**", "*.csv"), 72 | recursive=True 73 | ) 74 | return list(map(lambda path: os.path.splitext(os.path.basename(path))[0], paths)) 75 | 76 | def get_snippets(request: fastapi.Request): 77 | snippets.load_snippets(os.path.join(extension_base_dir, "..")) 78 | return snippets.get_snippets() 79 | 80 | app.add_api_route(extension_settings.get("EndPoint"), get, methods=["GET"]) 81 | app.add_api_route(extension_settings.get("EndPoint"), post, methods=["POST"]) 82 | app.add_api_route(extension_settings.get("GetEmbeddings"), get_embeddings, methods=["GET"]) 83 | app.add_api_route(extension_settings.get("CSV"), get_csvs, methods=["GET"]) 84 | app.add_api_route(extension_settings.get("GetSnippets"), get_snippets, methods=["GET"]) 85 | 86 | 87 | script_callbacks.on_app_started(on_app_started) 88 | -------------------------------------------------------------------------------- /settings/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taremin/webui-monaco-prompt/88823f0d5a942aa89457a26a6b889e6c380235bd/settings/.gitkeep -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501, W503, W504, E203, F821, F722 3 | 4 | -------------------------------------------------------------------------------- /snippets.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import json 4 | 5 | snippets = None 6 | 7 | 8 | def get_snippets(): 9 | global snippets 10 | return snippets 11 | 12 | 13 | def load_snippets(target_dir: str): 14 | global snippets 15 | snippets = [] 16 | custom_nodes_path = target_dir 17 | for path in glob.glob(os.path.join(custom_nodes_path, "*", "snippets", "*.json")): 18 | try: 19 | with open(path, "r") as fp: 20 | loaded_snippets = json.load(fp) 21 | 22 | for loaded_snippet in (loaded_snippets if isinstance(loaded_snippets, list) else [loaded_snippets]): 23 | label_text = loaded_snippet.get("label") 24 | insert_text = loaded_snippet.get("insertText") 25 | if ( 26 | label_text is not None and isinstance(label_text, str) and 27 | insert_text is not None and isinstance(insert_text, str) 28 | ): 29 | snippet = {} 30 | snippet["label"] = label_text 31 | snippet["insertText"] = insert_text 32 | snippet["path"] = os.path.relpath(path, custom_nodes_path) 33 | 34 | detail_text = loaded_snippet.get("detail") 35 | if detail_text is not None: 36 | snippet["detail"] = detail_text 37 | 38 | documentation_text = loaded_snippet.get("documentation") 39 | if documentation_text is not None: 40 | snippet["documentation"] = documentation_text 41 | 42 | snippets.append(snippet) 43 | except Exception: 44 | print("[SKIP] Webui Monaco Prompt: invalid json:", path) 45 | -------------------------------------------------------------------------------- /snippets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taremin/webui-monaco-prompt/88823f0d5a942aa89457a26a6b889e6c380235bd/snippets/.gitkeep -------------------------------------------------------------------------------- /src/@types/gradio.d.ts: -------------------------------------------------------------------------------- 1 | declare const onUiUpdate: (callback: Function) => void; 2 | declare const get_uiCurrentTabContent: () => HTMLElement; 3 | declare const gradioApp: () => Document|DocumentFragment; -------------------------------------------------------------------------------- /src/@types/monaco-vim.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/brijeshb42/monaco-vim/issues/33 2 | declare module "monaco-vim" { 3 | export interface EditorVimMode { 4 | dispose: () => void; 5 | } 6 | 7 | type initVimModeFn = ( 8 | editor: monaco.editor.IStandaloneCodeEditor, 9 | statusElm: HTMLElement 10 | ) => EditorVimMode; 11 | 12 | const initVimMode: initVimModeFn; 13 | export { initVimMode }; 14 | 15 | const VimMode: { 16 | Vim: { 17 | noremap: (from: string, to: string) => void; 18 | map: (from: string, to: string, mode: string) => void; 19 | }; 20 | }; 21 | export { VimMode }; 22 | } -------------------------------------------------------------------------------- /src/comfyui/api.ts: -------------------------------------------------------------------------------- 1 | const app = (await eval('import("../../scripts/app.js")')).app // call native import 2 | const api = (await eval('import("../../scripts/api.js")')).api // call native import 3 | const ui = (await eval('import("../../scripts/ui.js")')) // call native import 4 | 5 | export { 6 | app, 7 | api, 8 | ui, 9 | } -------------------------------------------------------------------------------- /src/comfyui/index.ts: -------------------------------------------------------------------------------- 1 | import * as utils from "./utils" 2 | import * as WebuiMonacoPrompt from "../index" // for typing 3 | import { link } from "./link" 4 | import { FindWidget, ReplaceWidget } from "./widget" 5 | import { app, api } from "./api" 6 | import { loadSetting, saveSettings, updateInstanceSettings } from "./settings" 7 | import { comfyPrompt, comfyDynamicPrompt } from "./languages" 8 | import { escapeHTML } from "../utils" 9 | 10 | declare let __webpack_public_path__: any; 11 | 12 | interface Window { 13 | WebuiMonacoPromptBaseURL: string 14 | WebuiMonacoPrompt: typeof WebuiMonacoPrompt 15 | } 16 | declare var window: Window 17 | 18 | // set dynamic path 19 | const srcURL = new URL(document.currentScript ? (document.currentScript as HTMLScriptElement).src : import.meta.url) 20 | const dir = srcURL.pathname.split('/').slice(0, -1).join('/'); 21 | window.WebuiMonacoPromptBaseURL = dir + "/"; 22 | __webpack_public_path__ = dir + "/" 23 | 24 | // codicon 25 | utils.loadCodicon(dir) 26 | 27 | // import は __webpack_public_path__ を使う場合は処理順の関係で使えない 28 | const MonacoPrompt = require("../index") as typeof WebuiMonacoPrompt 29 | window.WebuiMonacoPrompt = MonacoPrompt 30 | 31 | const languages = [ 32 | {id: "comfy-prompt", lang: comfyPrompt}, 33 | {id: "comfy-dynamic-prompt", lang: comfyDynamicPrompt}, 34 | ] 35 | MonacoPrompt.addLanguages(languages) 36 | 37 | const models = [ 38 | { 39 | keybinding: WebuiMonacoPrompt.KeyMod.chord( 40 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyM, 41 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyM 42 | ), 43 | model: "checkpoints" 44 | }, 45 | { 46 | keybinding: WebuiMonacoPrompt.KeyMod.chord( 47 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyM, 48 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyL 49 | ), 50 | model: "loras" 51 | }, 52 | { 53 | keybinding: WebuiMonacoPrompt.KeyMod.chord( 54 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyM, 55 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyE 56 | ), 57 | model: "embeddings" 58 | }, 59 | { 60 | keybinding: WebuiMonacoPrompt.KeyMod.chord( 61 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyM, 62 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyH 63 | ), 64 | model: "hypernetworks" 65 | }, 66 | { 67 | keybinding: WebuiMonacoPrompt.KeyMod.chord( 68 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyM, 69 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyA 70 | ), 71 | model: "vae" 72 | }, 73 | ] 74 | type ComfyAPIModels = string[] | {name: string, pathIndex: number}[] 75 | for (const {keybinding, model} of models) { 76 | MonacoPrompt.addCustomSuggest(model, keybinding, async () => { 77 | const items: Partial[] = [] 78 | const models = await api.getModels(model) as ComfyAPIModels 79 | models.forEach(model => { 80 | const label = typeof model === "string" ? model : model.name 81 | items.push({ 82 | label: label, 83 | kind: WebuiMonacoPrompt.CompletionItemKind.File, 84 | insertText: label, 85 | }) 86 | }) 87 | return items 88 | }) 89 | } 90 | 91 | // snippet 92 | MonacoPrompt.addCustomSuggest( 93 | "snippet", 94 | WebuiMonacoPrompt.KeyMod.chord( 95 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyM, 96 | WebuiMonacoPrompt.KeyMod.CtrlCmd | WebuiMonacoPrompt.KeyCode.KeyS, 97 | ), 98 | async () => { 99 | const items: Partial[] = [] 100 | const snippets = await api.fetchApi("/webui-monaco-prompt/snippet").then((res: Response) => res.json()) 101 | 102 | for (const snippet of snippets) { 103 | const usage = `**${escapeHTML(snippet.insertText)}**` 104 | items.push({ 105 | label: snippet.label, 106 | kind: WebuiMonacoPrompt.CompletionItemKind.Snippet, 107 | insertText: snippet.insertText, 108 | insertTextRules: WebuiMonacoPrompt.CompletionItemInsertTextRule.InsertAsSnippet, 109 | detail: snippet.path, 110 | documentation: { 111 | supportHtml: true, 112 | value: snippet.documentation ? 113 | [ 114 | usage, 115 | snippet.documentation 116 | ].join("

") : 117 | usage 118 | }, 119 | }) 120 | } 121 | 122 | return items 123 | } 124 | ) 125 | async function refreshSnippets() { 126 | await api.fetchApi("/webui-monaco-prompt/snippet-refresh").then((res: Response) => res.json()) 127 | return 128 | } 129 | 130 | let csvfiles: string[] 131 | async function loadCSV (files: string[]) { 132 | MonacoPrompt.clearCSV() 133 | 134 | for (const filename of files) { 135 | const path = [dir, filename].join('/') 136 | const filenameParts = filename.split('.') 137 | if (filenameParts.length > 2) { 138 | throw new Error("Invalid filename (too many '.')") 139 | } 140 | const basename = filenameParts[0] 141 | const value = await fetch(path).then(res => res.text()) 142 | 143 | MonacoPrompt.addCSV(basename, value) 144 | } 145 | } 146 | 147 | async function refreshCSV() { 148 | csvfiles = await fetch("/webui-monaco-prompt/csv").then(res => res.json()) 149 | await loadCSV(csvfiles) 150 | } 151 | 152 | await refreshCSV() 153 | await loadSetting() 154 | 155 | function onCreateTextarea(textarea: HTMLTextAreaElement, node: any) { 156 | if (textarea.readOnly) { 157 | console.log("[WebuiMonacoPrompt] Skip: TextArea is read-only:", textarea) 158 | return 159 | } 160 | 161 | const editor = new MonacoPrompt.PromptEditor(textarea, { 162 | autoLayout: true, 163 | handleTextAreaValue: true, 164 | }) 165 | for(const {keybinding, model} of models) { 166 | editor.addCustomSuggest(model) 167 | } 168 | editor.addCustomSuggest("snippet") 169 | 170 | // style 同期 171 | const observer = new MutationObserver((mutations, observer) => { 172 | for (const mutation of mutations) { 173 | if (mutation.target !== textarea) { 174 | continue 175 | } 176 | editor.style.cssText = (mutation.target as HTMLTextAreaElement).style.cssText 177 | } 178 | }) 179 | observer.observe(textarea, { 180 | attributes: true, 181 | attributeFilter: ["style"] 182 | }) 183 | editor.style.cssText = textarea.style.cssText 184 | 185 | Object.assign(editor.elements.header!.style, { 186 | backgroundColor: "#444", 187 | fontSize: "small", 188 | }) 189 | Object.assign(editor.elements.footer!.style, { 190 | backgroundColor: "#444", 191 | fontSize: "small", 192 | }) 193 | 194 | const id = editor.getInstanceId() 195 | textarea.dataset.webuiMonacoPromptTextareaId = "" + id 196 | editor.dataset.webuiMonacoPromptTextareaId = "" + id 197 | link[id] = { 198 | textarea: textarea, 199 | monaco: editor, 200 | observer: observer, 201 | node: node, 202 | } 203 | 204 | editor.addEventListener('keydown', (ev: KeyboardEvent) => { 205 | switch (ev.key) { 206 | case 'Esc': 207 | case 'Escape': 208 | ev.stopPropagation() 209 | break 210 | default: 211 | break 212 | } 213 | }) 214 | const mouseHandler = (ev: MouseEvent) => { 215 | const id = (ev.target as typeof editor).dataset.webuiMonacoPromptTextareaId! 216 | const node = link[id].node 217 | utils.setActiveNode(app, node) 218 | } 219 | editor.addEventListener("contextmenu", mouseHandler, {capture: true}) 220 | editor.addEventListener("click", mouseHandler, {capture: true}) 221 | 222 | if (textarea.parentElement) { 223 | textarea.parentElement.append(editor) 224 | } 225 | 226 | editor.onChangeTheme(() => { 227 | editor.monaco._themeService.onDidColorThemeChange(() => { 228 | utils.updateThemeStyle(editor) 229 | }) 230 | }) 231 | 232 | updateInstanceSettings(editor) 233 | utils.updateThemeStyle(editor) 234 | 235 | editor.onChangeBeforeSync(() => saveSettings(editor)) 236 | 237 | return editor 238 | } 239 | 240 | function onRemoveTextarea(textarea: HTMLTextAreaElement) { 241 | const id = textarea.dataset.webuiMonacoPromptTextareaId 242 | if (typeof(id) !== 'string') { 243 | return 244 | } 245 | 246 | const ctx = link[id] 247 | ctx.observer.disconnect() 248 | const editor = ctx.monaco 249 | editor.dispose() 250 | if (editor.parentElement) { 251 | editor.parentElement.removeChild(ctx.monaco) 252 | } 253 | delete link[id] 254 | } 255 | 256 | function hookNodeWidgets(node: any) { 257 | if (!node.widgets) { 258 | return 259 | } 260 | for (const widget of node.widgets) { 261 | if (!widget.element) { 262 | continue 263 | } 264 | if (widget.element instanceof HTMLTextAreaElement) { 265 | onCreateTextarea(widget.element, node) 266 | } 267 | } 268 | const onRemovedOriginal = node.onRemoved 269 | node.onRemoved = function() { 270 | if (onRemovedOriginal) { 271 | onRemovedOriginal.apply(this, arguments) 272 | } 273 | 274 | for (const widget of node.widgets) { 275 | if (!widget.element) { 276 | continue 277 | } 278 | if (widget.element instanceof HTMLTextAreaElement) { 279 | onRemoveTextarea(widget.element) 280 | } 281 | } 282 | } 283 | } 284 | 285 | interface CustomNodeWidget { 286 | nodeType: string 287 | widget: typeof FindWidget 288 | } 289 | 290 | const CustomNode: {[key: string]: CustomNodeWidget} = { 291 | find: { 292 | nodeType: "WebuiMonacoPromptFind", 293 | widget: FindWidget, 294 | }, 295 | replace: { 296 | nodeType: "WebuiMonacoPromptReplace", 297 | widget: ReplaceWidget, 298 | }, 299 | } 300 | 301 | const CustomNodeFromNodeType = Object.fromEntries( 302 | Object.entries(CustomNode).map(([key, value]) => { 303 | return [value.nodeType, value] 304 | }) 305 | ) 306 | 307 | // 既存ノードの textarea 置き換えと検索ノードの初期化 308 | for (const node of app.graph._nodes) { 309 | // textarea 置き換え 310 | hookNodeWidgets(node) 311 | 312 | // 検索ノード初期化 313 | /* 314 | const nodeTypes: {[key: string]: CustomNodeWidget} = {} 315 | const keys = Object.keys(CustomNode) as (keyof typeof CustomNode)[] 316 | keys.forEach((type: keyof typeof CustomNode) => { 317 | nodeTypes[CustomNode[type].nodeType] = CustomNode[type] 318 | }) 319 | if (nodeTypes[node.type] && nodeTypes[node.type].widget) { 320 | console.log("widget:", node.type, node) 321 | nodeTypes[node.type].widget.fromNode(app, node) 322 | } 323 | */ 324 | const customNode = CustomNodeFromNodeType[node.comfyClass] 325 | if (!customNode) { 326 | continue 327 | } 328 | customNode.widget.fromNode(app, node) 329 | } 330 | 331 | // これから追加されるノードの設定 332 | const register = (app: any) => { 333 | //const classNames = Object.values(CustomNode).map(node => node.nodeType) 334 | app.registerExtension({ 335 | name: ["Taremin", "WebuiMonacoPrompt"].join('.'), 336 | nodeCreated(node:any, app: any) { 337 | // 既存ノードの widget 置き換え(textarea) 338 | hookNodeWidgets(node) 339 | 340 | // Find / Replace widget 341 | const customNode = CustomNodeFromNodeType[node.comfyClass] 342 | if (!customNode) { 343 | return 344 | } 345 | customNode.widget.fromNode(app, node) 346 | }, 347 | // refresh button 348 | refreshComboInNodes: async function(nodeDef: any, app: any) { 349 | refreshCSV() 350 | refreshSnippets() 351 | } 352 | }) 353 | 354 | if (app.extensionManager && app.extensionManager.registerSidebarTab) { 355 | app.extensionManager.registerSidebarTab({ 356 | id: "webuimonacoprompt-search", 357 | icon: "pi pi-search", 358 | title: FindWidget.SidebarTitle, 359 | tooltip: FindWidget.SidebarTooltip, 360 | type: "custom", 361 | render: (el: HTMLElement) => { 362 | FindWidget.sidebar(app, el) 363 | }, 364 | }) 365 | } 366 | } 367 | register(app) 368 | -------------------------------------------------------------------------------- /src/comfyui/languages/comfy-dynamic-prompt.ts: -------------------------------------------------------------------------------- 1 | import {languages} from 'monaco-editor/esm/vs/editor/editor.api' 2 | import {conf as baseConf, language as baseLanguage} from '../../languages/sd-dynamic-prompt' 3 | import { conf as comfyConf, comments, whitespace} from './comfy-prompt' 4 | 5 | const conf: languages.LanguageConfiguration = Object.assign({}, baseConf, { 6 | comments: Object.assign({}, baseConf.comments, comfyConf.comments), 7 | }) 8 | 9 | const language: languages.IMonarchLanguage = Object.assign({}, baseLanguage, { 10 | brackets: baseLanguage.brackets!.concat([ 11 | { open: '{', close: '}', token: 'delimiter.curly' }, 12 | ]), 13 | 14 | tokenizer: Object.assign({}, baseLanguage.tokenizer, { 15 | whitespace: baseLanguage.tokenizer.whitespace.concat(whitespace), 16 | comment: comments, 17 | }), 18 | }) 19 | 20 | export { 21 | conf, 22 | language, 23 | } 24 | -------------------------------------------------------------------------------- /src/comfyui/languages/comfy-prompt.ts: -------------------------------------------------------------------------------- 1 | 2 | import {languages} from 'monaco-editor/esm/vs/editor/editor.api' 3 | import {conf as baseConf, language as baseLanguage} from '../../languages/sd-prompt' 4 | 5 | const conf: languages.LanguageConfiguration = Object.assign({}, baseConf, { 6 | comments: Object.assign({}, baseConf.comments, { 7 | lineComment: '//', 8 | blockComment: ["/*", "*/"], 9 | }), 10 | brackets: baseConf.brackets!.concat([ 11 | ['{', '}'], 12 | ]), 13 | autoClosingPairs: baseConf.autoClosingPairs!.concat([ 14 | { open: '{', close: '}' }, 15 | ]), 16 | surroundingPairs: baseConf.surroundingPairs!.concat([ 17 | { open: '{', close: '}' }, 18 | ]), 19 | }) 20 | 21 | const whitespace: languages.IMonarchLanguageRule[] = [ 22 | [/\/\*/, 'comment', '@comment'], // block comment 23 | [/\/\/.*$/, 'comment'], // line comment 24 | ] 25 | 26 | const comments: languages.IMonarchLanguageRule[] = [ 27 | [/[^\/*]+/, 'comment'], 28 | // [/\/\*/, 'comment', '@push' ], // nested comment not allowed :-( 29 | // [/\/\*/, 'comment.invalid' ], // this breaks block comments in the shape of /* //*/ 30 | [/\*\//, 'comment', '@pop'], 31 | [/[\/*]/, 'comment'] 32 | ] 33 | 34 | const language: languages.IMonarchLanguage = Object.assign({}, baseLanguage, { 35 | brackets: baseLanguage.brackets!.concat([ 36 | { open: '{', close: '}', token: 'delimiter.curly' }, 37 | ]), 38 | 39 | tokenizer: Object.assign({}, baseLanguage.tokenizer, { 40 | whitespace: baseLanguage.tokenizer.whitespace.concat(whitespace), 41 | comment: comments, 42 | }), 43 | }) 44 | 45 | export { 46 | conf, 47 | language, 48 | whitespace, 49 | comments, 50 | } 51 | -------------------------------------------------------------------------------- /src/comfyui/languages/index.ts: -------------------------------------------------------------------------------- 1 | export * as comfyPrompt from './comfy-prompt' 2 | export * as comfyDynamicPrompt from './comfy-dynamic-prompt' 3 | -------------------------------------------------------------------------------- /src/comfyui/link.ts: -------------------------------------------------------------------------------- 1 | import { WebuiMonacoPromptAdapter } from "./types" 2 | 3 | export const link: {[key: string]: WebuiMonacoPromptAdapter} = {} 4 | -------------------------------------------------------------------------------- /src/comfyui/settings.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual } from 'fast-equals' 2 | import * as utils from "./utils" 3 | import * as WebuiMonacoPrompt from "../index" // for typing 4 | import { api } from "./api" 5 | 6 | const me = "webui-monaco-prompt" 7 | 8 | // 設定の読み込み 9 | let prevSettings: any = null 10 | let settings: any = {} 11 | const updateSettings = (newSetting: any) => { 12 | if (!newSetting) { 13 | return 14 | } 15 | Object.assign(settings, newSetting) 16 | 17 | WebuiMonacoPrompt.runAllInstances((instance) => updateInstanceSettings(instance)) 18 | 19 | if (settings.editor && settings.editor.csvToggle) { 20 | const enables = Object.entries(settings.editor.csvToggle).filter(([contextKey, value]) => { 21 | return value 22 | }).map(([contextKey, value]) => { 23 | return contextKey.split('.').slice(-1)[0] 24 | }) 25 | WebuiMonacoPrompt.addLoadedCSV(enables) 26 | } 27 | } 28 | function updateInstanceSettings(instance: WebuiMonacoPrompt.PromptEditor) { 29 | if (!settings) { 30 | return 31 | } 32 | 33 | if (settings.editor) { 34 | instance.setSettings(settings.editor, true) 35 | } 36 | 37 | utils.updateThemeStyle(instance) 38 | } 39 | 40 | async function saveSettings(instance: WebuiMonacoPrompt.PromptEditor) { 41 | const currentSettings = instance.getSettings() 42 | if (deepEqual(prevSettings, currentSettings)) { 43 | return 44 | } 45 | prevSettings = currentSettings 46 | 47 | if (settings && settings.editor) { 48 | settings.editor = currentSettings 49 | } 50 | 51 | api.storeSetting(me, Object.assign(settings, { 52 | editor: currentSettings 53 | })).then((res: Response) => { 54 | }) 55 | } 56 | 57 | async function loadSetting() { 58 | const settings = await api.getSetting(me) 59 | updateSettings(settings) 60 | } 61 | 62 | function getSettings() { 63 | return settings 64 | } 65 | 66 | export { 67 | loadSetting, 68 | getSettings, 69 | updateSettings, 70 | updateInstanceSettings, 71 | saveSettings, 72 | } -------------------------------------------------------------------------------- /src/comfyui/static/codicon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "codicon"; 3 | src: 4 | url("./codicon.ttf") format("truetype"); 5 | } -------------------------------------------------------------------------------- /src/comfyui/types.ts: -------------------------------------------------------------------------------- 1 | import * as WebuiMonacoPrompt from "../index" // for typing 2 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' 3 | 4 | type PromptEditor = WebuiMonacoPrompt.PromptEditor & { 5 | instanceStyle?: HTMLStyleElement 6 | findDecorations?: monaco.editor.IEditorDecorationsCollection 7 | } 8 | 9 | interface WebuiMonacoPromptAdapter { 10 | textarea: HTMLTextAreaElement 11 | monaco: PromptEditor 12 | observer: MutationObserver 13 | node: any 14 | } 15 | 16 | interface NodeFindMatch { 17 | match: monaco.editor.FindMatch 18 | instanceId: number 19 | } 20 | 21 | export { 22 | PromptEditor, 23 | WebuiMonacoPromptAdapter, 24 | NodeFindMatch, 25 | } 26 | -------------------------------------------------------------------------------- /src/comfyui/utils.ts: -------------------------------------------------------------------------------- 1 | import * as WebuiMonacoPrompt from "../index" 2 | import { PromptEditor, NodeFindMatch } from "./types" 3 | // @ts-ignore 4 | import * as codicon from "monaco-editor/esm/vs/base/common/codiconsUtil" 5 | 6 | 7 | // Codicon を style 要素でロード 8 | const loadCodicon = (baseurl: string) => { 9 | const link = document.createElement("link") 10 | link.rel = "stylesheet" 11 | link.type = "text/css" 12 | link.href = [baseurl, "codicon.css"].join("/") 13 | document.head.appendChild(link) 14 | 15 | const codiconChars = codicon.getCodiconFontCharacters() 16 | const codiconStyle = document.createElement("style") 17 | const codiconLines = [] 18 | for (const key of Object.keys(codiconChars)) { 19 | const value = codiconChars[key] 20 | codiconLines.push(`.codicon-${key}:before { content: '\\${value.toString(16)}'; } `) 21 | } 22 | codiconStyle.textContent = codiconLines.join("\n") 23 | 24 | document.body.appendChild(codiconStyle) 25 | } 26 | 27 | // Monaco のテーマに合わせて検索マッチ部分の style 要素を生成・更新 28 | const themeStyleClassName = "webui-monaco-prompt-findmatch" 29 | const getThemeClassName = () => themeStyleClassName 30 | const updateThemeStyle = (instance: PromptEditor) => { 31 | let themeStyle 32 | 33 | if (!instance.shadowRoot) { 34 | throw new Error("shadowRoot not found") 35 | } 36 | if (!instance.instanceStyle) { 37 | themeStyle = document.createElement("style") 38 | instance.shadowRoot.appendChild(themeStyle) 39 | instance.instanceStyle = themeStyle 40 | } else { 41 | themeStyle = instance.instanceStyle 42 | } 43 | 44 | const editor = instance.monaco 45 | const theme = editor._themeService.getColorTheme() 46 | const style: any = {} 47 | for (const [cssProperty, monacoThemeColorId] of [ 48 | ["background-color", "editor.findMatchBackground"], 49 | ["border-color", "editor.findMatchBorder"], 50 | ]) { 51 | const color = theme.getColor(monacoThemeColorId, true) 52 | if (!color) { 53 | continue 54 | } 55 | style[cssProperty] = color.toString() 56 | } 57 | const lines = Object.keys(style).map((key: string) => { 58 | return `${key}: ${(style as any)[key]};` 59 | }).join(" ") 60 | 61 | themeStyle.innerHTML = `.${getThemeClassName()} { ${lines} }` 62 | } 63 | 64 | // すべての WebUI Monaco Prompt インスタンスで検索 65 | function find(searchString: string, isRegex: boolean, matchCase: boolean, matchWordOnly: boolean) { 66 | const allMmatches: NodeFindMatch[] = [] 67 | WebuiMonacoPrompt.runAllInstances((instance) => { 68 | Array.prototype.push.apply(allMmatches, findInstance(instance, searchString, isRegex, matchCase, matchWordOnly)) 69 | }) 70 | return allMmatches 71 | } 72 | 73 | function findInstance(instance: PromptEditor, searchString: string, isRegex: boolean, matchCase: boolean, matchWordOnly: boolean, decoration: boolean = true) { 74 | const editor = instance.monaco 75 | const model = editor.getModel() 76 | if (!model) { 77 | return [] 78 | } 79 | 80 | const editorConfig = editor.getConfiguration() 81 | const wordSeparators = editorConfig.wordSeparators as unknown as string 82 | const matches = model.findMatches( 83 | searchString, 84 | false, 85 | isRegex, 86 | matchCase, 87 | matchWordOnly ? wordSeparators : null, 88 | true, 89 | ) 90 | 91 | if (instance.findDecorations) { 92 | instance.findDecorations.clear() 93 | } 94 | 95 | if (decoration) { 96 | instance.findDecorations = editor.createDecorationsCollection( 97 | matches.map((findMatch) => { 98 | return { 99 | range: findMatch.range, 100 | options: { 101 | inlineClassName: getThemeClassName() 102 | }, 103 | } 104 | }) 105 | ) 106 | } 107 | 108 | return matches.map((match) => { 109 | return { 110 | match: match, 111 | instanceId: instance.getInstanceId(), 112 | } as NodeFindMatch 113 | }) 114 | } 115 | 116 | function replace(searchString: string, replaceString: string, isRegex: boolean, matchCase: boolean, matchWordOnly: boolean) { 117 | WebuiMonacoPrompt.runAllInstances((instance) => { 118 | replaceInInstance(instance, searchString, replaceString, isRegex, matchCase, matchWordOnly) 119 | }) 120 | } 121 | 122 | function replaceInInstance(instance: PromptEditor, searchString: string, replaceString: string, isRegex: boolean, matchCase: boolean, matchWordOnly: boolean) { 123 | const nodeFindMatches = findInstance(instance, searchString, isRegex, matchCase, matchWordOnly, false) 124 | 125 | const editOperations = nodeFindMatches.map((nodeFindMatch) => { 126 | if (isRegex) { 127 | const matches = nodeFindMatch.match.matches 128 | if (!matches || matches.length === 0) { 129 | throw new Error(`wrong match: ${matches}`) 130 | } 131 | const replaced = matches[0].replace(new RegExp(searchString), replaceString) 132 | return { 133 | range: nodeFindMatch.match.range, 134 | text: replaced 135 | } 136 | } else { 137 | return { 138 | range: nodeFindMatch.match.range, 139 | text: replaceString 140 | } 141 | } 142 | }) 143 | 144 | instance.monaco.executeEdits("replaceInstance", editOperations) 145 | } 146 | 147 | // litegraph の指定ノードをアクティブ(最前面に移動, ノードを選択) 148 | const setActiveNode = (app: any, node: any) => { 149 | app.canvas.bringToFront(node) 150 | app.canvas.selectNode(node, false) 151 | } 152 | 153 | export { 154 | loadCodicon, 155 | getThemeClassName, 156 | updateThemeStyle, 157 | find, 158 | replace, 159 | setActiveNode, 160 | } -------------------------------------------------------------------------------- /src/comfyui/widget/find_widget.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as utils from "../utils" 3 | import { ui } from "../api" 4 | import { link } from "../link" 5 | import * as Monaco from 'monaco-editor/esm/vs/editor/editor.api' // for typing 6 | 7 | import {default as style} from "./index.css" 8 | 9 | const $el = ui.$el 10 | const TooltipSurroundingLines = 2 11 | const TooltipDistance = 20 12 | 13 | interface FindWidgetElements { 14 | header: HTMLDivElement 15 | inputContainer: HTMLDivElement 16 | input: HTMLInputElement 17 | container: HTMLDivElement 18 | tableContainer: HTMLDivElement 19 | table: HTMLTableElement 20 | tbody: HTMLTableSectionElement 21 | thead: HTMLTableSectionElement 22 | } 23 | type LitegraphNode = any 24 | 25 | class FindWidget { 26 | _app: any 27 | _onNodeCreatedOriginal?: any 28 | elements: FindWidgetElements 29 | isInitialized: boolean 30 | 31 | static SidebarTitle = "WebuiMonacoPrompt Search" 32 | static SidebarTooltip = "WebuiMonacoPrompt Search" 33 | 34 | constructor(app: any) { 35 | this._app = app 36 | this.elements = {} as FindWidgetElements 37 | this.isInitialized = false 38 | } 39 | static fromNodeType(app:any, nodeType: any) { 40 | const widget = new this(app) 41 | 42 | widget._onNodeCreatedOriginal = nodeType.prototype.onNodeCreated 43 | nodeType.prototype.onNodeCreated = function(this: LitegraphNode) { 44 | widget.onNodeCreated(this) 45 | widget.isInitialized = true 46 | this.find = widget 47 | } 48 | } 49 | static fromNode(app: any, node: LitegraphNode) { 50 | const widget = new this(app) 51 | 52 | widget.onNodeCreated(node) 53 | widget.isInitialized = true 54 | node.find = widget 55 | } 56 | static sidebar(app: any, sidebar: HTMLElement) { 57 | const instance = new this(app) 58 | 59 | instance.initializeContainer() 60 | instance.elements.header.classList.add("comfy-vue-side-bar-header") 61 | instance.elements.tableContainer.classList.add("comfy-vue-side-bar-body") 62 | instance.elements.tableContainer.style.height = "auto" 63 | 64 | const iconfield = $el("div.p-iconfield", { 65 | style: { 66 | display: "flex", 67 | alignItems: "center", 68 | } 69 | }, [ 70 | $el("span.pi.pi-search"), 71 | instance.elements.inputContainer, 72 | ]) 73 | const container = $el("div.comfy-vue-side-bar-container.flex.flex-col.h-full.workflows-sidebar-tab", [ 74 | $el("div.comfy-vue-side-bar-header", [ 75 | $el("div.p-toolbar.p-component.flex-shrink-0.border-x-0.border-t-0.rounded-none.px-2.py-1.min-h-8", { 76 | "role": "toolbar", 77 | "data-pc-name": "toolbar", 78 | "data-pc-section": "root", 79 | style: { 80 | border: "1px solid rgb(63,63,70)", 81 | display: "flex", 82 | //border: "var(--p-toolbar-border-color)", 83 | } 84 | }, [ 85 | $el("div.p-toolbar-start", { 86 | style: { 87 | display: "flex", 88 | alignItems: "center", 89 | }, 90 | "data-pc-section": "start" 91 | },[ 92 | $el("span.text-sm", [ 93 | this.SidebarTitle 94 | ]) 95 | ]), 96 | $el("div.p-toolbar-center", { 97 | style: { 98 | display: "flex", 99 | alignItems: "center", 100 | }, 101 | "data-pc-section": "center" 102 | }), 103 | $el("div.p-toolbar-end", { 104 | style: { 105 | display: "flex", 106 | alignItems: "center", 107 | }, 108 | "data-pc-section": "end" 109 | }, [ 110 | $el("span.p-button.pi", { 111 | style: { 112 | "visibility": "hidden", 113 | "padding": "0.25rem 0", 114 | } 115 | }, [ 116 | $el("span.p-button-label", { 117 | style: { 118 | display: "inline-flex", 119 | height: "18px", 120 | width: 0, 121 | } 122 | }," "), 123 | ]), 124 | ]), 125 | ]), 126 | $el("div.p-2.2xl:p-4",[ 127 | iconfield, 128 | ]) 129 | ]), 130 | $el("div", [ 131 | instance.elements.tableContainer, 132 | ]), 133 | ]) 134 | sidebar.appendChild(container) 135 | } 136 | onNodeCreated(node: LitegraphNode) { 137 | this.callOriginalCallback.apply(node, arguments as any) 138 | this.initializeWidget(node) 139 | } 140 | callOriginalCallback() { 141 | if (this._onNodeCreatedOriginal) { 142 | this._onNodeCreatedOriginal.apply(this, arguments) 143 | } 144 | } 145 | initializeContainer() { 146 | // custom widget 147 | const containerEl = this.elements.container = document.createElement("div") 148 | containerEl.classList.add(style["webui-monaco-prompt-container"]) 149 | 150 | this.createWidgetHeader() 151 | this.createWidgetBody() 152 | } 153 | initializeWidget(node: LitegraphNode) { 154 | this.initializeContainer() 155 | 156 | const widget = node.addDOMWidget("webui-monaco-prompt-find", "webui-monaco-prompt-find-widget", this.elements.container, {}) 157 | widget.containerEl = this.elements.container 158 | } 159 | createWidgetBody() { 160 | const tableContainerEl = this.elements.tableContainer = document.createElement("div") 161 | tableContainerEl.classList.add(style["webui-monaco-prompt-table-container"]) 162 | 163 | const tableEl = this.elements.table = document.createElement("table") 164 | tableEl.classList.add(style["webui-monaco-prompt-table"]) 165 | 166 | const theadEl = this.elements.thead = document.createElement("thead") 167 | tableEl.appendChild(theadEl) 168 | theadEl.innerHTML = ` 169 | ID 170 | Title 171 | Pos 172 | ` 173 | 174 | const tbodyEl = this.elements.tbody = document.createElement("tbody") 175 | tableEl.appendChild(tbodyEl) 176 | 177 | tableContainerEl.appendChild(tableEl) 178 | 179 | const containerEl = this.elements.container 180 | containerEl.appendChild(tableContainerEl) 181 | } 182 | 183 | createWidgetHeader() { 184 | const headEl = this.elements.header = document.createElement("div") 185 | headEl.classList.add(style["webui-monaco-prompt-header"]) 186 | 187 | const inputContainerEl = this.elements.inputContainer = document.createElement("div") 188 | inputContainerEl.classList.add(style["webui-monaco-prompt-input"]) 189 | 190 | const inputEl = this.elements.input = document.createElement("input") 191 | inputEl.type = "text" 192 | inputEl.placeholder = "Find" 193 | inputEl.classList.add(style["webui-monaco-prompt-input-text"]) 194 | inputEl.addEventListener("keydown", (ev) => { 195 | return this.findInputHandler(ev) 196 | }) 197 | inputContainerEl.appendChild(inputEl) 198 | 199 | const controlsEl = document.createElement("div") 200 | controlsEl.classList.add("controls", style["webui-monaco-prompt-control"]) 201 | controlsEl.innerHTML = ` 202 |
203 |
204 |
205 | ` 206 | controlsEl.querySelectorAll("." + style["webui-monaco-prompt-toggle"]).forEach((element: HTMLElement) => { 207 | element.addEventListener("click", (ev) => { 208 | const cssClass = style["webui-monaco-prompt-toggle-checked"] 209 | element.classList.toggle(cssClass) 210 | element.dataset.checked = element.classList.contains(cssClass) ? "on" : "off" 211 | }) 212 | }) 213 | inputContainerEl.appendChild(controlsEl) 214 | headEl.appendChild(inputContainerEl) 215 | 216 | const containerEl = this.elements.container 217 | containerEl.appendChild(headEl) 218 | } 219 | 220 | findInputHandler(ev: KeyboardEvent) { 221 | if (ev.key !== "Enter") { 222 | return 223 | } 224 | this.execute() 225 | } 226 | 227 | getMatchFlags() { 228 | const headEl = this.elements.header as HTMLDivElement 229 | 230 | const matchCase = headEl.querySelector(".codicon-case-sensitive")?.dataset.checked === "on" 231 | const matchWordOnly = headEl.querySelector(".codicon-whole-word")?.dataset.checked === "on" 232 | const isRegex = headEl.querySelector(".codicon-regex")?.dataset.checked === "on" 233 | 234 | return { 235 | matchCase, 236 | matchWordOnly, 237 | isRegex, 238 | } 239 | } 240 | 241 | execute() { 242 | const inputEl = this.elements.input as HTMLInputElement 243 | const matchFlags = this.getMatchFlags() 244 | 245 | const nodeFindMatches = utils.find(inputEl.value, matchFlags.isRegex, matchFlags.matchCase, matchFlags.matchWordOnly) 246 | 247 | const tbodyEl = this.elements.tbody 248 | tbodyEl.classList.add("comfy-list-item") 249 | 250 | this.clearElements(tbodyEl) 251 | 252 | for (const nodeFindMatch of nodeFindMatches) { 253 | const app = this._app 254 | const webuiMonacoPromptId = nodeFindMatch.instanceId 255 | const node = link[webuiMonacoPromptId].node 256 | 257 | const trEl = document.createElement("tr") 258 | trEl.dataset.nodeId = node.id 259 | trEl.dataset.instanceId = "" + webuiMonacoPromptId 260 | trEl.dataset.startLine = "" + nodeFindMatch.match.range.startLineNumber 261 | trEl.dataset.startCol = "" + nodeFindMatch.match.range.startColumn 262 | 263 | for (const {cssClass, value, elementStyle} of [ 264 | { 265 | cssClass: style["webui-monaco-prompt-td"], 266 | value: node.id, 267 | elementStyle: {textAlign: "right"}, 268 | }, 269 | { 270 | cssClass: style["webui-monaco-prompt-td-expand"], 271 | value: node.title 272 | }, 273 | { 274 | cssClass: style["webui-monaco-prompt-td"], 275 | value: `Ln ${nodeFindMatch.match.range.startLineNumber}, Col ${nodeFindMatch.match.range.startColumn}`, 276 | elementStyle: {textAlign: "right"}, 277 | }, 278 | ]) { 279 | const tdEl = document.createElement("td") 280 | tdEl.classList.add(cssClass) 281 | tdEl.textContent = value 282 | if (elementStyle) { 283 | Object.assign(tdEl.style, elementStyle) 284 | } 285 | trEl.appendChild(tdEl) 286 | } 287 | 288 | trEl.addEventListener("click", (ev) => { 289 | const nodeId = trEl.dataset.nodeId 290 | const node = app.graph.getNodeById(nodeId) 291 | const instance = link[trEl.dataset.instanceId!] 292 | 293 | ev.stopPropagation() 294 | 295 | if (!instance) { 296 | return 297 | } 298 | 299 | const monaco = instance.monaco.monaco 300 | const lineNumber = (trEl.dataset.startLine as unknown as number) | 0 301 | const column = (trEl.dataset.startCol as unknown as number) | 0 302 | 303 | 304 | // 描画範囲外にいるときにエディタのスクロールなどの設定が無効化されることがあるため 305 | // ノードが描画範囲にはいるように移動後、次の描画タイミングでスクロール等を設定 306 | const setPosition = () => { 307 | monaco.focus() 308 | monaco.setPosition({ 309 | lineNumber: lineNumber, 310 | column: column, 311 | }) 312 | monaco.revealLine(lineNumber, Monaco.editor.ScrollType.Immediate) 313 | } 314 | 315 | // 上記タイミングで実行するため litegraph の canvas 更新後に実行される onDrawOverlay コールバックを使用 316 | // コールバックでさらに requestAnimationFrame で遅延させるのは 317 | // (現在) -> canvasの更新(ここで描画位置が変更されるが反映はされていない) -> 描画されている状態でスクロールなどを更新 318 | // とするため 319 | const onDrawOverlay = this._app.canvas.onDrawOverlay 320 | app.canvas.onDrawOverlay = function(ctx: any) { 321 | if (onDrawOverlay) { 322 | onDrawOverlay.apply(this, arguments) 323 | } 324 | 325 | requestAnimationFrame(setPosition) 326 | 327 | app.canvas.onDrawOverlay = onDrawOverlay 328 | } 329 | 330 | // ノードが描画領域の中心にくるように移動 331 | app.canvas.centerOnNode(node) 332 | utils.setActiveNode(app, node) 333 | }) 334 | 335 | setTooltip(trEl) 336 | 337 | tbodyEl.appendChild(trEl) 338 | } 339 | } 340 | 341 | clearElements(element: HTMLElement) { 342 | while (element.hasChildNodes()) { 343 | element.removeChild(element.firstChild!) 344 | } 345 | tooltip.style.display = "none" 346 | } 347 | } 348 | 349 | const createFindWidgetTooltip = () => { 350 | const tooltip = $el("div", { 351 | className: ["text-sm"].join(" "), 352 | style: { 353 | display: "none", 354 | position: "fixed", 355 | backgroundColor: "var(--bg-color)", 356 | color: "var(--fg-color)", 357 | overflowWrap: "anywhere", 358 | zIndex: 999999, 359 | } 360 | }) as HTMLElement 361 | 362 | const scopedStyle = document.createElement("style") 363 | const body = document.createElement("div") 364 | 365 | tooltip.appendChild(scopedStyle) 366 | tooltip.appendChild(body) 367 | 368 | document.body.appendChild(tooltip) 369 | 370 | return tooltip 371 | } 372 | const tooltip = createFindWidgetTooltip() 373 | const tooltipBody = tooltip.querySelector("div") as HTMLDivElement 374 | const tooltipStyle = tooltip.querySelector("style") as HTMLStyleElement 375 | const setTooltip = (targetElement: HTMLElement) => { 376 | targetElement.addEventListener("mouseenter", (ev) => { 377 | while (tooltipBody.firstChild) { 378 | tooltipBody.removeChild(tooltipBody.firstChild) 379 | } 380 | const instance = link[targetElement.dataset.instanceId!] 381 | 382 | // instance removed 383 | if (!instance) { 384 | return 385 | } 386 | 387 | const monaco = instance.monaco 388 | const line = +(targetElement.dataset.startLine!) 389 | const range = TooltipSurroundingLines 390 | 391 | if (monaco.instanceStyle) { 392 | tooltipStyle.textContent = `@scope { 393 | ${monaco.instanceStyle.textContent} 394 | .monaco-editor { 395 | padding: 1rem; 396 | } 397 | }` 398 | } 399 | 400 | const contentElement = monaco.getLinesTable(Math.max(1, line - range), line, line + range) 401 | tooltipBody.appendChild(contentElement) 402 | 403 | tooltip.style.display = "block" 404 | }) 405 | 406 | targetElement.addEventListener("mousemove", (ev) => { 407 | tooltip.style.left = (ev.clientX + TooltipDistance) + 'px' 408 | tooltip.style.top = (ev.clientY + TooltipDistance) + 'px' 409 | 410 | if (document.documentElement.clientHeight < ev.clientY + TooltipDistance + tooltip.getBoundingClientRect().height) { 411 | tooltip.style.top = (ev.clientY - TooltipDistance - tooltipBody.getBoundingClientRect().height) + 'px' 412 | } 413 | }) 414 | targetElement.addEventListener("mouseout", (ev) => { 415 | tooltip.style.display = "none" 416 | }) 417 | } 418 | 419 | export { 420 | FindWidget, 421 | FindWidgetElements, 422 | } -------------------------------------------------------------------------------- /src/comfyui/widget/index.css: -------------------------------------------------------------------------------- 1 | div.webui-monaco-prompt-container { 2 | display: flex; 3 | flex-flow: column; 4 | } 5 | div.webui-monaco-prompt-header { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | div.webui-monaco-prompt-input { 10 | display: flex; 11 | width: 100%; 12 | margin: 2px 0; 13 | } 14 | div.webui-monaco-prompt-control { 15 | display: flex; 16 | flex-flow: nowrap; 17 | } 18 | div.webui-monaco-prompt-toggle { 19 | } 20 | div.webui-monaco-prompt-toggle-checked { 21 | border-width: 1px; 22 | border-color: var(--fg-color); 23 | } 24 | 25 | input.webui-monaco-prompt-input-text[type="text"] { 26 | color: var(--input-text); 27 | background-color: var(--comfy-input-bg); 28 | width: 100%; 29 | } 30 | .webui-monaco-prompt-table-container { 31 | color: var(--input-text); 32 | display: flex; 33 | height: 100vh; 34 | overflow: auto; 35 | margin-top: 11px; 36 | } 37 | .webui-monaco-prompt-table { 38 | font-size: 11px; 39 | width: 100%; 40 | height: 0; 41 | } 42 | .webui-monaco-prompt-table-container thead > tr > th { 43 | border-bottom: 1px solid var(--fg-color); 44 | } 45 | .webui-monaco-prompt-table-container tbody > tr { 46 | cursor: pointer; 47 | /* 48 | display: flex; 49 | */ 50 | } 51 | .webui-monaco-prompt-table-container tbody > tr:hover { 52 | background-color: var(--fg-color) !important; 53 | color: var(--bg-color) !important; 54 | } 55 | .webui-monaco-prompt-table-container tbody > tr:nth-child(even) { 56 | background-color: var(--tr-even-bg-color); 57 | } 58 | .webui-monaco-prompt-table-container tbody > tr:nth-child(odd) { 59 | background-color: var(--tr-odd-bg-color); 60 | } 61 | .webui-monaco-prompt-td { 62 | /* 63 | flex: 0 0 auto; 64 | */ 65 | text-wrap: nowrap; 66 | } 67 | .webui-monaco-prompt-td-expand { 68 | /* 69 | flex: 1 1 auto; 70 | */ 71 | width: 100%; 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/comfyui/widget/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./find_widget" 2 | export * from "./replace_widget" -------------------------------------------------------------------------------- /src/comfyui/widget/replace_widget.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as utils from "../utils" 3 | import {default as style} from "./index.css" 4 | import { FindWidget, FindWidgetElements } from "./find_widget" 5 | 6 | interface ReplaceWidgetElements extends FindWidgetElements { 7 | replace: HTMLInputElement 8 | } 9 | class ReplaceWidget extends FindWidget { 10 | elements: ReplaceWidgetElements 11 | constructor(app: any) { 12 | super(app) 13 | this.elements = {} as ReplaceWidgetElements 14 | } 15 | createWidgetHeader() { 16 | super.createWidgetHeader() 17 | const headEl = this.elements.header 18 | 19 | const inputContainerEl = document.createElement("div") 20 | inputContainerEl.classList.add(style["webui-monaco-prompt-input"]) 21 | 22 | const replaceEl = this.elements.replace = document.createElement("input") 23 | replaceEl.type = "text" 24 | replaceEl.placeholder = "Replace" 25 | replaceEl.classList.add(style["webui-monaco-prompt-input-text"]) 26 | replaceEl.addEventListener("keydown", (ev) => { 27 | return this.findInputHandler(ev) 28 | }) 29 | 30 | inputContainerEl.appendChild(replaceEl) 31 | headEl.appendChild(inputContainerEl) 32 | } 33 | createWidgetBody() { 34 | // pass 35 | } 36 | execute() { 37 | const inputEl = this.elements.input 38 | const replaceEl = this.elements.replace 39 | const matchFlags = this.getMatchFlags() 40 | 41 | utils.replace(inputEl.value, replaceEl.value, matchFlags.isRegex, matchFlags.matchCase, matchFlags.matchWordOnly) 42 | } 43 | } 44 | 45 | export { 46 | ReplaceWidget, 47 | ReplaceWidgetElements, 48 | } -------------------------------------------------------------------------------- /src/completion.ts: -------------------------------------------------------------------------------- 1 | import { editor, languages, Position } from 'monaco-editor/esm/vs/editor/editor.api' 2 | import { parse } from 'csv-parse/browser/esm/sync' 3 | 4 | interface CompletionData { 5 | [type: string]: string[] 6 | } 7 | 8 | interface CompletionItemExtention { 9 | count: string 10 | } 11 | type CompletionItem = languages.CompletionItem & CompletionItemExtention 12 | 13 | interface State { 14 | threshold: number 15 | filteredTags: Partial[] 16 | isRplaceUnderscore: boolean 17 | loadedCSV: { 18 | [key: string]: string 19 | } 20 | enabledCSV: string[] 21 | } 22 | 23 | const tags: Partial[] = [] 24 | const data: CompletionData = {} 25 | const state: State = { 26 | threshold: 100, 27 | filteredTags: [], 28 | isRplaceUnderscore: false, 29 | loadedCSV: {}, 30 | enabledCSV: [], 31 | } 32 | 33 | const updateReplaceUnderscore = (replace: boolean) => { 34 | state.isRplaceUnderscore = replace 35 | } 36 | 37 | const getReplaceUnderscore = () => { 38 | return state.isRplaceUnderscore 39 | } 40 | 41 | const checkThreshold = (item: CompletionItem) => { 42 | if (isNaN(item.count as any)) { 43 | return true 44 | } 45 | return (item.count as unknown as number) > state.threshold 46 | } 47 | 48 | const filterTags = (items: CompletionItem[]) => { 49 | return items.filter(checkThreshold) 50 | } 51 | 52 | const addData = (type: string, list: string[], clear = false) => { 53 | if (!data[type] || clear) { 54 | data[type] = list.slice() 55 | return 56 | } 57 | 58 | Array.prototype.push.apply(data[type], list) 59 | } 60 | 61 | const clearCSV = () => { 62 | tags.length = 0 63 | state.filteredTags.length = 0 64 | } 65 | 66 | const loadCSV = (filename: string, csv: string) => { 67 | if (tags.length > 0) { 68 | clearCSV() 69 | } 70 | 71 | addCSV(filename, csv) 72 | } 73 | 74 | const addCSV = (filename: string, csv: string) => { 75 | state.loadedCSV[filename] = csv 76 | 77 | if (!state.enabledCSV.includes(filename)) { 78 | state.enabledCSV.push(filename) 79 | } 80 | 81 | _addCSV(csv, filename) 82 | } 83 | 84 | const addLoadedCSV = (files: string[]) => { 85 | const diff = compareArray(state.enabledCSV, files) 86 | 87 | if (diff.equal) { 88 | return 89 | } 90 | 91 | state.enabledCSV = files 92 | if (diff.remove.length > 0) { 93 | clearCSV() 94 | } else if (diff.add.length > 0) { 95 | files = diff.add 96 | } 97 | 98 | for (const filename of files) { 99 | const csv = state.loadedCSV[filename] 100 | if (!csv) { 101 | console.error(`"${filename}" is not loaded`) 102 | continue 103 | } 104 | _addCSV(csv, filename) 105 | } 106 | } 107 | 108 | const compareArray = (array1: any[], array2: any[]) => { 109 | const result = { 110 | equal: true, 111 | add: [] as any[], 112 | remove: [] as any[], 113 | } 114 | const array2map = new Map(array2.map(v => [v, true])) 115 | 116 | for (const array1value of array1) { 117 | if (!array2map.has(array1value)) { 118 | result.equal = false 119 | result.remove.push(array1value) 120 | } else { 121 | array2map.delete(array1value) 122 | } 123 | } 124 | if (array2map.size > 0) { 125 | result.equal = false 126 | } 127 | for (const key of array2map.keys()) { 128 | result.add.push(key) 129 | } 130 | 131 | return result 132 | } 133 | 134 | const _addCSV = (csv: string, sourceName?: string) => { 135 | for (const row of parse(csv, {columns: ["tag", "category", "count", "alias"]})) { 136 | const countString = isNaN(row.count) ? row.count : (+row.count).toLocaleString() 137 | const description = sourceName ? 138 | [`(${sourceName})`, countString].join(" ") : 139 | countString 140 | const item: Partial = { 141 | label: { 142 | label: row.tag, 143 | description: description, 144 | }, 145 | kind: languages.CompletionItemKind.Value, 146 | insertText: escape(row.tag), 147 | count: row.count, 148 | } 149 | tags.push(item) 150 | 151 | // filtered 152 | if (checkThreshold(item as CompletionItem)) { 153 | state.filteredTags.push(item) 154 | } 155 | 156 | for (const alias of row.alias.split(",")) { 157 | if (alias.length === 0) { 158 | continue 159 | } 160 | const item: Partial = { 161 | label: { 162 | label: alias, 163 | detail: ` -> ${row.tag}`, 164 | description: description, 165 | }, 166 | kind: languages.CompletionItemKind.Value, 167 | insertText: escape(row.tag), 168 | count: row.count, 169 | } 170 | tags.push(item) 171 | 172 | // filtered 173 | if (checkThreshold(item as CompletionItem)) { 174 | state.filteredTags.push(item) 175 | } 176 | } 177 | } 178 | } 179 | 180 | const updateFilteredTags = () => { 181 | state.filteredTags = filterTags(tags as CompletionItem[]) 182 | } 183 | 184 | const getCount = () => { 185 | return tags.length 186 | } 187 | 188 | const getLoadedCSV = () => { 189 | return Object.keys(state.loadedCSV) 190 | } 191 | 192 | const getEnabledCSV = () => { 193 | return state.enabledCSV.slice() 194 | } 195 | 196 | const escape = (str: string) => { 197 | return str 198 | .replaceAll(/([\(\)\[\]])/g, '\\$1') 199 | } 200 | 201 | const getWordPosition = (model:editor.ITextModel, position: Position): editor.IWordAtPosition => { 202 | const untilPosition = model.getWordUntilPosition(position) 203 | const wordPosition = model.getWordAtPosition(position) 204 | 205 | if (!wordPosition) { 206 | return untilPosition 207 | } 208 | 209 | return { 210 | startColumn: untilPosition.startColumn, 211 | endColumn: wordPosition.endColumn, 212 | word: wordPosition.word 213 | } 214 | } 215 | 216 | const provider: languages.CompletionItemProvider = { 217 | triggerCharacters: "<1234567890".split(''), 218 | provideCompletionItems: function (model: editor.ITextModel, position: Position, context: languages.CompletionContext) { 219 | let wordPosition = getWordPosition(model, position) 220 | const prevChar = model.getValueInRange({ 221 | startLineNumber: position.lineNumber, 222 | startColumn: wordPosition.startColumn - 1, 223 | endLineNumber: position.lineNumber, 224 | endColumn: wordPosition.startColumn, 225 | }) 226 | const triggerCharacter = context.triggerCharacter || prevChar 227 | 228 | // tags 229 | let suggestTags: languages.CompletionItem[] 230 | switch (triggerCharacter) { 231 | case '<': 232 | suggestTags = [] 233 | break 234 | default: 235 | suggestTags = state.filteredTags.map((item: Partial) => { 236 | return Object.assign( 237 | { 238 | range: { 239 | startLineNumber: position.lineNumber, 240 | startColumn: wordPosition.startColumn, 241 | endLineNumber: position.lineNumber, 242 | endColumn: wordPosition.endColumn 243 | } 244 | }, 245 | item, 246 | state.isRplaceUnderscore ? {insertText: item.insertText!.replaceAll('_', ' ')} : {}, 247 | ) as languages.CompletionItem 248 | }) 249 | break 250 | } 251 | 252 | // extra networks 253 | const extra: languages.CompletionItem[] = [] 254 | for (const [type, list] of Object.entries(data)) { 255 | if (type === "embedding" && triggerCharacter === "<") { 256 | continue 257 | } 258 | for (const word of list) { 259 | const escapedWord = escape(word) 260 | const insertText = `${type}:${escapedWord}:1.0` 261 | extra.push({ 262 | label: {label: escapedWord, description: type}, 263 | kind: languages.CompletionItemKind.Text, 264 | insertText: 265 | type === "embedding" ? escapedWord : 266 | triggerCharacter === "<" ? insertText : 267 | `<${insertText}>`, 268 | detail: type, 269 | range: { 270 | startLineNumber: position.lineNumber, 271 | startColumn: wordPosition.startColumn, 272 | endLineNumber: position.lineNumber, 273 | endColumn: wordPosition.endColumn, 274 | } 275 | }) 276 | } 277 | } 278 | return { 279 | suggestions: suggestTags.concat(extra), 280 | } 281 | }, 282 | } 283 | 284 | type CreateDynamicSuggestFunc = () => Promise[]> 285 | type CreateDynamicSuggest = (suggestFunc: CreateDynamicSuggestFunc, suggestCallback: () => void) => languages.CompletionItemProvider 286 | const createDynamicSuggest: CreateDynamicSuggest = (suggestFunc, suggestOnDispose) => { 287 | return { 288 | provideCompletionItems: async function(model: editor.ITextModel, position: Position, context: languages.CompletionContext) { 289 | const suggestWordList = await suggestFunc() 290 | 291 | suggestWordList.forEach(partialCompletionItem => { 292 | if (!partialCompletionItem.kind) { 293 | partialCompletionItem.kind = languages.CompletionItemKind.Text 294 | } 295 | partialCompletionItem.range = { 296 | startLineNumber: position.lineNumber, 297 | startColumn: position.column, 298 | endLineNumber: position.lineNumber, 299 | endColumn: position.column, 300 | } 301 | }) 302 | 303 | return { 304 | suggestions: suggestWordList as languages.CompletionItem[], 305 | dispose: () => { 306 | suggestOnDispose() 307 | } 308 | } 309 | } 310 | } 311 | } 312 | 313 | export { 314 | provider, 315 | createDynamicSuggest, 316 | clearCSV, 317 | addCSV, 318 | loadCSV, 319 | getCount, 320 | getEnabledCSV, 321 | getLoadedCSV, 322 | addLoadedCSV, 323 | addData, 324 | updateFilteredTags, 325 | updateReplaceUnderscore, 326 | getReplaceUnderscore, 327 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' 2 | import { initVimMode } from 'monaco-vim' 3 | import { sdPrompt, sdDynamicPrompt } from './languages' 4 | import { provider, createDynamicSuggest, addCSV, loadCSV, getCount, addData, clearCSV, getReplaceUnderscore, updateReplaceUnderscore, getLoadedCSV, addLoadedCSV, getEnabledCSV } from './completion' 5 | import { addActionWithCommandOption, addActionWithSubMenu, ActionsPartialDescripter, getMenuId, updateSubMenu, removeSubMenu } from './monaco_utils' 6 | import { MultipleSelectInstance, multipleSelect} from 'multiple-select-vanilla' 7 | // @ts-ignore 8 | import { ContextKeyExpr } from 'monaco-editor/esm/vs/platform/contextkey/common/contextkey' 9 | // @ts-ignore 10 | import { IQuickInputService } from 'monaco-editor/esm/vs/platform/quickinput/common/quickinput' 11 | // @ts-ignore 12 | import { StandaloneThemeService } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneThemeService' 13 | // @ts-ignore 14 | import { StringBuilder } from 'monaco-editor/esm/vs/editor/common/core/stringBuilder' 15 | // @ts-ignore 16 | import { ViewLineOptions } from 'monaco-editor/esm/vs/editor/browser/viewParts/lines/viewLine' 17 | // @ts-ignore 18 | import { RenderLineInput, renderViewLine } from 'monaco-editor/esm/vs/editor/common/viewLayout/viewLineRenderer' 19 | // @ts-ignore 20 | import { EditorFontLigatures } from 'monaco-editor/esm/vs/editor/common/config/editorOptions' 21 | // @ts-ignore 22 | import { InlineDecoration } from 'monaco-editor/esm/vs/editor/common/viewModel' 23 | // @ts-ignore 24 | import { ViewportData } from 'monaco-editor/esm/vs/editor/common/viewLayout/viewLinesViewportData' 25 | // @ts-ignore 26 | import { LineDecoration } from 'monaco-editor/esm/vs/editor/common/viewLayout/lineDecorations' 27 | // @ts-ignore 28 | import { View } from 'monaco-editor/esm/vs/editor/browser/view' 29 | // @ts-ignore 30 | import { SuggestController } from 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController' 31 | 32 | // copy from viewModel.ts 33 | const enum InlineDecorationType { 34 | Regular = 0, 35 | Before = 1, 36 | After = 2, 37 | RegularAffectingLetterSpacing = 3 38 | } 39 | 40 | import "multiple-select-vanilla/dist/styles/css/multiple-select.css" 41 | 42 | 43 | import style from "./styles/index.css" 44 | import { deepEqual } from 'fast-equals' 45 | 46 | 47 | // define prompt language 48 | const sdLanguages = [ 49 | {id: "sd-prompt", lang: sdPrompt}, 50 | {id: "sd-dynamic-prompt", lang: sdDynamicPrompt} 51 | ] 52 | const addLanguages = (languages: typeof sdLanguages) => { 53 | for (const {id, lang} of languages) { 54 | monaco.languages.register({id: id}) 55 | monaco.languages.setMonarchTokensProvider(id, lang.language) 56 | monaco.languages.setLanguageConfiguration(id, lang.conf) 57 | monaco.languages.registerCompletionItemProvider(id, provider) 58 | } 59 | } 60 | addLanguages(sdLanguages) 61 | 62 | const ContextPrefix = "monacoPromptEditor" 63 | const FontSizePreset = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48] 64 | 65 | interface PromptEditorGlobal { 66 | instances: {[key: number]: PromptEditor} 67 | } 68 | 69 | type CodeEditor = monaco.editor.IStandaloneCodeEditor & { 70 | _themeService: StandaloneThemeService, 71 | getConfiguration: () => typeof monaco.editor.EditorOptions, 72 | _modelData: { 73 | view: View 74 | }, 75 | } 76 | 77 | // global settings 78 | const settings: PromptEditorGlobal = { 79 | instances: {}, 80 | } 81 | let id = 0 82 | let currentFocusInstance: number | null = null 83 | 84 | interface PromptEditorOptions { 85 | focus: boolean; 86 | autoLayout: boolean; 87 | handleTextAreaValue: boolean; 88 | overlayZIndex: number; 89 | } 90 | 91 | interface PromptEditorSettings { 92 | minimap: boolean, 93 | lineNumbers: boolean, 94 | replaceUnderscore: boolean, 95 | mode: PromptEditorMode, 96 | theme: string, 97 | language: string, 98 | showHeader: boolean, 99 | fontSize: number, 100 | fontFamily: string, 101 | csvToggle: { 102 | [key: string]: boolean 103 | }, 104 | } 105 | 106 | interface PromptEditorElements { 107 | container: ShadowRoot 108 | header: HTMLElement 109 | main: HTMLElement 110 | footer: HTMLElement 111 | inner: HTMLDivElement 112 | monaco: HTMLDivElement 113 | language: HTMLSelectElement 114 | theme: HTMLSelectElement 115 | keyBindings: HTMLSelectElement 116 | status: HTMLDivElement 117 | lineNumbers: HTMLInputElement 118 | minimap: HTMLInputElement 119 | replaceUnderscore: HTMLInputElement 120 | overflowGuard: HTMLDivElement 121 | overflowContent: HTMLDivElement 122 | overflowOverlay: HTMLDivElement 123 | fontsize: HTMLSelectElement 124 | autocomplete: MultipleSelectInstance 125 | autocompleteElement: HTMLLabelElement 126 | } 127 | 128 | interface PromptEditorCheckboxParam { 129 | label: string 130 | title?: string 131 | isEnabledCallback: () => boolean 132 | callback: (label: HTMLLabelElement, input: HTMLInputElement) => void 133 | toggleCallback: (ev: Event) => void 134 | } 135 | 136 | const PromptEditorMode = { 137 | NORMAL: 'NORMAL', 138 | VIM: 'VIM', 139 | } 140 | type PromptEditorMode = typeof PromptEditorMode[keyof typeof PromptEditorMode] 141 | 142 | class PromptEditor extends HTMLElement { 143 | elements: Partial = {} 144 | mode: PromptEditorMode = PromptEditorMode.NORMAL 145 | monaco: CodeEditor 146 | theme: string 147 | showHeader: boolean 148 | vim: any // monaco-vim instance 149 | textareaDescriptor: PropertyDescriptor 150 | textareaDisplay: string 151 | onChangeShowHeaderCallbacks: Array<() => void> 152 | onChangeShowHeaderBeforeSyncCallbacks: Array<() => void> 153 | onChangeShowLineNumbersCallbacks: Array<() => void> 154 | onChangeShowLineNumbersBeforeSyncCallbacks: Array<() => void> 155 | onChangeShowMinimapCallbacks: Array<() => void> 156 | onChangeShowMinimapBeforeSyncCallbacks: Array<() => void> 157 | onChangeReplaceUnderscoreCallbacks: Array<() => void> 158 | onChangeReplaceUnderscoreBeforeSyncCallbacks: Array<() => void> 159 | onChangeThemeCallbacks: Array<() => void> 160 | onChangeThemeBeforeSyncCallbacks: Array<() => void> 161 | onChangeModeCallbacks: Array<() => void> 162 | onChangeModeBeforeSyncCallbacks: Array<() => void> 163 | onChangeLanguageCallbacks: Array<() => void> 164 | onChangeLanguageBeforeSyncCallbacks: Array<() => void> 165 | onChangeFontSizeCallbacks: Array<() => void> 166 | onChangeFontSizeBeforeSyncCallbacks: Array<() => void> 167 | onChangeFontFamilyCallbacks: Array<() => void> 168 | onChangeFontFamilyBeforeSyncCallbacks: Array<() => void> 169 | onChangeAutoCompleteToggleCallbacks: Array<() => void> 170 | onChangeAutoCompleteToggleBeforeSyncCallbacks: Array<() => void> 171 | _id: number 172 | 173 | constructor(textarea: HTMLTextAreaElement, options: Partial={}) { 174 | super() 175 | 176 | this._id = id++ 177 | this.textareaDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(textarea), 'value')! 178 | 179 | const container = this.elements.container = this.attachShadow({mode: 'open'}) 180 | const headerElement = this.elements.header = document.createElement('header') 181 | const mainElement= this.elements.main = document.createElement('main') 182 | const footerElement = this.elements.footer = document.createElement('footer') 183 | const innerElement = this.elements.inner = document.createElement('div') 184 | const monacoElement= this.elements.monaco = document.createElement('div') 185 | const statusElement = this.elements.status = document.createElement('div') 186 | 187 | mainElement.appendChild(monacoElement) 188 | footerElement.appendChild(statusElement) 189 | 190 | innerElement.appendChild(headerElement) 191 | innerElement.appendChild(mainElement) 192 | innerElement.appendChild(footerElement) 193 | 194 | container.appendChild(innerElement) 195 | 196 | innerElement.classList.add(style.inner) 197 | mainElement.classList.add(style.main) 198 | headerElement.classList.add(style.header) 199 | footerElement.classList.add(style.footer) 200 | monacoElement.classList.add(style.monaco) 201 | statusElement.classList.add(style.status) 202 | 203 | this.onChangeShowHeaderCallbacks = [] 204 | this.onChangeShowHeaderBeforeSyncCallbacks = [] 205 | this.onChangeShowLineNumbersCallbacks = [] 206 | this.onChangeShowLineNumbersBeforeSyncCallbacks = [] 207 | this.onChangeShowMinimapCallbacks = [] 208 | this.onChangeShowMinimapBeforeSyncCallbacks = [] 209 | this.onChangeReplaceUnderscoreCallbacks = [] 210 | this.onChangeReplaceUnderscoreBeforeSyncCallbacks = [] 211 | this.onChangeThemeCallbacks = [] 212 | this.onChangeThemeBeforeSyncCallbacks = [] 213 | this.onChangeModeCallbacks = [] 214 | this.onChangeModeBeforeSyncCallbacks = [] 215 | this.onChangeLanguageCallbacks = [] 216 | this.onChangeLanguageBeforeSyncCallbacks = [] 217 | this.onChangeFontSizeCallbacks = [] 218 | this.onChangeFontSizeBeforeSyncCallbacks = [] 219 | this.onChangeFontFamilyCallbacks = [] 220 | this.onChangeFontFamilyBeforeSyncCallbacks = [] 221 | this.onChangeAutoCompleteToggleCallbacks = [] 222 | this.onChangeAutoCompleteToggleBeforeSyncCallbacks = [] 223 | 224 | const editor = this.monaco = monaco.editor.create(monacoElement, { 225 | value: textarea.value, 226 | //language: languageId, 227 | bracketPairColorization: { 228 | enabled: true, 229 | }, 230 | automaticLayout: true, 231 | wordWrap: 'on', 232 | //fixedOverflowWidgets: true, 233 | } as any) as CodeEditor 234 | this.polyfillMonacoEditorConfiguration() 235 | 236 | this.showHeader = true 237 | this.theme = this.getThemeId() 238 | 239 | this.changeMode(PromptEditorMode.VIM) 240 | 241 | editor.onDidFocusEditorWidget(() => { 242 | currentFocusInstance = this.getInstanceId() 243 | }) 244 | 245 | if (options.focus) { 246 | editor.focus() 247 | } 248 | 249 | editor.getModel()?.onDidChangeContent((e) => { 250 | this.textareaDescriptor.set?.call(textarea, this.monaco.getValue()) 251 | 252 | // fire fake input event 253 | const input = new InputEvent('input') 254 | Object.defineProperty(input, 'target', {writable: false, value: textarea}) 255 | textarea.dispatchEvent(input) 256 | }) 257 | 258 | if (options.handleTextAreaValue) { 259 | this.hookTextAreaElement(textarea) 260 | } 261 | 262 | this.initHeader() 263 | if (options.autoLayout) { 264 | this.handleResize() 265 | } 266 | this.copyStyleToShadow() 267 | 268 | this.textareaDisplay = textarea.style.display 269 | textarea.style.display = 'none' 270 | 271 | const overflowGuard = this.elements.main!.querySelector('.overflow-guard')! as HTMLDivElement 272 | this.elements.overflowGuard = overflowGuard 273 | const overflowContent = this.elements.main!.querySelector('.overflowingContentWidgets')! as HTMLDivElement 274 | this.elements.overflowContent = overflowContent 275 | const overflowOverlay = this.elements.main!.querySelector('.overflowingOverlayWidgets')! as HTMLDivElement 276 | this.elements.overflowOverlay = overflowOverlay 277 | this.fixedOverflowWidgetWorkaround([overflowContent, overflowOverlay], options) 278 | 279 | this.updateAutoComplete() 280 | this.setContextMenu() 281 | 282 | // init context 283 | this.setSettings(Object.assign({}, this.getSettings(), { 284 | csvToggle: Object.fromEntries(getEnabledCSV().map(csvName => [this.createContextKey("csv", csvName), true])), 285 | }), true) 286 | 287 | this.setEventHandler() 288 | 289 | settings.instances[this._id] = this 290 | } 291 | 292 | getCurrentFocus() { 293 | if (!settings) { 294 | return null 295 | } 296 | if (!settings.instances) { 297 | return null 298 | } 299 | if (currentFocusInstance === null) { 300 | return null 301 | } 302 | if (!settings.instances[currentFocusInstance]) { 303 | console.warn("instance not found: ", currentFocusInstance, settings.instances) 304 | return null 305 | } 306 | 307 | return settings.instances[currentFocusInstance] 308 | } 309 | 310 | dispose() { 311 | if (this.monaco) { 312 | const model = this.monaco.getModel() 313 | if (model) { 314 | model.dispose() 315 | } 316 | this.monaco.dispose() 317 | } 318 | delete settings.instances[this._id] 319 | } 320 | 321 | // fixedOverflowWidget相当のworkaroundを行う 322 | fixedOverflowWidgetWorkaround(elements: HTMLElement[], options: Partial) { 323 | const overflowGuard = this.elements.overflowGuard! 324 | overflowGuard.style.position = 'absolute' 325 | 326 | for (const overlay of elements) { 327 | overlay.style.position = 'fixed' 328 | } 329 | 330 | const scrollbar = overflowGuard.querySelector(".scrollbar.vertical") as HTMLElement 331 | if (scrollbar) { 332 | scrollbar.style.zIndex = "6" 333 | } 334 | 335 | this.setOverlayZIndex(10) // default z-index 336 | if (typeof(options.overlayZIndex) === "number") { 337 | this.setOverlayZIndex(options.overlayZIndex) 338 | } 339 | } 340 | 341 | createContextKey(...args: string[]) { 342 | return [ContextPrefix, ...args].join('.') 343 | } 344 | 345 | setContextMenu() { 346 | addActionWithCommandOption(this.monaco, { 347 | id: 'header', 348 | label: 'Show Header', 349 | order: 0, 350 | groupId: "monaco-prompt-editor", 351 | run: () => { 352 | this.changeShowHeader(!this.getContext(this.createContextKey("showHeader"))) 353 | this.syncShowHeader() 354 | }, 355 | commandOptions: { 356 | toggled: { 357 | condition: ContextKeyExpr.deserialize(this.createContextKey("showHeader")) 358 | } 359 | }, 360 | }) 361 | addActionWithCommandOption(this.monaco, { 362 | id: 'minimap', 363 | label: 'Show Minimap', 364 | order: 1, 365 | groupId: "monaco-prompt-editor", 366 | run: () => { 367 | this.changeShowMinimap(!this.getContext(this.createContextKey("minimap"))) 368 | this.syncMinimap() 369 | }, 370 | commandOptions: { 371 | toggled: { 372 | condition: ContextKeyExpr.deserialize(this.createContextKey("minimap")) 373 | } 374 | }, 375 | }) 376 | addActionWithCommandOption(this.monaco, { 377 | id: 'line_numbers_show', 378 | label: 'LineNum', 379 | order: 2, 380 | groupId: "monaco-prompt-editor", 381 | run: () => { 382 | this.changeShowLineNumbers(!this.getContext(this.createContextKey("lineNumbers"))) 383 | this.syncLineNumbers() 384 | }, 385 | commandOptions: { 386 | toggled: { 387 | condition: ContextKeyExpr.deserialize(this.createContextKey("lineNumbers")) 388 | } 389 | }, 390 | }) 391 | addActionWithCommandOption(this.monaco, { 392 | id: 'underscore_replace', 393 | label: 'Replace Underscore', 394 | order: 3, 395 | groupId: "monaco-prompt-editor", 396 | run: () => { 397 | this.changeReplaceUnderscore(!this.getContext(this.createContextKey("replaceUnderscore"))) 398 | this.syncReplaceUnderscore() 399 | }, 400 | commandOptions: { 401 | toggled: { 402 | condition: ContextKeyExpr.deserialize(this.createContextKey("replaceUnderscore")) 403 | } 404 | }, 405 | }) 406 | addActionWithSubMenu(this.monaco, { 407 | title: "FontSize", 408 | context: ["MonacoPromptEditorFontSize", this._id].join("_"), 409 | group: 'monaco-prompt-editor', 410 | order: 4, 411 | actions: FontSizePreset.map(size => { 412 | return { 413 | id: ["fontsize", size].join("_"), 414 | label: ""+size, 415 | run: () => { 416 | this.changeFontSize(size) 417 | this.syncFontSize() 418 | }, 419 | commandOptions: { 420 | toggled: { 421 | condition: ContextKeyExpr.deserialize(`${this.createContextKey("fontSize")} == ${size}`) 422 | } 423 | } 424 | } 425 | }) 426 | }) 427 | this.monaco.addAction({ 428 | id: "fontfamily", 429 | label: "FontFamily", 430 | run: () => { 431 | (this.monaco as any).invokeWithinContext(async (accessor:any) => { 432 | const service = accessor.get(IQuickInputService) 433 | const inputBox = service.createInputBox() 434 | 435 | inputBox.placeholder = "input font family" 436 | inputBox.value = this.monaco.getOption(monaco.editor.EditorOption.fontFamily) 437 | inputBox.onDidAccept(() => { 438 | this.changeFontFamily(inputBox.value) 439 | this.syncFontFamily() 440 | inputBox.dispose() 441 | }) 442 | 443 | inputBox.show() 444 | }) 445 | }, 446 | contextMenuOrder: 5, 447 | contextMenuGroupId: 'monaco-prompt-editor', 448 | }) 449 | 450 | addActionWithSubMenu(this.monaco, { 451 | title: "Language", 452 | context: ["MonacoPromptEditorLanguage", this._id].join("_"), 453 | group: 'monaco-prompt-editor', 454 | order: 6, 455 | actions: monaco.languages.getLanguages().map(lang => { 456 | return { 457 | id: ["language", lang.id].join("_"), 458 | label: lang.id, 459 | run: () => { 460 | this.changeLanguage(lang.id) 461 | this.syncLanguage() 462 | }, 463 | commandOptions: { 464 | toggled: { 465 | condition: ContextKeyExpr.deserialize(`${this.createContextKey("language")} == ${lang.id}`) 466 | } 467 | } 468 | } 469 | }) 470 | }) 471 | addActionWithSubMenu(this.monaco, { 472 | title: "KeyBindings", 473 | context: ["MonacoPromptEditorKeyBindings", this._id].join("_"), 474 | group: 'monaco-prompt-editor', 475 | order: 7, 476 | actions: Object.values(PromptEditorMode).map(value => { 477 | return { 478 | id: ["keybinding", value].join("_"), 479 | label: value, 480 | run: () => { 481 | this.changeMode(value) 482 | this.syncKeyBindings() 483 | }, 484 | commandOptions: { 485 | toggled: { 486 | condition: ContextKeyExpr.deserialize(`${this.createContextKey("keybinding")} == ${value}`) 487 | } 488 | } 489 | } 490 | }) 491 | }) 492 | addActionWithSubMenu(this.monaco, { 493 | title: "Theme", 494 | context: ["MonacoPromptEditorTheme", this._id].join("_"), 495 | group: 'monaco-prompt-editor', 496 | order: 8, 497 | actions: Object.keys(this._mapToObject((this.monaco as any)._themeService._knownThemes)).map(value => { 498 | return { 499 | id: ["theme", value].join("_"), 500 | label: value, 501 | run: () => { 502 | this.changeTheme(value) 503 | this.syncTheme() 504 | }, 505 | commandOptions: { 506 | toggled: { 507 | condition: ContextKeyExpr.deserialize(`${this.createContextKey("theme")} == ${value}`) 508 | } 509 | } 510 | } 511 | }) 512 | }) 513 | } 514 | 515 | createOrUpdateSubMenu(title: string, id: string, group: string, order: number, actions: ActionsPartialDescripter[]) { 516 | const menuContext = [id, this.getInstanceId()].join("_") 517 | const subMenu = { 518 | title: title, 519 | context: menuContext, 520 | group: group, 521 | order: order, 522 | actions: actions, 523 | } 524 | 525 | if (!getMenuId(menuContext)) { 526 | addActionWithSubMenu(this.monaco, subMenu) 527 | } else { 528 | updateSubMenu(this.monaco, subMenu) 529 | } 530 | 531 | return menuContext 532 | } 533 | 534 | removeSubMenu(id: string) { 535 | removeSubMenu(id) 536 | } 537 | 538 | updateAutoComplete() { 539 | const csvfiles = getLoadedCSV() 540 | 541 | // context menu 542 | const order = 9 543 | this.createOrUpdateSubMenu("Autocomplete", "AutoComplete", "AutoComplete", order, csvfiles.map((filename) => { 544 | const basename = filename.split(".", 2)[0] 545 | const contextKey = this.createContextKey("csv", basename) 546 | return { 547 | id: ["autocomplete", basename].join("_"), 548 | label: basename, 549 | run: () => { 550 | const current = this.getContext(contextKey) 551 | this.changeAutoCompleteToggle(contextKey, !current, true) 552 | this.syncAutoCompleteToggle() 553 | }, 554 | commandOptions: { 555 | toggled: { 556 | condition: ContextKeyExpr.equals(contextKey, true) 557 | //condition: ContextKeyExpr.deserialize(`${contextKey}`) 558 | } 559 | } 560 | } 561 | })) 562 | 563 | this.updateAutoCompleteHeader() 564 | } 565 | 566 | getCurrentEnableAutoCompleteToggle() { 567 | return Object.entries(this.getLocalContextValues("csv")) 568 | .filter(([key, value]) => value) 569 | .map(([key, value]) => key.split(".").pop()) 570 | } 571 | 572 | updateAutoCompleteHeader() { 573 | const csvfiles = getLoadedCSV() 574 | const currentSelected = this.getCurrentEnableAutoCompleteToggle() 575 | let multipleSelectInstance: MultipleSelectInstance 576 | 577 | if (!this.elements.autocomplete) { 578 | // create 579 | const labelElement = document.createElement("label") 580 | const divElement = document.createElement("div") 581 | const selectElement = document.createElement("select") 582 | 583 | labelElement.textContent = "AutoComplete" 584 | divElement.appendChild(selectElement) 585 | divElement.style.display = "inline-block" 586 | divElement.style.marginLeft = "0.5rem" 587 | labelElement.appendChild(divElement) 588 | 589 | this.elements.header!.appendChild(labelElement) 590 | this.elements.autocompleteElement = labelElement 591 | 592 | selectElement.classList.add("multiple-select") 593 | 594 | const multipleSelectInit = (multipleSelectInstance: MultipleSelectInstance) => { 595 | const parent = multipleSelectInstance.getParentElement() 596 | const button = parent.querySelector('.ms-choice')! 597 | button.classList.add(style["ms-choice"]) 598 | } 599 | 600 | multipleSelectInstance = multipleSelect(selectElement, { 601 | filter: true, 602 | single: false, 603 | showSearchClear: true, 604 | data: csvfiles, 605 | width: "24rem", 606 | selectAll: false, 607 | onClick: (view) => { 608 | const contextKey = this.createContextKey("csv", view.value) 609 | const newValue = (view as any).selected 610 | 611 | this.changeAutoCompleteToggle(contextKey, newValue, true) 612 | this.syncAutoCompleteToggle() 613 | }, 614 | onAfterCreate: () => { 615 | if (!this.elements.autocomplete) { 616 | return 617 | } 618 | multipleSelectInit(this.elements.autocomplete) 619 | }, 620 | }) as MultipleSelectInstance 621 | 622 | multipleSelectInit(multipleSelectInstance) 623 | this.elements.autocomplete = multipleSelectInstance 624 | 625 | } else { 626 | // update 627 | multipleSelectInstance = this.elements.autocomplete 628 | 629 | multipleSelectInstance.refreshOptions({ 630 | data: csvfiles, 631 | }) 632 | } 633 | 634 | multipleSelectInstance.setSelects(currentSelected) 635 | } 636 | updateAutoCompleteHeaderToggle() { 637 | const multipleSelectInstance = this.elements.autocomplete 638 | if (!multipleSelectInstance) { 639 | return 640 | } 641 | multipleSelectInstance.setSelects(this.getCurrentEnableAutoCompleteToggle()) 642 | } 643 | 644 | setEventHandler() { 645 | this.monaco.onDidChangeConfiguration((e) => { 646 | if (e.hasChanged(monaco.editor.EditorOption.fontSize)) { 647 | this.changeFontSize(this.monaco.getOption(monaco.editor.EditorOption.fontSize), false) 648 | } 649 | if (e.hasChanged(monaco.editor.EditorOption.fontFamily)) { 650 | this.changeFontFamily(this.monaco.getOption(monaco.editor.EditorOption.fontFamily), false) 651 | } 652 | }) 653 | } 654 | 655 | setOverlayZIndex(zIndex: number) { 656 | if (!this.elements.overflowContent) { 657 | return 658 | } else { 659 | this.elements.overflowContent.style.zIndex = "" + zIndex 660 | } 661 | if (!this.elements.overflowOverlay) { 662 | return 663 | } else { 664 | this.elements.overflowOverlay.style.zIndex = "" + (zIndex + 1) 665 | } 666 | } 667 | 668 | setContext(key:string, value: any) { 669 | // @ts-ignore 670 | const contextKeyService = this.monaco._contextKeyService 671 | const contextValueContainer = contextKeyService.getContextValuesContainer(contextKeyService._myContextId) 672 | contextValueContainer.setValue(key, value) 673 | } 674 | 675 | getContext(key:string) { 676 | // @ts-ignore 677 | const contextKeyService = this.monaco._contextKeyService 678 | const contextValueContainer = contextKeyService.getContextValuesContainer(contextKeyService._myContextId) 679 | return contextValueContainer.getValue(key) 680 | } 681 | 682 | getContextValues() { 683 | // @ts-ignore 684 | const contextKeyService = this.monaco._contextKeyService 685 | const contextValueContainer = contextKeyService.getContextValuesContainer(contextKeyService._myContextId) 686 | return contextValueContainer.value 687 | } 688 | 689 | getLocalContextValues(...args: string[]) { 690 | const values = this.getContextValues() 691 | const start = this.createContextKey(...args) + "." 692 | return Object.fromEntries(Object.entries(values).filter(([key, value]) => key.startsWith(start))) 693 | } 694 | 695 | changeMode(newMode: PromptEditorMode) { 696 | if (this.mode === newMode) { 697 | return 698 | } 699 | 700 | // From VIM 701 | if (this.mode === PromptEditorMode.VIM) { 702 | this.vim.dispose() 703 | this.vim = null 704 | } 705 | // To VIM 706 | if (newMode === PromptEditorMode.VIM) { 707 | this.vim = initVimMode(this.monaco, this.elements.status!) 708 | } 709 | 710 | this.mode = newMode 711 | this.setContext(this.createContextKey("keybinding"), this.mode) 712 | 713 | if (this.elements.keyBindings) { 714 | this.elements.keyBindings.value = newMode 715 | } 716 | 717 | for (const callback of this.onChangeModeCallbacks) { 718 | callback() 719 | } 720 | } 721 | 722 | changeTheme(newThemeId: string) { 723 | this.theme = newThemeId 724 | 725 | if (this.elements.theme) { 726 | this.elements.theme.value = newThemeId 727 | } 728 | 729 | (this.monaco as any)._themeService.setTheme(this.theme) 730 | this.setContext(this.createContextKey("theme"), this.theme) 731 | 732 | for (const callback of this.onChangeThemeCallbacks) { 733 | callback() 734 | } 735 | } 736 | 737 | changeLanguage(languageId: string) { 738 | if (this.elements.language) { 739 | this.elements.language.value = languageId 740 | } 741 | 742 | const model = this.monaco.getModel() 743 | monaco.editor.setModelLanguage(model!, languageId) 744 | this.setContext(this.createContextKey("language"), languageId) 745 | 746 | for (const callback of this.onChangeLanguageCallbacks) { 747 | callback() 748 | } 749 | } 750 | 751 | changeShowHeader(show: boolean) { 752 | this.showHeader = show 753 | this.setContext(this.createContextKey("showHeader"), show) 754 | 755 | this.toggleHeader() 756 | 757 | for (const callback of this.onChangeShowHeaderCallbacks) { 758 | callback() 759 | } 760 | } 761 | 762 | changeShowLineNumbers(show: boolean) { 763 | if (this.elements.lineNumbers) { 764 | this.elements.lineNumbers.checked = show 765 | } 766 | this.monaco.updateOptions({ 767 | lineNumbers: show ? 'on' : 'off' 768 | }) 769 | this.setContext(this.createContextKey("lineNumbers"), show) 770 | 771 | for (const callback of this.onChangeShowLineNumbersCallbacks) { 772 | callback() 773 | } 774 | } 775 | 776 | changeShowMinimap(show: boolean, noCallback: boolean=false) { 777 | if (this.elements.minimap) { 778 | this.elements.minimap.checked = show 779 | } 780 | this.monaco.updateOptions({ 781 | minimap: { 782 | enabled: show 783 | } 784 | }) 785 | this.setContext(this.createContextKey("minimap"), show) 786 | 787 | for (const callback of this.onChangeShowMinimapCallbacks) { 788 | callback() 789 | } 790 | } 791 | 792 | changeReplaceUnderscore(isReplace: boolean) { 793 | if (this.elements.replaceUnderscore) { 794 | this.elements.replaceUnderscore.checked = isReplace 795 | } 796 | updateReplaceUnderscore(isReplace) 797 | this.setContext(this.createContextKey("replaceUnderscore"), isReplace) 798 | 799 | for (const callback of this.onChangeReplaceUnderscoreCallbacks) { 800 | callback() 801 | } 802 | } 803 | 804 | changeFontSize(size: number, updateEditorOption=true) { 805 | if (this.elements.fontsize) { 806 | this.elements.fontsize.value = ""+size 807 | } 808 | 809 | // avoid update loop 810 | if (updateEditorOption) { 811 | this.monaco.updateOptions({ 812 | "fontSize": size 813 | }) 814 | } 815 | this.setContext(this.createContextKey("fontSize"), size) 816 | 817 | for (const callback of this.onChangeFontSizeCallbacks) { 818 | callback() 819 | } 820 | } 821 | 822 | changeFontFamily(fontFamily: string, updateEditorOption=true) { 823 | if (updateEditorOption) { 824 | this.monaco.updateOptions({ 825 | fontFamily: fontFamily 826 | }) 827 | } 828 | this.setContext(this.createContextKey("fontFamily"), fontFamily) 829 | 830 | for (const callback of this.onChangeFontFamilyCallbacks) { 831 | callback() 832 | } 833 | } 834 | 835 | changeAutoCompleteToggle(filename: string, value: boolean, isContextKey = false) { 836 | const contextKey = isContextKey ? filename : this.createContextKey("csv", filename) 837 | 838 | this.setContext(contextKey, value) 839 | //this.updateAutoCompleteHeader() 840 | this.updateAutoCompleteHeaderToggle() 841 | 842 | for (const callback of this.onChangeAutoCompleteToggleCallbacks) { 843 | callback() 844 | } 845 | } 846 | 847 | polyfillMonacoEditorConfiguration() { 848 | if (typeof (this.monaco as any)["getConfiguration"] === 'function') { 849 | return 850 | } 851 | (this.monaco as any)["getConfiguration"] = () => { 852 | const configuration: any = {} 853 | 854 | for (const [name, option] of Object.entries(monaco.editor.EditorOptions)) { 855 | const value = this.monaco.getOption(option.id) 856 | configuration[name] = value 857 | if (name === 'cursorWidth') { 858 | configuration["viewInfo"] ||= {} 859 | configuration["viewInfo"][name] = value 860 | } 861 | } 862 | return configuration 863 | } 864 | } 865 | 866 | getThemeId() { 867 | return (this.monaco as any)._themeService._theme.id 868 | } 869 | 870 | getInstanceId() { 871 | return this._id 872 | } 873 | 874 | setValue(value: string) { 875 | if (value === void 0) { 876 | return 877 | } 878 | if (value === this.monaco.getValue()) { 879 | return 880 | } 881 | const pos = this.monaco.getPosition()! 882 | this.monaco.setValue(value) 883 | this.monaco.setPosition(pos) 884 | } 885 | 886 | hookTextAreaElement(textarea: HTMLTextAreaElement) { 887 | const promptEditor = this 888 | 889 | const defaultDescriptor = this.textareaDescriptor 890 | Object.defineProperty(textarea, 'value', { 891 | set: function(val) { 892 | promptEditor.setValue(val) 893 | return defaultDescriptor.set!.call(this, val) 894 | }, 895 | get: defaultDescriptor.get, 896 | }) 897 | } 898 | 899 | initHeader() { 900 | const headerElement = this.elements.header! 901 | 902 | // Monaco Options 903 | for (const {label, title, callback, isEnabledCallback, toggleCallback} of [ 904 | { 905 | label: "Minimap", 906 | callback: (label: HTMLLabelElement, checkbox: HTMLInputElement) => { 907 | this.elements.minimap = checkbox 908 | }, 909 | isEnabledCallback: () => this.monaco.getOption(monaco.editor.EditorOption.minimap).enabled, 910 | toggleCallback: (ev: Event) => { 911 | this.syncMinimap() 912 | } 913 | }, 914 | { 915 | label: "LineNum", 916 | callback: (label: HTMLLabelElement, checkbox: HTMLInputElement) => { 917 | this.elements.lineNumbers = checkbox 918 | }, 919 | isEnabledCallback: () => { 920 | return this.monaco.getOption(monaco.editor.EditorOption.lineNumbers).renderType !== monaco.editor.RenderLineNumbersType.Off 921 | }, 922 | toggleCallback: (ev: Event) => { 923 | this.syncLineNumbers() 924 | } 925 | }, 926 | { 927 | label: "Underscore", 928 | title: "Replace Underscore -> Space (AutoComplete)", 929 | callback: (label: HTMLLabelElement, checkbox: HTMLInputElement) => { 930 | this.elements.replaceUnderscore = checkbox 931 | }, 932 | isEnabledCallback: () => { 933 | return getReplaceUnderscore() 934 | }, 935 | toggleCallback: (ev: Event) => { 936 | this.syncReplaceUnderscore() 937 | } 938 | }, 939 | ] as PromptEditorCheckboxParam[]) { 940 | headerElement.appendChild(this.createCheckbox(label, callback, isEnabledCallback, toggleCallback, title)) 941 | } 942 | 943 | for (const {label, data, callback, isSelectedCallback, changeCallback, getValue} of [ 944 | { 945 | label: "FontSize", 946 | data: this._arrayToObject(FontSizePreset), 947 | callback: (label: HTMLLabelElement, select: HTMLSelectElement) => { 948 | this.elements.fontsize = select 949 | }, 950 | isSelectedCallback: (dataValue: string) => { 951 | return +dataValue === this.monaco.getOption(monaco.editor.EditorOption.fontSize) 952 | }, 953 | changeCallback: (ev: Event) => { 954 | const value = +(ev.target as HTMLSelectElement).value 955 | this.changeFontSize(value) 956 | this.syncFontSize() 957 | } 958 | }, 959 | { 960 | label: "Language", 961 | data: this._arrayToObject(monaco.languages.getLanguages().map(lang => lang.id)), 962 | callback: (label: HTMLLabelElement, select: HTMLSelectElement) => { 963 | this.elements.language = select 964 | }, 965 | isSelectedCallback: (dataValue: string) => { 966 | return dataValue === this.monaco.getModel()!.getLanguageId() 967 | }, 968 | changeCallback: (ev: Event) => { 969 | const value = (ev.target as HTMLSelectElement).value 970 | this.syncLanguage() 971 | //this.changeLanguage(value) 972 | } 973 | }, 974 | { 975 | label: "KeyBindings", 976 | data: PromptEditorMode, 977 | callback: (label: HTMLLabelElement, select: HTMLSelectElement) => { 978 | this.elements.keyBindings = select 979 | }, 980 | isSelectedCallback: (dataValue: PromptEditorMode) => { 981 | return dataValue === this.mode 982 | }, 983 | changeCallback: (ev: Event) => { 984 | const value = (ev.target as HTMLSelectElement).value as PromptEditorMode 985 | this.syncKeyBindings() 986 | } 987 | }, 988 | { 989 | label: "Theme", 990 | data: this._mapToObject((this.monaco as any)._themeService._knownThemes), 991 | callback: (label: HTMLLabelElement, select: HTMLSelectElement) => { 992 | this.elements.theme = select 993 | }, 994 | isSelectedCallback: (dataValue: monaco.editor.ThemeColor) => { 995 | return dataValue.id === this.theme 996 | }, 997 | changeCallback: (ev: Event) => { 998 | const value = (ev.target as HTMLSelectElement).value as PromptEditorMode 999 | if (this.getThemeId() !== value) { 1000 | this.syncTheme() 1001 | } 1002 | }, 1003 | getValue: (value: any) => { 1004 | return value.id 1005 | } 1006 | }, 1007 | ]) { 1008 | headerElement.appendChild(this.createSelect( 1009 | label, data, callback, isSelectedCallback, changeCallback, getValue 1010 | )) 1011 | } 1012 | 1013 | headerElement.addEventListener("contextmenu", (ev: MouseEvent) => { 1014 | ev.stopPropagation() 1015 | ev.preventDefault() 1016 | }) 1017 | 1018 | headerElement.querySelectorAll('header > *').forEach((item) => { 1019 | (item as HTMLElement).style.marginRight = "1rem" 1020 | }) 1021 | } 1022 | 1023 | syncLanguage() { 1024 | if (!this.elements.language) { 1025 | return 1026 | } 1027 | const value = this.elements.language.value 1028 | for (const callback of this.onChangeLanguageBeforeSyncCallbacks) { 1029 | callback() 1030 | } 1031 | runAllInstances((instance) => { 1032 | instance.changeLanguage(value) 1033 | }) 1034 | } 1035 | 1036 | syncKeyBindings() { 1037 | if (!this.elements.keyBindings) { 1038 | return 1039 | } 1040 | const value = this.elements.keyBindings.value as PromptEditorMode 1041 | for (const callback of this.onChangeModeBeforeSyncCallbacks) { 1042 | callback() 1043 | } 1044 | runAllInstances((instance) => { 1045 | instance.changeMode(value) 1046 | }) 1047 | this.monaco.focus() 1048 | } 1049 | 1050 | syncTheme() { 1051 | if (!this.elements.theme) { 1052 | return 1053 | } 1054 | const value = this.elements.theme.value 1055 | for (const callback of this.onChangeThemeBeforeSyncCallbacks) { 1056 | callback() 1057 | } 1058 | runAllInstances((instance) => { 1059 | instance.changeTheme(value) 1060 | }) 1061 | } 1062 | 1063 | syncShowHeader() { 1064 | for (const callback of this.onChangeShowHeaderBeforeSyncCallbacks) { 1065 | callback() 1066 | } 1067 | runAllInstances((instance) => { 1068 | instance.changeShowHeader(this.showHeader) 1069 | }) 1070 | } 1071 | 1072 | syncLineNumbers() { 1073 | if (!this.elements.lineNumbers) { 1074 | return 1075 | } 1076 | const value = this.elements.lineNumbers.checked 1077 | for (const callback of this.onChangeShowLineNumbersBeforeSyncCallbacks) { 1078 | callback() 1079 | } 1080 | runAllInstances((instance) => { 1081 | instance.changeShowLineNumbers(value) 1082 | }) 1083 | } 1084 | 1085 | syncMinimap() { 1086 | if (!this.elements.minimap) { 1087 | return 1088 | } 1089 | const value = this.elements.minimap.checked 1090 | for (const callback of this.onChangeShowMinimapBeforeSyncCallbacks) { 1091 | callback() 1092 | } 1093 | runAllInstances((instance) => { 1094 | instance.changeShowMinimap(value) 1095 | }) 1096 | } 1097 | 1098 | syncReplaceUnderscore() { 1099 | if (!this.elements.replaceUnderscore) { 1100 | return 1101 | } 1102 | const value = this.elements.replaceUnderscore.checked 1103 | for (const callback of this.onChangeReplaceUnderscoreBeforeSyncCallbacks) { 1104 | callback() 1105 | } 1106 | runAllInstances((instance) => { 1107 | instance.changeReplaceUnderscore(value) 1108 | }) 1109 | } 1110 | 1111 | syncFontSize() { 1112 | const value = this.getContext(this.createContextKey("fontSize")) 1113 | for (const callback of this.onChangeFontSizeBeforeSyncCallbacks) { 1114 | callback() 1115 | } 1116 | runAllInstances((instance) => { 1117 | instance.changeFontSize(value) 1118 | }) 1119 | } 1120 | 1121 | syncFontFamily() { 1122 | const value = this.getContext(this.createContextKey("fontFamily")) 1123 | for (const callback of this.onChangeFontFamilyBeforeSyncCallbacks) { 1124 | callback() 1125 | } 1126 | runAllInstances((instance) => { 1127 | instance.changeFontFamily(value) 1128 | }) 1129 | } 1130 | 1131 | updateAutoCompleteToggle() { 1132 | const values = this.getLocalContextValues("csv") 1133 | const enables = Object.entries(values).filter(([contextKey, value]) => { 1134 | return value 1135 | }).map(([contextKey, value]) => { 1136 | return contextKey.split('.').slice(-1)[0] 1137 | }) 1138 | 1139 | addLoadedCSV(enables) 1140 | } 1141 | 1142 | syncAutoCompleteToggle() { 1143 | const values = this.getLocalContextValues("csv") 1144 | 1145 | this.updateAutoCompleteToggle() 1146 | 1147 | for (const callback of this.onChangeAutoCompleteToggleBeforeSyncCallbacks) { 1148 | callback() 1149 | } 1150 | 1151 | runAllInstances((instance) => { 1152 | for (const [contextKey, value] of Object.entries(values)) { 1153 | instance.changeAutoCompleteToggle(contextKey, value, true) 1154 | } 1155 | }) 1156 | } 1157 | 1158 | createCheckbox( 1159 | labelText: string, 1160 | callback: (label: HTMLLabelElement, input: HTMLInputElement) => void, 1161 | isEnabledCallback: () => boolean, 1162 | toggleCallback: (ev: Event) => void, 1163 | title?: string, 1164 | ) { 1165 | const label = document.createElement('label') 1166 | const input = document.createElement('input') 1167 | 1168 | Object.assign(label.style, { 1169 | display: "flex", 1170 | }) 1171 | 1172 | input.checked = isEnabledCallback() 1173 | input.type = 'checkbox' 1174 | input.addEventListener('change', toggleCallback) 1175 | 1176 | label.textContent = labelText 1177 | label.prepend(input) 1178 | if (title) { 1179 | label.title = title 1180 | } 1181 | 1182 | callback(label, input) 1183 | 1184 | return label 1185 | } 1186 | 1187 | createSelect( 1188 | labelText: string, 1189 | data: object, 1190 | callback: (label: HTMLLabelElement, select: HTMLSelectElement) => void, 1191 | isSelectedCallback: (dataValue: any) => boolean, 1192 | changeCallback: (ev: Event) => void, 1193 | getValue?: (value: any) => string, 1194 | multiple: boolean = false, 1195 | ) { 1196 | const labelElement = document.createElement('label') 1197 | Object.assign(labelElement.style, { 1198 | display: "flex", 1199 | }) 1200 | const selectElement = document.createElement('select') 1201 | if (multiple) { 1202 | selectElement.multiple = true 1203 | selectElement.size = 1 1204 | } 1205 | Object.assign(selectElement.style, { 1206 | marginLeft: "0.5rem", 1207 | }) 1208 | for (const [key, value] of Object.entries(data)){ 1209 | const option = document.createElement('option') 1210 | option.textContent = key 1211 | option.value = typeof getValue === 'function' ? getValue(value) : value 1212 | 1213 | if (isSelectedCallback(value)) { 1214 | option.selected = true 1215 | } 1216 | selectElement.appendChild(option) 1217 | } 1218 | selectElement.addEventListener('change', changeCallback) 1219 | labelElement.textContent = labelText 1220 | labelElement.appendChild(selectElement) 1221 | 1222 | callback(labelElement, selectElement) 1223 | return labelElement 1224 | } 1225 | 1226 | _mapToObject(map: Map) { 1227 | const obj: {[key: string]: any} = {} 1228 | map.forEach((value, key) => { 1229 | obj[key] = value 1230 | }) 1231 | return obj 1232 | } 1233 | 1234 | _arrayToObject(array: T[]) { 1235 | const obj: {[key in T]: T} = {} as any 1236 | array.forEach((value) => { 1237 | obj[value] = value 1238 | }) 1239 | return obj 1240 | } 1241 | 1242 | handleResize() { 1243 | const callback = (mutations: (MutationRecord|IntersectionObserverEntry)[], observer: (MutationObserver|IntersectionObserver)) => { 1244 | const main = this.elements.main 1245 | if (!main) { 1246 | return 1247 | } 1248 | this.toggleHeader() 1249 | //main.style.maxHeight = this.clientHeight + "px" 1250 | if (this.parentElement) { 1251 | main.style.height = this.parentElement.clientHeight + "px" 1252 | } 1253 | this.monaco.layout() 1254 | } 1255 | const mutation = new MutationObserver(callback) 1256 | const intersection = new IntersectionObserver(callback, { 1257 | root: document.documentElement 1258 | }) 1259 | mutation.observe(this, {attributes: true, attributeFilter: ["style"]}) 1260 | intersection.observe(this) 1261 | } 1262 | 1263 | toggleHeader() { 1264 | const child = this.elements.header 1265 | const parent = this.elements.inner 1266 | 1267 | if (!child || !parent) { 1268 | return 1269 | } 1270 | 1271 | if (!this.showHeader) { 1272 | child.style.display = "none" 1273 | return 1274 | } 1275 | 1276 | child.style.display = "block" 1277 | 1278 | const childRect = child.getBoundingClientRect() 1279 | const parentRect = parent.getBoundingClientRect() 1280 | 1281 | if ( 1282 | childRect.width <= parentRect.width && 1283 | childRect.height <= parentRect.height && 1284 | childRect.top >= parentRect.top && 1285 | childRect.left >= parentRect.left && 1286 | childRect.bottom <= parentRect.bottom && 1287 | childRect.right <= parentRect.right 1288 | ) { 1289 | child.style.removeProperty("display") 1290 | } else { 1291 | child.style.display = "none" 1292 | } 1293 | } 1294 | 1295 | copyStyleToShadow() { 1296 | document.head.querySelectorAll('style').forEach((style) => { 1297 | this.elements.container!.appendChild(style.cloneNode(true)) 1298 | }) 1299 | } 1300 | 1301 | getSettings() { 1302 | return { 1303 | minimap: this.elements.minimap?.checked, 1304 | showHeader: this.showHeader, 1305 | lineNumbers: this.elements.lineNumbers?.checked, 1306 | replaceUnderscore: getReplaceUnderscore(), 1307 | language: this.monaco.getModel()!.getLanguageId(), 1308 | theme: this.theme, 1309 | mode: this.mode, 1310 | fontSize: this.getContext(this.createContextKey("fontSize")), 1311 | fontFamily: this.getContext(this.createContextKey("fontFamily")), 1312 | csvToggle: this.getLocalContextValues("csv"), 1313 | } as PromptEditorSettings 1314 | } 1315 | 1316 | setSettings(settings: Partial, force=false) { 1317 | const currentSettings = this.getSettings() 1318 | 1319 | if ( 1320 | settings.minimap !== void 0 && ( 1321 | force || 1322 | settings.minimap !== currentSettings.minimap 1323 | ) 1324 | ) { 1325 | this.changeShowMinimap(settings.minimap) 1326 | } 1327 | if ( 1328 | settings.showHeader !== void 0 && ( 1329 | force || 1330 | settings.showHeader !== currentSettings.showHeader 1331 | ) 1332 | ) { 1333 | this.changeShowHeader(settings.showHeader) 1334 | } 1335 | if ( 1336 | settings.lineNumbers !== void 0 && ( 1337 | force || 1338 | settings.lineNumbers !== currentSettings.lineNumbers 1339 | ) 1340 | ) { 1341 | this.changeShowLineNumbers(settings.lineNumbers) 1342 | } 1343 | if ( 1344 | settings.replaceUnderscore !== void 0 && ( 1345 | force || 1346 | settings.replaceUnderscore !== currentSettings.replaceUnderscore 1347 | ) 1348 | ) { 1349 | this.changeReplaceUnderscore(settings.replaceUnderscore) 1350 | } 1351 | if ( 1352 | settings.language !== void 0 && ( 1353 | force || 1354 | settings.language !== currentSettings.language 1355 | ) 1356 | ) { 1357 | this.changeLanguage(settings.language) 1358 | } 1359 | if ( 1360 | settings.theme !== void 0 && ( 1361 | force || 1362 | settings.theme !== currentSettings.theme 1363 | ) 1364 | ) { 1365 | this.changeTheme(settings.theme) 1366 | } 1367 | if ( 1368 | settings.mode !== void 0 && ( 1369 | force || 1370 | settings.mode !== currentSettings.mode 1371 | ) 1372 | ) { 1373 | this.changeMode(settings.mode) 1374 | } 1375 | 1376 | if ( 1377 | settings.fontSize !== void 0 && ( 1378 | force || 1379 | settings.fontSize !== currentSettings.fontSize 1380 | ) 1381 | ) { 1382 | this.changeFontSize(settings.fontSize) 1383 | } 1384 | 1385 | if ( 1386 | settings.fontFamily !== void 0 && ( 1387 | force || 1388 | settings.fontFamily !== currentSettings.fontFamily 1389 | ) 1390 | ) { 1391 | this.changeFontFamily(settings.fontFamily) 1392 | } 1393 | 1394 | if ( 1395 | settings.csvToggle !== void 0 && ( 1396 | force || 1397 | !deepEqual(settings.csvToggle, currentSettings.csvToggle) 1398 | ) 1399 | ) { 1400 | for (const [contextKey, enabled] of Object.entries(settings.csvToggle)) { 1401 | if (currentSettings.csvToggle[contextKey] !== enabled) { 1402 | this.changeAutoCompleteToggle(contextKey, enabled, true) 1403 | } 1404 | } 1405 | this.updateAutoCompleteToggle() 1406 | } 1407 | } 1408 | 1409 | onChangeShowHeader(callback: () => void) { 1410 | this.onChangeShowHeaderCallbacks.push(callback) 1411 | } 1412 | 1413 | onChangeShowHeaderBeforeSync(callback: () => void) { 1414 | this.onChangeShowHeaderBeforeSyncCallbacks.push(callback) 1415 | } 1416 | 1417 | onChangeShowLineNumbers(callback: () => void) { 1418 | this.onChangeShowLineNumbersCallbacks.push(callback) 1419 | } 1420 | 1421 | onChangeShowLineNumbersBeforeSync(callback: () => void) { 1422 | this.onChangeShowLineNumbersBeforeSyncCallbacks.push(callback) 1423 | } 1424 | 1425 | onChangeShowMinimap(callback: () => void) { 1426 | this.onChangeShowMinimapCallbacks.push(callback) 1427 | } 1428 | 1429 | onChangeShowMinimapBeforeSync(callback: () => void) { 1430 | this.onChangeShowMinimapBeforeSyncCallbacks.push(callback) 1431 | } 1432 | 1433 | onChangeReplaceUnderscore(callback: () => void) { 1434 | this.onChangeReplaceUnderscoreCallbacks.push(callback) 1435 | } 1436 | 1437 | onChangeReplaceUnderscoreBeforeSync(callback: () => void) { 1438 | this.onChangeReplaceUnderscoreBeforeSyncCallbacks.push(callback) 1439 | } 1440 | 1441 | onChangeTheme(callback: () => void) { 1442 | this.onChangeThemeCallbacks.push(callback) 1443 | } 1444 | 1445 | onChangeThemeBeforeSync(callback: () => void) { 1446 | this.onChangeThemeBeforeSyncCallbacks.push(callback) 1447 | } 1448 | 1449 | onChangeMode(callback: () => void) { 1450 | this.onChangeModeCallbacks.push(callback) 1451 | } 1452 | 1453 | onChangeModeBeforeSync(callback: () => void) { 1454 | this.onChangeModeBeforeSyncCallbacks.push(callback) 1455 | } 1456 | onChangeLanguage(callback: () => void) { 1457 | this.onChangeLanguageCallbacks.push(callback) 1458 | } 1459 | 1460 | onChangeLanguageBeforeSync(callback: () => void) { 1461 | this.onChangeLanguageBeforeSyncCallbacks.push(callback) 1462 | } 1463 | 1464 | onChangeFontSize(callback: () => void) { 1465 | this.onChangeFontSizeCallbacks.push(callback) 1466 | } 1467 | 1468 | onChangeFontSizeBeforeSync(callback: () => void) { 1469 | this.onChangeFontSizeBeforeSyncCallbacks.push(callback) 1470 | } 1471 | 1472 | onChangeFontFamily(callback: () => void) { 1473 | this.onChangeFontFamilyCallbacks.push(callback) 1474 | } 1475 | 1476 | onChangeFontFamilyBeforeSync(callback: () => void) { 1477 | this.onChangeFontFamilyBeforeSyncCallbacks.push(callback) 1478 | } 1479 | 1480 | onChangeAutoCompleteToggle(callback: () => void) { 1481 | this.onChangeAutoCompleteToggleCallbacks.push(callback) 1482 | } 1483 | 1484 | onChangeAutoCompleteToggleBeforeSync(callback: () => void) { 1485 | this.onChangeAutoCompleteToggleBeforeSyncCallbacks.push(callback) 1486 | } 1487 | 1488 | onChange(callback: () => void) { 1489 | this.onChangeShowHeader(callback) 1490 | this.onChangeShowLineNumbers(callback) 1491 | this.onChangeShowMinimap(callback) 1492 | this.onChangeReplaceUnderscore(callback) 1493 | this.onChangeTheme(callback) 1494 | this.onChangeMode(callback) 1495 | this.onChangeLanguage(callback) 1496 | this.onChangeFontSize(callback) 1497 | this.onChangeFontFamily(callback) 1498 | this.onChangeAutoCompleteToggle(callback) 1499 | } 1500 | 1501 | onChangeBeforeSync(callback: () => void) { 1502 | this.onChangeShowHeaderBeforeSync(callback) 1503 | this.onChangeShowLineNumbersBeforeSync(callback) 1504 | this.onChangeShowMinimapBeforeSync(callback) 1505 | this.onChangeReplaceUnderscoreBeforeSync(callback) 1506 | this.onChangeThemeBeforeSync(callback) 1507 | this.onChangeModeBeforeSync(callback) 1508 | this.onChangeLanguageBeforeSync(callback) 1509 | this.onChangeFontSizeBeforeSync(callback) 1510 | this.onChangeFontFamilyBeforeSync(callback) 1511 | this.onChangeAutoCompleteToggleBeforeSync(callback) 1512 | } 1513 | 1514 | getLinesTable(start: number, active: number, end: number) { 1515 | const lines = this.elements.main!.querySelector('.view-lines')! as HTMLDivElement 1516 | const model = this.monaco.getModel() 1517 | if (!model) { 1518 | throw new Error("Model not found in Monaco Editor") 1519 | } 1520 | const lineCount = Math.min(end, model.getLineCount()) 1521 | const container = document.createElement("div") 1522 | const styleContainer = document.createElement("style") 1523 | const table = document.createElement("table") 1524 | 1525 | container.appendChild(styleContainer) 1526 | styleContainer.textContent = `@scope { ${ this.monaco._themeService._themeCSS } }` 1527 | 1528 | table.classList.add(style["find-lines-table"], "monaco-editor") 1529 | 1530 | const options = new ViewLineOptions({ options: this.monaco.getOptions() }, this.getThemeId()) 1531 | for (let currentLineNum = Math.max(start, 1); currentLineNum <= lineCount; ++currentLineNum) { 1532 | const trEl = document.createElement("tr") 1533 | const lineNumberContainer = document.createElement("td") 1534 | const lineContentContainer = document.createElement("td") 1535 | 1536 | lineNumberContainer.textContent = currentLineNum as unknown as string 1537 | lineNumberContainer.classList.add(style["find-line-number"]) 1538 | lineContentContainer.classList.add(style["find-line-content"]) 1539 | 1540 | // monaco.editor.colorize* は Decoration の処理をしないため ViewLine を元に自力でHTMLを生成する必要がある 1541 | const lineDecorations = model.getLineDecorations(currentLineNum) 1542 | 1543 | const view = this.monaco._modelData.view 1544 | const partialViewportData = Object.assign(view._context.viewLayout.getLinesViewportData(), { 1545 | startLineNumber: currentLineNum, 1546 | endLineNumber: currentLineNum+1, 1547 | }) 1548 | const viewportData = new ViewportData(view._selections, partialViewportData, view._context.viewLayout.getWhitespaceViewportData(), view._context.viewModel) 1549 | 1550 | const lineData = viewportData.getViewLineRenderingData(currentLineNum) 1551 | const inlineDecorations = lineDecorations.map(lineDecoration => new InlineDecoration( 1552 | lineDecoration.range, 1553 | lineDecoration.options.inlineClassName, 1554 | lineDecoration.options.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular 1555 | )) 1556 | const lineContent = model.getLineContent(currentLineNum) 1557 | const actualInlineDecorations = LineDecoration.filter(inlineDecorations, currentLineNum, 1, lineContent.length + 1); 1558 | const renderLineInput = new RenderLineInput( 1559 | options.useMonospaceOptimizations, 1560 | options.canUseHalfwidthRightwardsArrow, 1561 | 1562 | lineContent, 1563 | 1564 | lineData.continuesWithWrappedLine, 1565 | lineData.isBasicASCII, 1566 | lineData.containsRTL, 1567 | 0, 1568 | // ITextModel.tokenization はドキュメントに記載されていない 1569 | // see: https://github.com/microsoft/vscode/blob/12c1d4fb1753aeda4b55de73b8a8ee58c607d780/src/vs/editor/common/model/textModel.ts#L286 1570 | (model as any).tokenization.getLineTokens(currentLineNum), 1571 | actualInlineDecorations, 1572 | lineData.tabSize, 1573 | lineData.startVisibleColumn, 1574 | options.spaceWidth, 1575 | options.middotWidth, 1576 | options.wsmiddotWidth, 1577 | options.stopRenderingLineAfter, 1578 | options.renderWhitespace, 1579 | options.renderControlCharacters, 1580 | options.fontLigatures !== EditorFontLigatures.OFF, 1581 | null 1582 | ) 1583 | 1584 | const sb = new StringBuilder(10000) 1585 | const output = renderViewLine(renderLineInput, sb) 1586 | 1587 | if (lineContent.length === 0) { 1588 | // 行の内容が空だとheightが小さくなってしまうので空白文字を入れる 1589 | lineContentContainer.innerHTML = " " 1590 | } else { 1591 | lineContentContainer.innerHTML = sb.build() 1592 | } 1593 | trEl.appendChild(lineNumberContainer) 1594 | trEl.appendChild(lineContentContainer) 1595 | table.appendChild(trEl) 1596 | } 1597 | 1598 | container.appendChild(table) 1599 | return container 1600 | } 1601 | addCustomSuggest(id: string) { 1602 | const context = customSuggestContext[id] 1603 | if (!context) { 1604 | throw new Error(`Custom Suggest Context not found: ${id}`) 1605 | } 1606 | 1607 | const createSuggest = context.createSuggest 1608 | if (!createSuggest) { 1609 | throw new Error(`create suggest function not found: ${id}`) 1610 | } 1611 | 1612 | const keybinding = context.keybinding 1613 | 1614 | const command = this.monaco.addCommand( 1615 | keybinding, 1616 | () => { 1617 | // A1111 で最後のインスタンスで command が実行されてしまうため, 1618 | // thisを使用せず最後にフォーカスしたインスタンスで処理を行う 1619 | const instance = this.getCurrentFocus() 1620 | if (instance === null) { 1621 | return 1622 | } 1623 | if (instance && instance.mode === PromptEditorMode.VIM && instance.vim && instance.vim.state.keyMap !== "vim-insert") { 1624 | return 1625 | } 1626 | const languageId = instance.getContext(instance.createContextKey("language")) 1627 | const completionItemProvider = createDynamicSuggest(createSuggest, () => { 1628 | if (provider) { 1629 | // snippet に choice が含まれていると即時 dispose で候補がサジェストされなくなる 1630 | setTimeout(() => { 1631 | provider.dispose() 1632 | }, 0) 1633 | } 1634 | }) 1635 | const provider = monaco.languages.registerCompletionItemProvider(languageId, completionItemProvider) 1636 | const suggestController = instance.monaco.getContribution(SuggestController.ID) as SuggestController 1637 | suggestController.triggerSuggest(new Set([completionItemProvider])) 1638 | } 1639 | ) 1640 | } 1641 | } 1642 | window.customElements.define('prompt-editor', PromptEditor); 1643 | 1644 | const runAllInstances = (callback: (instance: T) => boolean|void) => { 1645 | for (const instanceId of (Object.keys(settings.instances) as unknown as number[]).sort()) { 1646 | if (callback(settings.instances[instanceId] as T)) { 1647 | break 1648 | } 1649 | } 1650 | } 1651 | 1652 | const customSuggestContext: {[key: string]: { 1653 | keybinding: number, 1654 | createSuggest: () => Promise[]>, 1655 | }} = {} 1656 | const addCustomSuggest = (id: string, keybinding: number, createSuggests: () => Promise[]>) => { 1657 | customSuggestContext[id] = { 1658 | keybinding: keybinding, 1659 | createSuggest: createSuggests, 1660 | } 1661 | 1662 | runAllInstances((instance) => { 1663 | instance.addCustomSuggest(id) 1664 | }) 1665 | } 1666 | 1667 | const updateAutoComplete = () => { 1668 | const files = getLoadedCSV() 1669 | runAllInstances((instance) => { 1670 | instance.updateAutoComplete() 1671 | return 1672 | }) 1673 | } 1674 | 1675 | const _loadCSV = (filename: string, csv: string) => { 1676 | const retval = loadCSV.call(this, filename, csv) 1677 | updateAutoComplete() 1678 | return retval 1679 | } 1680 | 1681 | const _addCSV = (filename: string, csv: string) => { 1682 | const retval = addCSV.call(this, filename, csv) 1683 | updateAutoComplete() 1684 | return retval 1685 | } 1686 | 1687 | const _clearCSV = () => { 1688 | const retval = clearCSV.call(this) 1689 | updateAutoComplete() 1690 | return retval 1691 | } 1692 | 1693 | const KeyMod = monaco.KeyMod 1694 | const KeyCode = monaco.KeyCode 1695 | type CompletionItem = monaco.languages.CompletionItem 1696 | const CompletionItemKind = monaco.languages.CompletionItemKind 1697 | const CompletionItemInsertTextRule = monaco.languages.CompletionItemInsertTextRule 1698 | 1699 | export { 1700 | PromptEditor, 1701 | getCount, 1702 | _loadCSV as loadCSV, 1703 | _addCSV as addCSV, 1704 | _clearCSV as clearCSV, 1705 | getLoadedCSV, 1706 | addLoadedCSV, 1707 | addData, 1708 | addCustomSuggest, 1709 | addLanguages, 1710 | runAllInstances, 1711 | PromptEditorSettings, 1712 | ContextKeyExpr, 1713 | KeyMod, 1714 | KeyCode, 1715 | CompletionItem, 1716 | CompletionItemKind, 1717 | CompletionItemInsertTextRule, 1718 | } -------------------------------------------------------------------------------- /src/languages/index.ts: -------------------------------------------------------------------------------- 1 | export * as sdPrompt from './sd-prompt' 2 | export * as sdDynamicPrompt from './sd-dynamic-prompt' 3 | -------------------------------------------------------------------------------- /src/languages/sd-dynamic-prompt.ts: -------------------------------------------------------------------------------- 1 | import {languages} from 'monaco-editor/esm/vs/editor/editor.api' 2 | import {conf as baseConf, language as baseLanguage} from './sd-prompt' 3 | 4 | const conf: languages.LanguageConfiguration = Object.assign({}, baseConf, { 5 | comments: Object.assign({}, baseConf.comments, { 6 | lineComment: '#', 7 | }), 8 | brackets: baseConf.brackets!.concat([ 9 | ['{', '}'], 10 | ]), 11 | autoClosingPairs: baseConf.autoClosingPairs!.concat([ 12 | { open: '{', close: '}' }, 13 | ]), 14 | surroundingPairs: baseConf.surroundingPairs!.concat([ 15 | { open: '{', close: '}' }, 16 | ]), 17 | }) 18 | 19 | const language: languages.IMonarchLanguage = Object.assign({}, baseLanguage, { 20 | brackets: baseLanguage.brackets!.concat([ 21 | { open: '{', close: '}', token: 'delimiter.curly' }, 22 | ]), 23 | 24 | tokenizer: Object.assign({}, baseLanguage.tokenizer, { 25 | root: [ 26 | [/[a-zA-Z]\w*/, { 27 | cases: { 28 | '@keywords': 'keyword', 29 | '@default': 'identifier', 30 | } 31 | }], 32 | 33 | { include: '@whitespace' }, 34 | { include: '@numbers' }, 35 | 36 | [/[,:|]/, 'delimiter'], 37 | [/[{}\[\]()<>]/, '@brackets'], 38 | ], 39 | whitespace: baseLanguage.tokenizer.whitespace.concat([ 40 | [/(^#.*$)/, 'comment'], 41 | ]), 42 | }), 43 | }) 44 | 45 | export { 46 | conf, 47 | language, 48 | } 49 | -------------------------------------------------------------------------------- /src/languages/sd-prompt.ts: -------------------------------------------------------------------------------- 1 | import {languages} from 'monaco-editor/esm/vs/editor/editor.api' 2 | 3 | const conf: languages.LanguageConfiguration = { 4 | comments: { 5 | }, 6 | brackets: [ 7 | ['[', ']'], 8 | ['(', ')'], 9 | ], 10 | autoClosingPairs: [ 11 | { open: '[', close: ']' }, 12 | { open: '(', close: ')' }, 13 | { open: '<', close: '>' }, 14 | ], 15 | surroundingPairs: [ 16 | { open: '[', close: ']' }, 17 | { open: '(', close: ')' }, 18 | ], 19 | wordPattern: new RegExp('[^\\s,{}\\[\\]()<>:#]+') 20 | } 21 | 22 | const language: languages.IMonarchLanguage = { 23 | defaultToken: '', 24 | tokenPostfix: '.prompt', 25 | 26 | keywords: [ 27 | "lora", 28 | "hypernet", 29 | "AND", 30 | "BREAK", 31 | ], 32 | 33 | brackets: [ 34 | { open: '[', close: ']', token: 'delimiter.bracket' }, 35 | { open: '(', close: ')', token: 'delimiter.parenthesis' }, 36 | { open: '<', close: '>', token: 'delimiter.angle' }, 37 | ], 38 | 39 | escapes: /\\./, 40 | 41 | tokenizer: { 42 | root: [ 43 | [/[a-zA-Z]\w*/, { 44 | cases: { 45 | '@keywords': 'keyword', 46 | '@default': 'identifier', 47 | } 48 | }], 49 | 50 | { include: '@whitespace' }, 51 | { include: '@numbers' }, 52 | 53 | [/[,:]/, 'delimiter'], 54 | [/[\[\]()<>]/, '@brackets'], 55 | ], 56 | 57 | whitespace: [ 58 | [/\s+/, 'white'], 59 | ], 60 | 61 | numbers: [ 62 | [/-?(\d*\.)?\d+/, 'number'] 63 | ], 64 | } 65 | } 66 | 67 | export { 68 | conf, 69 | language, 70 | } 71 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as MonacoPrompt from './index' 2 | import { escapeHTML } from "./utils" 3 | import { deepEqual } from 'fast-equals' 4 | import { EndPoint, GetEmbeddings, GetSnippets, CSV } from '../extension.json' 5 | declare const gradio_config: any; 6 | 7 | const me = "webui-monaco-prompt"; 8 | 9 | ((srcURL) => { 10 | let isLoaded = false 11 | 12 | const models = [ 13 | { 14 | keybinding: MonacoPrompt.KeyMod.chord( 15 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyM, 16 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyM 17 | ), 18 | model: "sd-models" 19 | }, 20 | { 21 | keybinding: MonacoPrompt.KeyMod.chord( 22 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyM, 23 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyL 24 | ), 25 | model: "loras" 26 | }, 27 | { 28 | keybinding: MonacoPrompt.KeyMod.chord( 29 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyM, 30 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyE 31 | ), 32 | model: "embeddings" 33 | }, 34 | { 35 | keybinding: MonacoPrompt.KeyMod.chord( 36 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyM, 37 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyH 38 | ), 39 | model: "hypernetworks" 40 | }, 41 | { 42 | keybinding: MonacoPrompt.KeyMod.chord( 43 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyM, 44 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyA 45 | ), 46 | model: "sd-vae" 47 | }, 48 | ] 49 | for (const {keybinding, model} of models) { 50 | MonacoPrompt.addCustomSuggest(model, keybinding, async () => { 51 | const items: Partial[] = [] 52 | const models = await fetch(`/sdapi/v1/${model}`).then(res => res.json()) 53 | switch (model) { 54 | case "sd-models": 55 | case "sd-vae": 56 | (models as {model_name: string, filename: string}[]).forEach(model => { 57 | const label = model.model_name 58 | items.push({ 59 | label: label, 60 | kind: MonacoPrompt.CompletionItemKind.File, 61 | insertText: label, 62 | }) 63 | }) 64 | break 65 | case "hypernetworks": 66 | (models as {name: string, path: string}[]).forEach(model => { 67 | const label = model.name 68 | items.push({ 69 | label: label, 70 | kind: MonacoPrompt.CompletionItemKind.File, 71 | insertText: label, 72 | }) 73 | }) 74 | break 75 | case "embeddings": 76 | Object.keys((models as { 77 | loaded: {[key: string]: {sd_checkpoint: string, sd_checkpoint_name: string}}, 78 | skipped: {[key: string]: {sd_checkpoint: string, sd_checkpoint_name: string}}, 79 | }).loaded).forEach(key => { 80 | const label = key 81 | items.push({ 82 | label: label, 83 | kind: MonacoPrompt.CompletionItemKind.File, 84 | insertText: label, 85 | }) 86 | }) 87 | break 88 | case "loras": 89 | (models as {name: string, path: string, prompt: string }[]).forEach(model => { 90 | const label = model.name 91 | items.push({ 92 | label: label, 93 | kind: MonacoPrompt.CompletionItemKind.File, 94 | insertText: label, 95 | }) 96 | }) 97 | break 98 | default: 99 | throw new Error("Unknown model type: " + model) 100 | } 101 | return items 102 | }) 103 | } 104 | 105 | // snippet 106 | MonacoPrompt.addCustomSuggest( 107 | "snippet", 108 | MonacoPrompt.KeyMod.chord( 109 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyM, 110 | MonacoPrompt.KeyMod.CtrlCmd | MonacoPrompt.KeyCode.KeyS, 111 | ), 112 | async () => { 113 | const items: Partial[] = [] 114 | const snippets = await fetch(GetSnippets).then((res: Response) => res.json()) 115 | 116 | for (const snippet of snippets) { 117 | const usage = `**${escapeHTML(snippet.insertText)}**` 118 | items.push({ 119 | label: snippet.label, 120 | kind: MonacoPrompt.CompletionItemKind.Snippet, 121 | insertText: snippet.insertText, 122 | insertTextRules: MonacoPrompt.CompletionItemInsertTextRule.InsertAsSnippet, 123 | detail: snippet.path, 124 | documentation: { 125 | supportHtml: true, 126 | value: snippet.documentation ? 127 | [ 128 | usage, 129 | snippet.documentation 130 | ].join("

") : 131 | usage 132 | }, 133 | }) 134 | } 135 | 136 | return items 137 | } 138 | ) 139 | 140 | const onLoad = async () => { 141 | if (isLoaded) { 142 | return 143 | } 144 | isLoaded = true 145 | 146 | const document = gradioApp() 147 | 148 | const loadInitialExtranetworks= async () => { 149 | const embeddings = await fetch(GetEmbeddings).then(res => res.json()).catch(e => new Error(`fetch error: ${e}`)) 150 | if (embeddings && embeddings.loaded) { 151 | console.log(me, "load", "embedding", Object.keys(embeddings.loaded)) 152 | MonacoPrompt.addData("embedding", Object.keys(embeddings.loaded), true) 153 | } 154 | 155 | for (const [type, elemId] of [ 156 | ["lora", "setting_sd_lora"], 157 | ["hypernet", "setting_sd_hypernetwork"], 158 | ["lyco", "setting_sd_lyco"], 159 | ]) { 160 | const component = gradio_config.components.filter((c: any) => { 161 | return (c.props.elem_id === elemId && c.props.choices) 162 | })[0] 163 | if (!component) { 164 | continue 165 | } 166 | const choices = component.props.choices.slice() 167 | if (choices[0] === "None") { 168 | choices.shift() 169 | } 170 | console.log(me, "load", type, choices) 171 | MonacoPrompt.addData(type, choices, true) 172 | } 173 | } 174 | await loadInitialExtranetworks() 175 | 176 | let promptLoaded = false 177 | onUiUpdate(async () => { 178 | if (promptLoaded) { 179 | return 180 | } 181 | 182 | const promptIds = [ 183 | "txt2img_prompt", "txt2img_neg_prompt", 184 | "img2img_prompt", "img2img_neg_prompt", 185 | ] 186 | 187 | // not ready 188 | for (const id of promptIds) { 189 | if (document.getElementById(id) === null) { 190 | return 191 | } 192 | } 193 | 194 | const styleEditorIds = [ 195 | // webui 1.6.0+ style editor 196 | "txt2img_edit_style_prompt", "txt2img_edit_style_neg_prompt", 197 | "img2img_edit_style_prompt", "img2img_edit_style_neg_prompt", 198 | ] 199 | 200 | const extraIds = [ 201 | // Wildcards Manager 202 | "file_edit_box_id", 203 | "sddp-wildcard-file-editor", 204 | ] 205 | 206 | promptLoaded = true 207 | 208 | let prevSettings: MonacoPrompt.PromptEditorSettings|null = null 209 | const settings = await fetch(EndPoint, {method: 'GET'}) 210 | .then(res => res.json()) 211 | .catch(err => console.error("fetch error:", EndPoint, err)) 212 | 213 | for (const id of promptIds.concat(styleEditorIds, extraIds)) { 214 | const container = document.getElementById(id) 215 | if (!container) { 216 | continue 217 | } 218 | const textarea = container.querySelector('textarea')! 219 | const editor = new MonacoPrompt.PromptEditor(textarea, { 220 | autoLayout: true, 221 | handleTextAreaValue: true, 222 | overlayZIndex: 99999, 223 | }) 224 | 225 | // custom suggest 226 | for(const {keybinding, model} of models) { 227 | editor.addCustomSuggest(model) 228 | } 229 | editor.addCustomSuggest("snippet") 230 | 231 | editor.addEventListener('keydown', (ev) => { 232 | switch (ev.key) { 233 | case 'Esc': 234 | case 'Escape': 235 | ev.stopPropagation() 236 | break 237 | default: 238 | break 239 | } 240 | }) 241 | if (textarea.parentElement) { 242 | textarea.parentElement.append(editor) 243 | } else { 244 | container.append(editor) 245 | } 246 | Object.assign(editor.style, { 247 | resize: "vertical", 248 | overflow: "overlay", 249 | display: "block", 250 | height: "100%", 251 | minHeight: "20rem", 252 | width: "100%", 253 | }) 254 | 255 | if (styleEditorIds.includes(id)) { 256 | const observer = new IntersectionObserver( 257 | (entry) => { 258 | editor.handleResize() 259 | }, 260 | ) 261 | observer.observe(editor) 262 | } 263 | 264 | editor.setSettings(settings) 265 | 266 | const saveSettings = async () => { 267 | const currentSettings = editor.getSettings() 268 | if (deepEqual(prevSettings, currentSettings)) { 269 | return 270 | } 271 | prevSettings = currentSettings 272 | 273 | const res = await fetch(EndPoint, {method: 'POST', body: JSON.stringify(currentSettings)}) 274 | .then(res => res.json()) 275 | .catch(err => console.error("fetch error:", EndPoint, err)) 276 | if (!res.success) { 277 | console.error("fetch failed:", res) 278 | } 279 | } 280 | 281 | editor.onChange(saveSettings) 282 | } 283 | }) 284 | 285 | const csvs = await fetch(CSV).then(res => res.json()).catch(e => new Error(`fetch error: ${e}`)) 286 | const pathname = srcURL.pathname 287 | // -2 = *.js -> javascript = extension base directory 288 | const basename = pathname.split('/').slice(0, -2) 289 | for (const filename of csvs) { 290 | const path = basename.concat(["csv", filename]).join('/') + ".csv" 291 | 292 | MonacoPrompt.clearCSV() 293 | fetch(path).then(res => res.text()).then((value) => { 294 | console.log("add:", filename, path) 295 | MonacoPrompt.addCSV(filename, value) 296 | return 297 | }) 298 | } 299 | 300 | const extraNetworkCallback: (()=>void)[] = [] 301 | 302 | function onExtraNetworkUpdate(callback: () => void) { 303 | extraNetworkCallback.push(callback) 304 | } 305 | 306 | // TODO: 現在はリフレッシュの開始と終了時を検知してしまっている 307 | function detectRefreshExtraNetwork(mutationRecords: MutationRecord[]) { 308 | for (const mutationRecord of mutationRecords) { 309 | const target = mutationRecord.target as HTMLElement 310 | if (!target || !target.closest) { 311 | continue 312 | } 313 | if (target.closest("#txt2img_extra_tabs")) { 314 | extraNetworkCallback.forEach(callback => callback()) 315 | break 316 | } 317 | } 318 | } 319 | 320 | const cards = [ 321 | { 322 | id: "txt2img_textual_inversion_cards", 323 | type: "embedding", 324 | }, 325 | { 326 | id: "txt2img_hypernetworks_cards", 327 | type: "hypernet" 328 | }, 329 | { 330 | id: "txt2img_lora_cards", 331 | type: "lora" 332 | }, 333 | { 334 | id: "txt2img_lycoris_cards", 335 | type: "lyco" 336 | } 337 | ] 338 | 339 | onExtraNetworkUpdate(() => { 340 | for (const card of cards) { 341 | const base = document.getElementById(card.id) 342 | const result: string[] = [] 343 | if (base) { 344 | base.querySelectorAll(".card .name").forEach((item) => { 345 | const name = item.textContent 346 | if (!name) { 347 | return 348 | } 349 | result.push(name) 350 | }) 351 | } 352 | if (result.length == 0) { 353 | continue 354 | } 355 | console.log(me, "refresh", card.type, result) 356 | MonacoPrompt.addData(card.type, result, true) 357 | } 358 | }) 359 | 360 | onUiUpdate(detectRefreshExtraNetwork) 361 | } 362 | 363 | onUiUpdate(onLoad) 364 | })(new URL((document.currentScript! as HTMLScriptElement).src)) 365 | -------------------------------------------------------------------------------- /src/monaco_utils.ts: -------------------------------------------------------------------------------- 1 | import { editor } from 'monaco-editor' 2 | // @ts-ignore 3 | import { LinkedList } from 'monaco-editor/esm/vs/base/common/linkedList' 4 | // @ts-ignore 5 | import { MenuId, MenuRegistry } from 'monaco-editor/esm/vs/platform/actions/common/actions' 6 | 7 | type ActionDescriptor = { 8 | run: (editor: editor.IStandaloneCodeEditor) => void 9 | label: string 10 | id: string 11 | order: number 12 | groupId: string 13 | commandOptions?: object 14 | } 15 | 16 | interface ActionsPartialDescripter extends Omit { 17 | order?: ActionDescriptor["order"] 18 | groupId?: ActionDescriptor["groupId"] 19 | } 20 | 21 | type SubMenuDescriptor = { 22 | title: string 23 | context: string 24 | group: string 25 | order: number 26 | actions: ActionsPartialDescripter[] 27 | } 28 | 29 | const addActionWithSubMenu = function( 30 | editor: editor.IStandaloneCodeEditor, 31 | descriptor: SubMenuDescriptor 32 | ) { 33 | const submenu = createMenuId(descriptor.context) 34 | const list = new LinkedList() 35 | 36 | MenuRegistry._menuItems.set(submenu, list); 37 | 38 | for (let i = 0, il = descriptor.actions.length; i < il; ++i) { 39 | const action = descriptor.actions[i]; 40 | 41 | if (typeof action.order !== "number") { 42 | action.order = i 43 | } 44 | if (!action.groupId) { 45 | action.groupId = descriptor.group 46 | } 47 | 48 | addActionWithCommandOption(editor, action as ActionDescriptor) 49 | 50 | const actionId = editor.getSupportedActions().find(a => a.label === action.label && a.id.endsWith(action.id))!.id 51 | const items = MenuRegistry._menuItems.get(MenuId.EditorContext) as LinkedList; 52 | const item = popItem(items, actionId); 53 | if (item) { 54 | list.push(item); 55 | } 56 | } 57 | 58 | MenuRegistry._menuItems.get(MenuId.EditorContext).push({ 59 | group: descriptor.group, 60 | order: descriptor.order, 61 | submenu: submenu, 62 | title: descriptor.title, 63 | }) 64 | } 65 | 66 | const removeSubMenu = (id: string) => { 67 | const menuId = getMenuId(id) 68 | 69 | if (!menuId) { 70 | throw new Error(`MenuId(${id}) is not found`) 71 | } 72 | 73 | console.log("remove submenu:", menuId) 74 | MenuRegistry._menuItems.delete(menuId) 75 | } 76 | 77 | const updateSubMenu = (editor: editor.IStandaloneCodeEditor, descriptor: SubMenuDescriptor) => { 78 | removeSubMenu(descriptor.context) 79 | addActionWithSubMenu(editor, descriptor) 80 | } 81 | 82 | const getMenuId = (id: string) => { 83 | return MenuId._instances.get(id) 84 | 85 | } 86 | 87 | const createMenuId = (id: string) => { 88 | const menuId = getMenuId(id) 89 | 90 | if (!menuId) { 91 | return new MenuId(id) 92 | } 93 | 94 | return menuId 95 | } 96 | 97 | const addActionWithCommandOption = function( 98 | editor: editor.IStandaloneCodeEditor, 99 | action: ActionDescriptor 100 | ) { 101 | const retval = editor.addAction({ 102 | id: action.id, 103 | label: action.label, 104 | run: action.run, 105 | contextMenuOrder: action.order, 106 | contextMenuGroupId: action.groupId, 107 | }) 108 | 109 | const actionId = editor.getSupportedActions().find(a => a.label === action.label && a.id.endsWith(action.id))!.id 110 | 111 | const items = MenuRegistry._menuItems.get(MenuId.EditorContext) as LinkedList 112 | const item = findItem(items, actionId) 113 | 114 | if (item && item.element && item.element.command && action.commandOptions) { 115 | Object.assign(item.element.command, action.commandOptions) 116 | } 117 | 118 | return retval 119 | } 120 | 121 | const popItem = (items: LinkedList, id: string): any => { 122 | const node = findItem(items, id) 123 | if (node) { 124 | items._remove(node) 125 | return node.element 126 | } 127 | return null 128 | } 129 | 130 | const findItem = (items: LinkedList, id: string): any => { 131 | let node = items._first; 132 | do { 133 | if (node.element?.command?.id === id) { 134 | return node 135 | } 136 | node = node.next 137 | } while (node !== void 0) 138 | return null 139 | } 140 | 141 | export { 142 | ActionsPartialDescripter, 143 | addActionWithSubMenu, 144 | addActionWithCommandOption, 145 | updateSubMenu, 146 | createMenuId, 147 | removeSubMenu, 148 | getMenuId, 149 | } -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | .inner { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | .main { 9 | flex-grow: 1; 10 | overflow-y: auto; 11 | } 12 | 13 | .header { 14 | display: flex; 15 | justify-content: start; 16 | flex-wrap: wrap; 17 | } 18 | 19 | .footer { 20 | height: 1.5em; 21 | } 22 | 23 | .monaco { 24 | height: 100%; 25 | } 26 | 27 | .status { 28 | } 29 | 30 | .ms-choice { 31 | height: 100%; 32 | line-height: normal; 33 | } 34 | 35 | .find-lines-table { 36 | border-spacing: 0; 37 | } 38 | .find-line-number { 39 | text-align: right; 40 | vertical-align: text-top; 41 | white-space: nowrap; 42 | padding: 0; 43 | padding-right: 1rem; 44 | opacity: 0.5; 45 | } 46 | 47 | .find-line-active { 48 | background-color: rgba(255, 255, 192, 0.1) 49 | } 50 | 51 | .find-line-content { 52 | overflow-wrap: anywhere; 53 | } -------------------------------------------------------------------------------- /src/styles/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const styles: { [className: string]: string }; 3 | export default styles; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function escapeHTML(unsafe: string) { 2 | return unsafe 3 | .replaceAll('&', '&') 4 | .replaceAll('<', '<') 5 | .replaceAll('>', '>') 6 | .replaceAll('"', '"') 7 | .replaceAll("'", ''') 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "compilerOptions": { 6 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 7 | /* Basic Options */ 8 | // "incremental": true, /* Enable incremental compilation */ 9 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 10 | //"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 11 | "module": "es2022", 12 | // "lib": [], /* Specify library files to be included in the compilation. */ 13 | "allowJs": true, 14 | "noFallthroughCasesInSwitch": true, 15 | //"isolatedModules": true, 16 | //"noEmit": true, 17 | // "allowJs": true, /* Allow javascript files to be compiled. */ 18 | // "checkJs": true, /* Report errors in .js files. */ 19 | "jsx": "react-jsx", 20 | "jsxImportSource": "preact", 21 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 22 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 23 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 24 | //"outFile": "./dist/out.js", /* Concatenate and emit output to single file. */ 25 | "outDir": "./dist/src/", /* Redirect output structure to the directory. */ 26 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 27 | //"composite": true, /* Enable project compilation */ 28 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 29 | // "removeComments": true, /* Do not emit comments to output. */ 30 | // "noEmit": true, /* Do not emit outputs. */ 31 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 32 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 33 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 34 | /* Strict Type-Checking Options */ 35 | "strict": true, /* Enable all strict type-checking options. */ 36 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 37 | // "strictNullChecks": true, /* Enable strict null checks. */ 38 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 39 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 40 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 41 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 42 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 43 | /* Additional Checks */ 44 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 45 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 46 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 47 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 48 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 49 | /* Module Resolution Options */ 50 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 51 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 52 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 53 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 54 | "typeRoots": [ 55 | "src/@types", 56 | "node_modules/@types" 57 | ], /* List of folders to include type definitions from. */ 58 | // "types": [], /* Type declaration files to be included in compilation. */ 59 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 60 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 61 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 62 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 63 | /* Source Map Options */ 64 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 67 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 68 | /* Experimental Options */ 69 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 70 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 71 | /* Advanced Options */ 72 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 73 | "resolveJsonModule": true, 74 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 75 | } 76 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*", 4 | "test/**/*" 5 | ], 6 | "compilerOptions": { 7 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 8 | /* Basic Options */ 9 | // "incremental": true, /* Enable incremental compilation */ 10 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 11 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 12 | //"module": "esnext", 13 | // "lib": [], /* Specify library files to be included in the compilation. */ 14 | "allowJs": true, 15 | "noFallthroughCasesInSwitch": true, 16 | //"isolatedModules": true, 17 | //"noEmit": true, 18 | // "allowJs": true, /* Allow javascript files to be compiled. */ 19 | // "checkJs": true, /* Report errors in .js files. */ 20 | "jsx": "react-jsx", 21 | "jsxImportSource": "preact", 22 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 23 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 24 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 25 | //"outFile": "./dist/out.js", /* Concatenate and emit output to single file. */ 26 | "outDir": "./test-dist/", /* Redirect output structure to the directory. */ 27 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 28 | //"composite": true, /* Enable project compilation */ 29 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 30 | // "removeComments": true, /* Do not emit comments to output. */ 31 | // "noEmit": true, /* Do not emit outputs. */ 32 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 33 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 34 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 35 | /* Strict Type-Checking Options */ 36 | "strict": true, /* Enable all strict type-checking options. */ 37 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 38 | // "strictNullChecks": true, /* Enable strict null checks. */ 39 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 40 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 41 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 42 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 43 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 44 | /* Additional Checks */ 45 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 46 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 47 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 48 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 49 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 50 | /* Module Resolution Options */ 51 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 52 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 53 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 54 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 55 | "typeRoots": [ 56 | "src/@types", 57 | "node_modules/@types" 58 | ], /* List of folders to include type definitions from. */ 59 | // "types": [], /* Type declaration files to be included in compilation. */ 60 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 61 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 62 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 63 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 64 | /* Source Map Options */ 65 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 67 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 68 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 69 | /* Experimental Options */ 70 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 71 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 72 | /* Advanced Options */ 73 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 74 | "resolveJsonModule": true, 75 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 76 | } 77 | } -------------------------------------------------------------------------------- /webpack.comfy.dev.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.comfy.js'); 2 | 3 | module.exports = Object.assign(common, { 4 | mode: 'development', 5 | devtool: 'inline-source-map', 6 | }) 7 | -------------------------------------------------------------------------------- /webpack.comfy.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const common = require('./webpack.common.js') 3 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin') 4 | const CopyWebpackPlugin = require('copy-webpack-plugin') 5 | 6 | const projectRootDir = path.dirname(__filename) 7 | 8 | module.exports = Object.assign(common, { 9 | mode: 'production', 10 | entry: { 11 | main: './src/comfyui/index.ts', 12 | }, 13 | resolve: { 14 | extensions: ['.ts', '.js', 'tsx'] 15 | }, 16 | output: { 17 | filename: '[name].bundle.js', 18 | library: 'MonacoPrompt', 19 | libraryTarget: 'umd', 20 | path: path.resolve(__dirname, 'comfy'), 21 | publicPath: "", 22 | }, 23 | plugins: [ 24 | new MonacoWebpackPlugin({ 25 | filename: '[name].worker.mjs', 26 | languages: [], 27 | }), 28 | ] 29 | }) 30 | 31 | const staticPathFormat = path.join(projectRootDir, "comfy", "[name][ext]") 32 | module.exports.plugins.push( 33 | new CopyWebpackPlugin({ 34 | patterns: [ 35 | { 36 | from: 'csv/*.csv', 37 | to: staticPathFormat 38 | }, 39 | { 40 | from: 'src/comfyui/static/*', 41 | to: staticPathFormat 42 | } 43 | ] 44 | }) 45 | ) 46 | /* 47 | module.exports.module.rules.push({ 48 | test: /api\.js$/, 49 | type: 'asset/resource', 50 | }) 51 | */ 52 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin') 3 | 4 | module.exports = { 5 | entry: { 6 | main: './src/main.ts', 7 | }, 8 | resolve: { 9 | extensions: ['.ts', '.js', 'tsx'] 10 | }, 11 | output: { 12 | filename: '[name].bundle.js', 13 | library: 'MonacoPrompt', 14 | libraryTarget: 'umd', 15 | path: path.resolve(__dirname, 'javascript') 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(ts|tsx)?$/, 21 | use: 'ts-loader', 22 | exclude: /node_modules/ 23 | }, 24 | { 25 | test: /\.(css)$/, 26 | use: ['style-loader', 'css-loader'], 27 | include: /node_modules/ 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader:'css-loader', 35 | options: { 36 | modules: { 37 | localIdentName: '[name]__[local]___[hash:base64:5]', 38 | } 39 | } 40 | } 41 | ], 42 | exclude: /node_modules/ 43 | }, 44 | { 45 | test: /\.ttf$/, 46 | type: 'asset/resource', 47 | generator: { 48 | filename: "[name][ext]" 49 | } 50 | } 51 | ], 52 | parser: { 53 | javascript: { importMeta: false }, 54 | }, 55 | }, 56 | plugins: [ 57 | new MonacoWebpackPlugin({ 58 | languages: [], 59 | }) 60 | ] 61 | }; 62 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.js'); 2 | 3 | module.exports = Object.assign(common, { 4 | mode: 'development', 5 | devtool: 'inline-source-map', 6 | devServer: { 7 | static: './javascript', 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /webpack.editor.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = Object.assign( 5 | common, 6 | { 7 | mode: 'development', 8 | entry: { 9 | index: './src/index.ts', 10 | }, 11 | output: { 12 | filename: '[name].bundle.js', 13 | library: 'MonacoPrompt', 14 | libraryTarget: 'umd', 15 | path: path.resolve(__dirname, 'dist') 16 | }, 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.js'); 2 | 3 | module.exports = Object.assign(common, { 4 | mode: 'production', 5 | }) 6 | --------------------------------------------------------------------------------