Skip to content

Commit 33395a2

Browse files
authored
Merge pull request #38 from Sandijigs/monaco-editor-branch
feat: integrate monaco editor
2 parents fa0586a + 1ee26fe commit 33395a2

8 files changed

Lines changed: 307 additions & 5 deletions

File tree

apps/frontend/angular.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
{
2020
"glob": "**/*",
2121
"input": "public"
22+
},
23+
{
24+
"glob": "**/*",
25+
"input": "./node_modules/monaco-editor/min",
26+
"output": "/assets/monaco-editor/min"
2227
}
2328
],
2429
"styles": [

apps/frontend/bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
"@angular/platform-server": "^20.1.0",
3333
"@angular/router": "^20.1.0",
3434
"@angular/ssr": "^20.1.4",
35+
"@materia-ui/ngx-monaco-editor": "^6.0.0",
3536
"express": "^5.1.0",
37+
"monaco-editor": "^0.52.2",
3638
"rxjs": "~7.8.0",
3739
"tslib": "^2.3.0"
3840
},

apps/frontend/src/app/app.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.editor-container {
2+
margin-top: 2rem;
3+
width: 100%;
4+
max-width: 800px;
5+
}
6+
7+
.editor-container h3 {
8+
color: var(--gray-900);
9+
margin-bottom: 1rem;
10+
font-size: 1.5rem;
11+
font-weight: 500;
12+
}
13+
14+
.content {
15+
flex-direction: column !important;
16+
max-width: 1200px !important;
17+
align-items: flex-start !important;
18+
}
19+
20+
.left-side {
21+
width: 100% !important;
22+
max-width: none !important;
23+
}
24+
25+
@media screen and (max-width: 650px) {
26+
.editor-container {
27+
width: 100%;
28+
}
29+
}

apps/frontend/src/app/app.html

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,22 @@
186186
<div class="content">
187187
<div class="left-side">
188188
<img src="Scaffold-Rust-Logo.jpg" alt="" width="30">
189-
<h1>Hello, awesome developer!</h1>
190-
<p>Congratulations! Your app is running. 🎉</p>
191-
<p>We are very excited that you are contributing to this great project. This is just the Angular template, so everything needs to be modified. [including this text]</p>
189+
<h1>Online Soroban Compiler</h1>
190+
<p>Write and test your Stellar smart contracts! 🚀</p>
191+
<p>Use the Monaco Editor below to write Rust smart contracts for the Stellar blockchain using the Soroban SDK.</p>
192+
193+
<!-- Monaco Editor -->
194+
<div class="editor-container">
195+
<h3>Rust Smart Contract Editor</h3>
196+
<app-monaco-editor
197+
[language]="'rust'"
198+
[theme]="'vs-dark'"
199+
[height]="'500px'"
200+
[options]="{ minimap: { enabled: false } }"
201+
[(ngModel)]="rustCode"
202+
(ngModelChange)="onEditorChange($event)">
203+
</app-monaco-editor>
204+
</div>
192205
</div>
193206
<div class="divider" role="separator" aria-label="Divider"></div>
194207
<div class="right-side">

apps/frontend/src/app/app.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ import { provideZonelessChangeDetection } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
33
import { App } from './app';
44

5+
// Mock Monaco Editor for tests
6+
(globalThis as unknown as { monaco: unknown }).monaco = {
7+
editor: {
8+
create: () => ({
9+
getValue: () => '',
10+
setValue: () => {},
11+
dispose: () => {},
12+
onDidChangeModelContent: () => {},
13+
updateOptions: () => {},
14+
layout: () => {}
15+
})
16+
}
17+
};
18+
519
describe('App', () => {
620
beforeEach(async () => {
721
await TestBed.configureTestingModule({
@@ -20,6 +34,6 @@ describe('App', () => {
2034
const fixture = TestBed.createComponent(App);
2135
fixture.detectChanges();
2236
const compiled = fixture.nativeElement as HTMLElement;
23-
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
37+
expect(compiled.querySelector('h1')?.textContent).toContain('Online Soroban Compiler');
2438
});
2539
});

apps/frontend/src/app/app.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
11
import { Component, signal } from '@angular/core';
22
import { RouterOutlet } from '@angular/router';
3+
import { FormsModule } from '@angular/forms';
4+
import { MonacoEditorComponent } from './monaco-editor/monaco-editor.component';
35

46
@Component({
57
selector: 'app-root',
6-
imports: [RouterOutlet],
8+
imports: [RouterOutlet, MonacoEditorComponent, FormsModule],
79
templateUrl: './app.html',
810
styleUrl: './app.css'
911
})
1012
export class App {
1113
protected readonly title = signal('frontend');
14+
protected readonly rustCode = signal(`// Sample Rust smart contract for Stellar
15+
use soroban_sdk::{contract, contractimpl, log, Env, Symbol, symbol_short};
16+
17+
#[contract]
18+
pub struct HelloContract;
19+
20+
#[contractimpl]
21+
impl HelloContract {
22+
/// Says hello to someone
23+
pub fn hello(env: Env, to: Symbol) -> Symbol {
24+
log!(&env, "Hello {}", to);
25+
symbol_short!("Hello")
26+
}
27+
28+
/// Returns a greeting
29+
pub fn greet(env: Env, name: Symbol) -> Symbol {
30+
log!(&env, "Greeting {}", name);
31+
symbol_short!("Greet")
32+
}
33+
}`);
34+
35+
onEditorChange(value: string) {
36+
this.rustCode.set(value);
37+
}
1238
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { Component, Input, forwardRef, OnInit, OnDestroy, ViewEncapsulation, signal, effect, ViewChild, ElementRef, inject, PLATFORM_ID } from '@angular/core';
2+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
3+
import { isPlatformBrowser } from '@angular/common';
4+
5+
interface MonacoEditor {
6+
getValue(): string;
7+
setValue(value: string): void;
8+
dispose(): void;
9+
onDidChangeModelContent(callback: () => void): void;
10+
updateOptions(options: MonacoEditorOptions): void;
11+
layout(): void;
12+
}
13+
14+
interface MonacoEditorOptions {
15+
value?: string;
16+
language?: string;
17+
theme?: string;
18+
minimap?: { enabled: boolean };
19+
automaticLayout?: boolean;
20+
scrollBeyondLastLine?: boolean;
21+
fontSize?: number;
22+
wordWrap?: string;
23+
lineNumbers?: string;
24+
glyphMargin?: boolean;
25+
folding?: boolean;
26+
lineDecorationsWidth?: number;
27+
lineNumbersMinChars?: number;
28+
renderLineHighlight?: string;
29+
contextmenu?: boolean;
30+
mouseWheelZoom?: boolean;
31+
readOnly?: boolean;
32+
}
33+
34+
interface WindowRequire {
35+
config(config: { paths: { vs: string } }): void;
36+
(modules: string[], callback: () => void): void;
37+
}
38+
39+
declare const monaco: {
40+
editor: {
41+
create(element: HTMLElement, options: MonacoEditorOptions): MonacoEditor;
42+
};
43+
};
44+
45+
declare global {
46+
interface Window {
47+
require: WindowRequire;
48+
}
49+
}
50+
51+
@Component({
52+
selector: 'app-monaco-editor',
53+
template: `<div #editorContainer style="height: 100%;"></div>`,
54+
styleUrls: [],
55+
encapsulation: ViewEncapsulation.None,
56+
providers: [
57+
{
58+
provide: NG_VALUE_ACCESSOR,
59+
useExisting: forwardRef(() => MonacoEditorComponent),
60+
multi: true
61+
}
62+
],
63+
imports: [FormsModule],
64+
standalone: true
65+
})
66+
export class MonacoEditorComponent implements OnInit, OnDestroy, ControlValueAccessor {
67+
@Input() language = 'rust';
68+
@Input() theme = 'vs-dark';
69+
@Input() height = '500px';
70+
@Input() options: MonacoEditorOptions = {};
71+
72+
@ViewChild('editorContainer', { static: true }) editorContainer!: ElementRef<HTMLDivElement>;
73+
74+
private editor: MonacoEditor | null = null;
75+
private currentValue = signal('');
76+
77+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
78+
private onChange = (_value: string) => { /* no-op */ };
79+
private onTouched = () => {};
80+
81+
private platformId = inject(PLATFORM_ID);
82+
83+
constructor() {
84+
effect(() => {
85+
if (this.editor && this.currentValue() !== this.editor.getValue()) {
86+
this.editor.setValue(this.currentValue());
87+
}
88+
});
89+
}
90+
91+
ngOnInit() {
92+
this.loadMonacoEditor();
93+
}
94+
95+
ngOnDestroy() {
96+
if (this.editor) {
97+
this.editor.dispose();
98+
}
99+
}
100+
101+
private loadMonacoEditor() {
102+
// Only load Monaco Editor in the browser
103+
if (!isPlatformBrowser(this.platformId)) {
104+
return;
105+
}
106+
107+
// Load Monaco Editor
108+
if (typeof monaco === 'undefined') {
109+
const script = document.createElement('script');
110+
script.type = 'text/javascript';
111+
script.src = '/assets/monaco-editor/min/vs/loader.js';
112+
script.onload = () => {
113+
window.require.config({
114+
paths: { vs: '/assets/monaco-editor/min/vs' }
115+
});
116+
window.require(['vs/editor/editor.main'], () => {
117+
this.initEditor();
118+
});
119+
};
120+
script.onerror = () => {
121+
this.loadFromCDN();
122+
};
123+
document.getElementsByTagName('head')[0].appendChild(script);
124+
} else {
125+
this.initEditor();
126+
}
127+
}
128+
129+
private loadFromCDN() {
130+
if (!isPlatformBrowser(this.platformId)) {
131+
return;
132+
}
133+
134+
const script = document.createElement('script');
135+
script.src = 'https://unpkg.com/monaco-editor@latest/min/vs/loader.js';
136+
script.onload = () => {
137+
window.require.config({
138+
paths: { vs: 'https://unpkg.com/monaco-editor@latest/min/vs' }
139+
});
140+
window.require(['vs/editor/editor.main'], () => {
141+
this.initEditor();
142+
});
143+
};
144+
document.head.appendChild(script);
145+
}
146+
147+
private initEditor() {
148+
const editorElement = this.editorContainer.nativeElement;
149+
if (!editorElement) {
150+
setTimeout(() => this.initEditor(), 100);
151+
return;
152+
}
153+
154+
const defaultOptions: MonacoEditorOptions = {
155+
value: this.currentValue(),
156+
language: this.language,
157+
theme: this.theme,
158+
minimap: { enabled: false },
159+
automaticLayout: true,
160+
scrollBeyondLastLine: false,
161+
fontSize: 14,
162+
wordWrap: 'on',
163+
lineNumbers: 'on',
164+
glyphMargin: false,
165+
folding: true,
166+
lineDecorationsWidth: 20,
167+
lineNumbersMinChars: 3,
168+
renderLineHighlight: 'line',
169+
contextmenu: true,
170+
mouseWheelZoom: true,
171+
...this.options
172+
};
173+
174+
this.editor = monaco.editor.create(editorElement, defaultOptions);
175+
176+
// Set up change listener
177+
this.editor.onDidChangeModelContent(() => {
178+
const value = this.editor?.getValue() || '';
179+
this.currentValue.set(value);
180+
this.onChange(value);
181+
this.onTouched();
182+
});
183+
184+
// Set height
185+
editorElement.style.height = this.height;
186+
this.editor.layout();
187+
}
188+
189+
// ControlValueAccessor implementation
190+
writeValue(value: string): void {
191+
this.currentValue.set(value || '');
192+
}
193+
194+
registerOnChange(fn: (value: string) => void): void {
195+
this.onChange = fn;
196+
}
197+
198+
registerOnTouched(fn: () => void): void {
199+
this.onTouched = fn;
200+
}
201+
202+
setDisabledState(isDisabled: boolean): void {
203+
if (this.editor) {
204+
this.editor.updateOptions({ readOnly: isDisabled });
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)