├── .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 |
--------------------------------------------------------------------------------