@kispace-io/extension-monaco-editor 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@kispace-io/extension-monaco-editor",
3
+ "version": "0.8.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./src/index.ts",
9
+ "types": "./src/index.ts"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "@kispace-io/core": "*",
14
+ "@kispace-io/extension-python-runtime": "*",
15
+ "@kispace-io/extension-ai-system": "*",
16
+ "monaco-editor": "0.55.1"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.9.3"
20
+ }
21
+ }
package/src/i18n.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "namespace": "extensions",
3
+ "en": {
4
+ "EXT_MONACO_NAME": "Monaco Code Editor",
5
+ "EXT_MONACO_DESC": "Code editor with syntax highlighting, code completion, and Python execution support"
6
+ },
7
+ "de": {
8
+ "EXT_MONACO_NAME": "Monaco-Code-Editor",
9
+ "EXT_MONACO_DESC": "Code-Editor mit Syntaxhervorhebung, Code-Vervollständigung und Python-Ausführungsunterstützung"
10
+ }
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { extensionRegistry, i18nLazy, contributionRegistry, SYSTEM_LANGUAGE_BUNDLES } from '@kispace-io/core';
2
+ import bundle from './i18n.json';
3
+
4
+ contributionRegistry.registerContribution(SYSTEM_LANGUAGE_BUNDLES, bundle as any);
5
+
6
+ const t = i18nLazy('extensions');
7
+
8
+ extensionRegistry.registerExtension({
9
+ id: "system.monaco",
10
+ name: t('EXT_MONACO_NAME'),
11
+ description: t('EXT_MONACO_DESC'),
12
+ loader: () => import("./monaco-editor-extension"),
13
+ icon: "file-pen",
14
+
15
+ dependencies: ["system.pythonruntime"],
16
+ });
@@ -0,0 +1,20 @@
1
+ **JavaScript Programming Assistant:**
2
+
3
+ You are helping with JavaScript code. JavaScript code is executed in a worker.
4
+
5
+ **JavaScript-Specific Focus:**
6
+ - Help with JavaScript, OpenLayers usage, and module patterns
7
+ - When a user requests a starter module, provide this template:
8
+
9
+ ```javascript
10
+ export default function ({ map, env, render, html, style, when, ref, createRef, events, settings }) {
11
+ const state = {};
12
+ return () => html`
13
+ <div>
14
+ <h3>Hello!</h3>
15
+ <button @click=${() => {}}>Click me</button>
16
+ </div>
17
+ `;
18
+ }
19
+ ```
20
+
@@ -0,0 +1,342 @@
1
+ import * as monaco from 'monaco-editor';
2
+ import styles from "monaco-editor/min/vs/editor/editor.main.css?raw";
3
+ import {customElement, property, state} from "lit/decorators.js";
4
+ import {KPart} from "@kispace-io/core";
5
+ import {css, html} from "lit";
6
+ import {createRef, ref} from "lit/directives/ref.js";
7
+ import {EditorInput, EditorContentProvider} from "@kispace-io/core";
8
+ import {styleMap} from "lit/directives/style-map.js";
9
+ import { PyEnv, pythonPackageManagerService } from "@kispace-io/extension-python-runtime";
10
+ import { workspaceService } from "@kispace-io/core";
11
+ import { logger } from '@kispace-io/core';
12
+
13
+ const workerMap: Record<string, string> = {
14
+ 'json': new URL('monaco-editor/esm/vs/language/json/json.worker.js', import.meta.url).href,
15
+ 'css': new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url).href,
16
+ 'scss': new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url).href,
17
+ 'less': new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url).href,
18
+ 'html': new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url).href,
19
+ 'handlebars': new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url).href,
20
+ 'razor': new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url).href,
21
+ 'typescript': new URL('monaco-editor/esm/vs/language/typescript/ts.worker.js', import.meta.url).href,
22
+ 'javascript': new URL('monaco-editor/esm/vs/language/typescript/ts.worker.js', import.meta.url).href,
23
+ };
24
+
25
+ self.MonacoEnvironment = {
26
+ getWorkerUrl(_: any, label: string) {
27
+ return workerMap[label] || new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url).href;
28
+ }
29
+ };
30
+
31
+ @customElement('k-monaco-editor')
32
+ export class KMonacoEditor extends KPart implements EditorContentProvider {
33
+ @property({attribute: false})
34
+ public input?: EditorInput;
35
+ @property()
36
+ public readOnly: boolean = false;
37
+
38
+ private editorRef = createRef();
39
+ private editor?: any;
40
+ private model?: any;
41
+
42
+ @state()
43
+ private canExecute: boolean = false;
44
+
45
+ @state()
46
+ private pyenv?: PyEnv;
47
+
48
+ @state()
49
+ private requiredPackages: string[] = [];
50
+
51
+
52
+ protected doBeforeUI() {
53
+ const file = this.input!.data
54
+ this.canExecute = file.getName().endsWith(".py")
55
+ }
56
+
57
+ protected updated(changedProperties: Map<string, any>) {
58
+ super.updated(changedProperties);
59
+
60
+ if (changedProperties.has('canExecute') || changedProperties.has('pyenv')) {
61
+ this.updateToolbar();
62
+ }
63
+ }
64
+
65
+ protected async doInitUI() {
66
+ const file = this.input!.data
67
+ const textContents = await file.getContents()
68
+ const container = this.editorRef.value as HTMLElement
69
+ const uri = monaco.Uri.file(file.getName())
70
+ this.model = monaco.editor.createModel(textContents, undefined, uri)
71
+ this.editor = monaco.editor.create(container, {
72
+ theme: 'vs-dark',
73
+ automaticLayout: false,
74
+ })
75
+ this.model.onDidChangeContent((_event: Event) => {
76
+ if (this.readOnly) {
77
+ return
78
+ }
79
+ this.markDirty(true)
80
+ })
81
+ this.editor.setModel(this.model)
82
+
83
+ if (this.canExecute) {
84
+ this.requiredPackages = this.parsePackagesFromContent(textContents as string)
85
+ }
86
+ }
87
+
88
+ public getEditor() {
89
+ return this.editor
90
+ }
91
+
92
+ save(): void {
93
+ const value = this.model.getValue()
94
+ this.input?.data.saveContents(value)
95
+ this.markDirty(false)
96
+ }
97
+
98
+ protected doClose() {
99
+ this.model?.dispose();
100
+ this.editor?.dispose()
101
+ this.model = undefined
102
+ this.editor = undefined
103
+ this.pyenv?.close()
104
+ this.pyenv = undefined
105
+ }
106
+
107
+ private async onRunCode() {
108
+ if (!this.canExecute) {
109
+ return
110
+ }
111
+ if (!this.pyenv) {
112
+ await this.toggleConnection();
113
+ }
114
+ this.pyenv?.execCode(this.model.getValue())
115
+ }
116
+
117
+ public getLanguage() {
118
+ return this.model?.getLanguageId()?.toLowerCase() || null
119
+ }
120
+
121
+ public getContent(): string | null {
122
+ if (!this.model) {
123
+ return null
124
+ }
125
+ return this.model.getValue()
126
+ }
127
+
128
+ public getSelection(): string | null {
129
+ if (!this.editor || !this.model) {
130
+ return null
131
+ }
132
+ try {
133
+ const selection = this.editor.getSelection()
134
+ if (!selection || selection.isEmpty()) {
135
+ return null
136
+ }
137
+ return this.model.getValueInRange(selection) || null
138
+ } catch (err) {
139
+ return null
140
+ }
141
+ }
142
+
143
+ public getSnippet(lines: number = 5): { snippet: string; cursorLine: number } | null {
144
+ if (!this.editor || !this.model) {
145
+ return null
146
+ }
147
+ try {
148
+ const position = this.editor.getPosition()
149
+ if (!position) {
150
+ return null
151
+ }
152
+
153
+ if (isNaN(lines) || lines < 0) {
154
+ lines = 5
155
+ }
156
+
157
+ const cursorLineNumber = position.lineNumber
158
+ const totalLines = this.model.getLineCount()
159
+
160
+ const startLine = Math.max(1, cursorLineNumber - lines)
161
+ const endLine = Math.min(totalLines, cursorLineNumber + lines)
162
+
163
+ const snippet = this.model.getValueInRange({
164
+ startLineNumber: startLine,
165
+ startColumn: 1,
166
+ endLineNumber: endLine,
167
+ endColumn: this.model.getLineMaxColumn(endLine)
168
+ })
169
+
170
+ return {
171
+ snippet,
172
+ cursorLine: cursorLineNumber
173
+ }
174
+ } catch (err) {
175
+ return null
176
+ }
177
+ }
178
+
179
+ public getFilePath(): string | null {
180
+ return this.input?.data?.getWorkspacePath() || null
181
+ }
182
+
183
+ protected renderToolbar() {
184
+ if (!this.canExecute) {
185
+ return html``;
186
+ }
187
+
188
+ return html`
189
+ <wa-button @click=${() => this.onRunCode()} title="Run code"
190
+ ?disabled="${!this.canExecute}" appearance="plain" size="small">
191
+ <wa-icon name="play" label="Run code"></wa-icon>
192
+ </wa-button>
193
+ <wa-button @click=${() => this.toggleConnection()}
194
+ style="${styleMap({color: this.pyenv ? "var(--wa-color-success-fill-loud)" : "var(--wa-color-danger-fill-loud)"})}"
195
+ title="(Re)Connect to execution environment"
196
+ ?disabled="${!this.canExecute}"
197
+ appearance="plain" size="small">
198
+ <wa-icon name="circle" label="Connection status"></wa-icon>
199
+ </wa-button>
200
+ <wa-button
201
+ size="small"
202
+ appearance="plain"
203
+ @click=${() => this.openPackageManager()}
204
+ title="Manage required Python packages">
205
+ <wa-icon name="box" label="Packages"></wa-icon>
206
+ Packages
207
+ </wa-button>
208
+ `;
209
+ }
210
+
211
+ private async toggleConnection() {
212
+ if (this.pyenv) {
213
+ this.pyenv.close()
214
+ this.pyenv = undefined
215
+ }
216
+ if (this.model.getLanguageId() !== "python") {
217
+ logger.error("Language not supported: " + this.model.getLanguageId());
218
+ return
219
+ }
220
+ this.pyenv = new PyEnv()
221
+ const workspace = await workspaceService.getWorkspace()
222
+ await this.pyenv.init(workspace!)
223
+
224
+ if (this.requiredPackages.length > 0) {
225
+ try {
226
+ await this.pyenv.loadPackages(this.requiredPackages)
227
+ } catch (error) {
228
+ logger.error("Failed to load required packages: " + String(error))
229
+ }
230
+ }
231
+ }
232
+
233
+ private parsePackagesFromContent(content: string): string[] {
234
+ const lines = content.split('\n')
235
+ const magicCommentRegex = /^#\s*@gs-packages:\s*(.+)$/i
236
+
237
+ for (const line of lines) {
238
+ const match = line.match(magicCommentRegex)
239
+ if (match) {
240
+ return match[1]
241
+ .split(',')
242
+ .map(pkg => pkg.trim())
243
+ .filter(pkg => pkg.length > 0)
244
+ }
245
+ }
246
+
247
+ return []
248
+ }
249
+
250
+ private updatePackagesInContent(): void {
251
+ if (!this.model) return
252
+
253
+ const content = this.model.getValue()
254
+ const lines = content.split('\n')
255
+ const magicCommentRegex = /^#\s*@gs-packages:/i
256
+
257
+ let magicCommentLineIndex = -1
258
+ for (let i = 0; i < lines.length; i++) {
259
+ if (magicCommentRegex.test(lines[i])) {
260
+ magicCommentLineIndex = i
261
+ break
262
+ }
263
+ }
264
+
265
+ const newMagicComment = this.requiredPackages.length > 0
266
+ ? `# @gs-packages: ${this.requiredPackages.join(', ')}`
267
+ : null
268
+
269
+ if (newMagicComment) {
270
+ if (magicCommentLineIndex >= 0) {
271
+ lines[magicCommentLineIndex] = newMagicComment
272
+ } else {
273
+ const insertIndex = lines[0]?.startsWith('#!') ? 1 : 0
274
+ lines.splice(insertIndex, 0, newMagicComment)
275
+ }
276
+ } else if (magicCommentLineIndex >= 0) {
277
+ lines.splice(magicCommentLineIndex, 1)
278
+ }
279
+
280
+ const newContent = lines.join('\n')
281
+ if (newContent !== content) {
282
+ this.model.setValue(newContent)
283
+ }
284
+ }
285
+
286
+ private openPackageManager() {
287
+ pythonPackageManagerService.showPackageManager({
288
+ packages: this.requiredPackages,
289
+ pyenv: this.pyenv,
290
+ onPackageAdded: (packageName: string) => {
291
+ if (!this.requiredPackages.includes(packageName)) {
292
+ this.requiredPackages = [...this.requiredPackages, packageName]
293
+ this.updatePackagesInContent()
294
+ }
295
+ },
296
+ onPackageRemoved: (packageName: string) => {
297
+ this.requiredPackages = this.requiredPackages.filter(pkg => pkg !== packageName)
298
+ this.updatePackagesInContent()
299
+ }
300
+ })
301
+ }
302
+
303
+ render() {
304
+ return html`
305
+ <style>
306
+ ${styles}
307
+ </style>
308
+ <div class="monaco-editor-container" ${ref(this.editorRef)}>
309
+ </div>
310
+ `
311
+ }
312
+
313
+ static styles = css`
314
+ :host {
315
+ display: flex;
316
+ flex-direction: column;
317
+ position: relative;
318
+ width: 100%;
319
+ height: 100%;
320
+ }
321
+
322
+ input.prompt {
323
+ flex: 1;
324
+ font-size: large;
325
+ }
326
+
327
+ div.monaco-editor-container {
328
+ position: absolute;
329
+ top: 0;
330
+ left: 0;
331
+ right: 0;
332
+ bottom: 0;
333
+ }
334
+ `;
335
+ }
336
+
337
+ declare global {
338
+ interface HTMLElementTagNameMap {
339
+ 'k-monaco-editor': KMonacoEditor
340
+ }
341
+ }
342
+
@@ -0,0 +1,72 @@
1
+ import {html} from "lit";
2
+ import {EditorInput, editorRegistry} from "@kispace-io/core";
3
+ import {File} from "@kispace-io/core";
4
+ import {contributionRegistry} from "@kispace-io/core";
5
+ import {CID_PROMPT_ENHANCERS, type PromptEnhancer, type PromptEnhancerContribution} from "@kispace-io/extension-ai-system";
6
+ import type {ExecutionContext} from "@kispace-io/core";
7
+ import PYTHON_PROMPT from "./py-programming-prompt.txt?raw";
8
+ import JAVASCRIPT_PROMPT from "./js-programming-prompt.txt?raw";
9
+
10
+ editorRegistry.registerEditorInputHandler({
11
+ lazyInit: async () => {
12
+ await import('./k-monaco-editor');
13
+ },
14
+ canHandle: input => input instanceof File,
15
+ handle: async (input: File) => {
16
+ const editorInput = {
17
+ title: input.getName(),
18
+ data: input,
19
+ key: input.getName(),
20
+ editorId: "monaco-editor",
21
+ icon: "file-pen",
22
+ noOverflow: false,
23
+ state: {},
24
+ } as EditorInput
25
+ editorInput.widgetFactory = () => html`
26
+ <k-monaco-editor .input=${editorInput}></k-monaco-editor>`
27
+ return editorInput;
28
+ }
29
+ })
30
+
31
+ // Helper to check if active editor is a Monaco editor with specific language
32
+ function isMonacoEditorWithLanguage(context: ExecutionContext, language: string): boolean {
33
+ const activeEditor = context.activeEditor as any;
34
+ return activeEditor &&
35
+ typeof activeEditor.getEditor === 'function' &&
36
+ typeof activeEditor.getLanguage === 'function' &&
37
+ activeEditor.getLanguage() === language;
38
+ }
39
+
40
+ // Python programming prompt enhancer
41
+ const pythonPromptEnhancer: PromptEnhancer = {
42
+ priority: 50,
43
+ enhance: async (prompt: string, context: ExecutionContext) => {
44
+ if (!isMonacoEditorWithLanguage(context, 'python')) {
45
+ return prompt;
46
+ }
47
+ return `${prompt}\n\n${PYTHON_PROMPT}`;
48
+ }
49
+ };
50
+
51
+ // JavaScript programming prompt enhancer
52
+ const javascriptPromptEnhancer: PromptEnhancer = {
53
+ priority: 50,
54
+ enhance: async (prompt: string, context: ExecutionContext) => {
55
+ if (!isMonacoEditorWithLanguage(context, 'javascript')) {
56
+ return prompt;
57
+ }
58
+ return `${prompt}\n\n${JAVASCRIPT_PROMPT}`;
59
+ }
60
+ };
61
+
62
+ // Register prompt enhancers for programming languages
63
+ contributionRegistry.registerContribution(CID_PROMPT_ENHANCERS, {
64
+ label: "Python Programming Enhancer",
65
+ enhancer: pythonPromptEnhancer
66
+ } as PromptEnhancerContribution);
67
+
68
+ contributionRegistry.registerContribution(CID_PROMPT_ENHANCERS, {
69
+ label: "JavaScript Programming Enhancer",
70
+ enhancer: javascriptPromptEnhancer
71
+ } as PromptEnhancerContribution);
72
+
@@ -0,0 +1,15 @@
1
+ **Python Programming Assistant:**
2
+
3
+ You are helping with Python code. Python code is executed in the browser using Pyodide.
4
+
5
+ **Python-Specific Responsibilities:**
6
+ - Explain Python code and help debug Python errors
7
+ - Help with `js` module interaction: calling JavaScript functions from Python and passing data between Python and JavaScript
8
+ - Provide Python code examples
9
+ - Remember code runs in a browser environment (Pyodide) - be mindful of browser limitations
10
+
11
+ **Python Environment:**
12
+ - Working directory is `/workspace` (linked to user's local file system)
13
+ - Install packages by adding them to `requirements.txt` in the workspace
14
+ - Packages in `requirements.txt` are automatically installed before code execution
15
+
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true
8
+ },
9
+ "include": [
10
+ "src/**/*"
11
+ ]
12
+ }