The Monaco editor is a code editor developed by Microsoft on which vscode, as we know it, is based. That said, everything we can do in vscode theoretically you can do with the Monaco editor.

This time we will take a closer look at how to use the Monaco Editor to make a beautiful, easy-to-use code editor for your pages. The following will be introduced in this article:

  • Integrate within the projectmonaco editor, this paper will be based onAngularTo practice;
  • Jump between parameter definitions and references;
  • Introducing dependency auto-completion;
  • Add a custom area below the line of code.

There are more features like:

  • integrationprettierFormat
  • integrationeslintCarry on the specification prompt
  • The customQuick fix
  • .

If you’re interested, leave a comment in the comments section and we’ll talk about it next time

Integrate the Monaco Editor into the project

1.1 New Project

# We created a multi-application project to facilitate the writing of our pages and components
ng new try-monaco-editor --createApplication=false
# Create an app to show the effect
ng g application website
Create a library for component authoring
ng g library tools
Copy the code

1.2 Installation-related Dependencies

You may want to read the official guidance for more details.

Integrated AMD version

Integrate the ESM version

npm i monaco-editor
Copy the code

1.3 change presents. Json

{... ."projects": {
    "website": {... ."architect": {
        "build": {
          "options": {
            "assets": [{"glob": "* * / *"."input": "node_modules/monaco-editor/min/vs"."output": "/assets/vs/"},]}}}}}}Copy the code

1.4 Write a Service to load the Monaco script

cd projects/website
ng g s services/code-editor
Copy the code

Complete the code-editor.service.ts loading script

// code-editor.service.ts
import { Inject, Injectable, InjectionToken, Optional } from '@angular/core';
import { AsyncSubject, Subject } from 'rxjs';

// You may need to change the resource path depending on the deployment at use
export const APP_MONACO_BASE_HREF = new InjectionToken<string> ('appMonacoBaseHref'
);

@Injectable({
  providedIn: 'root'
})
export class CodeEditorService {
  private afterScriptLoad$: AsyncSubject<boolean> = new AsyncSubject<boolean> ();private isScriptLoaded = false;

  constructor(@Optional(a)@Inject(APP_MONACO_BASE_HREF) private base: string) {
    this.loadMonacoScript();
  }

  public getScriptLoadSubject(): AsyncSubject<boolean> {
    return this.afterScriptLoad$;
  }

  public loadMonacoScript(): void {
    // Load the Monaco script from AMD
    const onGotAmdLoader: any = () = > {
      // load monaco here
      (<any>window).require.config({
        paths: { vs: `The ${this.base || 'assets/vs'}`}}); (<any>window).require(['vs/editor/editor.main'].() = > {
        this.isScriptLoaded = true;
        this.afterScriptLoad$.next(true);
        this.afterScriptLoad$.complete();
      });
    };

    // Here you will need to load loader.js of Monaco
    if(! (<any>window).require) {
      const loaderScript = document.createElement('script');
      loaderScript.type = 'text/javascript';
      loaderScript.src = `The ${this.base || 'assets/vs'}/loader.js`;
      loaderScript.addEventListener('load', onGotAmdLoader);
      document.body.appendChild(loaderScript);
    } else{ onGotAmdLoader(); }}}Copy the code

Set the base href in app.module.ts

// app.module.ts.@NgModule({
  providers: [{ provide: APP_MONACO_BASE_HREF, useValue: 'assets/vs'}],})export class AppModule {}
Copy the code

Now that we’re done with our pre-work, let’s test that we’ve successfully loaded Monaco.

// app.component.ts
export class AppComponent implements AfterViewInit {
  private destroy$: Subject<void> = new Subject<void> ();constructor(private codeEditorService: CodeEditorService) {}

  ngAfterViewInit(): void {
    this.codeEditorService
      .getScriptLoadSubject()
      .pipe(takeUntil(this.destroy$))
      .subscribe((isLoaded) = > {
        if (isLoaded) {
          // All subsequent operations we initialize should be done after Monaco has successfully loaded
          console.log('load success'); }}); }}Copy the code

After the modification, open the console window in the browser and type Monaco. The image below shows that it has been successfully loaded

Now we can begin to encapsulate it as a component.

2 Editor component writing

cd projects/tools/src/lib
ng g m editor --flat
ng g c editor --flat
Copy the code

SCSS, editor.component.ts, editor.component.ts, Editor.module. ts (remember to modify the file export in public-api.ts), we just need to modify editor.component.ts

// editor.component.ts
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  EventEmitter,
  forwardRef,
  HostBinding,
  Input,
  NgZone,
  OnDestroy,
  Output,
  Renderer2,
  ViewChild
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { CodeEditorService } from 'projects/website/services/code-editor.service';
import { fromEvent, Subject, takeUntil } from 'rxjs';

declare const monaco: any;
@Component({
  selector: 'app-editor'.template: ` <div #editor class="my-editor"></div> `.styleUrls: ['./editor.component.scss'].changeDetection: ChangeDetectionStrategy.OnPush,
  // We will use bidirectional binding to pass values to the editor. Note the introduction of NG_VALUE_ACCESSOR, ControlValueAccessor
  providers: [{provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() = > EditorComponent),
      multi: true}]})export class EditorComponent
  implements AfterViewInit.OnDestroy.ControlValueAccessor
{
  // Here detailed options can be viewed https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html
  @Input() options: any;
  // Set the height
  @Input(a)@HostBinding('style.height') height: string;
  // The editor instance is thrown after initialization. Users can use the editor instance to do some personalization
  @Output(a)readonly editorInitialized: EventEmitter<any> =
    new EventEmitter<any> ();@ViewChild('editor', { static: true }) editorContentRef: ElementRef;

  private destroy$: Subject<void> = new Subject<void> ();private _editor: any = undefined;
  private _value: string = ' ';
  // As many of Monaco's methods return IDisposable, please dispose of your components when they are destroyed by executing the Dispose () method
  private _disposables: any[] = [];

  onChange = (_ :any) = > {};
  onTouched = () = > {};

  constructor(
    private zone: NgZone,
    private codeEditorService: CodeEditorService,
    private renderer: Renderer2
  ) {}

  // Bidirectional binding sets the editor content
  writeValue(value: string) :void {
    this._value = value || ' ';
    this.setValue();
  }

  // Pass the changed value through the onChange method, ngModelChange
  registerOnChange(fn: any) :void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any) :void {
    this.onTouched = fn;
  }

  ngAfterViewInit(): void {
    this.codeEditorService
      .getScriptLoadSubject()
      .pipe(takeUntil(this.destroy$))
      .subscribe((isLoaded) = > {
        if (isLoaded) {
          this.initMonaco(); }});// Monitor the size of the browser window and do the editor adaptation
    fromEvent(window.'resize')
      .pipe(takeUntil(this.destroy$))
      .subscribe(() = > {
        if (this._editor) {
          this._editor.layout(); }}); }private initMonaco(): void {
    const options = this.options;
    const language = this.options['language'];
    const editorDiv: HTMLDivElement = this.editorContentRef.nativeElement;

    if (!this._editor) {
      this._editor = monaco.editor.create(editorDiv, options);
      this._editor.setModel(monaco.editor.createModel(this._value, language));
      this.editorInitialized.emit(this._editor);
      this.renderer.setStyle(
        this.editorContentRef.nativeElement,
        'height'.this.height
      );
      this.setValueEmitter();
      this._editor.layout(); }}private setValue(): void {
    if (!this._editor || !this._editor.getModel()) {
      return;
    }
    this._editor.getModel().setValue(this._value);
  }

  private setValueEmitter() {
    if (this._editor) {
      const model = this._editor.getModel();
      // The onDidChangeContent method will be triggered when the content changes, at which point value will be thrown, adding its return value to _Disposables
      this._disposables.push(
        model.onDidChangeContent(() = > {
          this.zone.run(() = > {
            this.onChange(model.getValue());
            this._value = model.getValue(); }); })); } } ngOnDestroy():void {
    // The subscription and instance are cleared when the component is destroyed
    this.destroy$.next();
    this.destroy$.complete();
    if (this._editor) {
      this._editor.dispose();
      this._editor = undefined;
    }
    // In Monaco, many methods return an IDisposable that requires its dispose() method at the time of destruction
    if (this._disposables.length) {
      this._disposables.forEach((disposable) = > disposable.dispose());
      this._disposables = []; }}}Copy the code

Now let’s test the effect by introducing the EditorModule in app.module.ts and using < app-Editor > in app.component.html.

<div class="app">
  <app-editor
    [options] ="{language: 'typescript'}"
    [height] ="'300px'"
    [ngModel] ="'export class Test {}'"
  ></app-editor>
</div>
Copy the code

Then you can see the following effect on the page

So far we have completed a simple editor component, and then you can add a variety of APIS, options and other configurations according to your preferences. For convenience, we will implement the Monaco Editor Play Ground directly (what is implemented here can also be implemented in the component).

3 Monaco Editor PlayGround

PlayGround

3.1 Jump between Parameter Definition and reference

The following code, which can be run directly on PlayGround, does a few things:

  • Multiple models were created so that they could be correlated, allowing us to see references and definitions between variables
var code3 = `let d = a + 5; \nlet mmm = a + 7; \nlet sum = 0; \nfor (let i = 0; i < 10; i ++) {\n\tsum += i; \n}; `;

var models = [
  {
    code: 'let a = 1; \nlet b = 2; '.language: 'typescript'.uri: 'file://root/file1.ts'
  },
  {
    code: 'let c = a + 3; \nlet mm = a - 6; '.language: 'typescript'.uri: 'file://root/file2.ts'
  },
  {
    code: code3,
    language: 'typescript'.uri: 'file://root/file3.ts'}];var myModel;

models.forEach((model) = > {
  myModel = monaco.editor.createModel(
    model.code,
    model.language,
    monaco.Uri.parse(model.uri)
  );
});

var editor = monaco.editor.create(document.getElementById('container'), {
  value: ' '.language: 'typescript'
});

editor.setModel(myModel);
Copy the code

So how do we jump from the current file to the referenced file? Let’s look at the following code, which can be added directly to the code above

var editorService = editor._codeEditorService;
var openEditorBase = editorService.openCodeEditor.bind(editorService);
editorService.openCodeEditor = async (input, source) => {
  const result = await openEditorBase(input, source);
  if (result === null) {
    const currentModel = monaco.editor.getModel(input.resource);
    const range = {
      startLineNumber: input.options.selection.startLineNumber,
      endLineNumber: input.options.selection.endLineNumber,
      startColumn: input.options.selection.startColumn,
      endColumn: input.options.selection.endColumn
    };
    editor.setModel(currentModel);
    editor.revealRangeInCenterIfOutsideViewport(range);
    editor.setPosition({
      lineNumber: input.options.selection.startLineNumber,
      column: input.options.selection.startColumn
    });
  }
  return result; // always return the base result
};
Copy the code

To jump, double-click the reference in file2. Ts. (The same goes for the jump to Definition, just click on Goto Definition.)

How do I highlight related reference variables after I jump files

/ / in editorService openCodeEditor added
editorService.openCodeEditor = async (input, source) => {
  ...
  editor.setPosition({
      lineNumber: input.options.selection.startLineNumber,
      column: input.options.selection.startColumn,
  });
  // Add a background color to the class myInlineDecoration in the CSS file and it will disappear after 1s
  const decorations = editor.deltaDecorations(
    [],
    [
      {
        range,
        options: { inlineClassName: 'myInlineDecoration'}},]);setTimeout(() = > {
    editor.deltaDecorations(decorations, []);
  }, 1000); . }Copy the code

The steps of the jump are the same as described above. Let’s look at the effect after the jump. We can see that the variable A corresponding to the jump is highlighted

3.2 Introducing automatic prompts for dependencies

Sometimes we introduce a third party library, and we may need to be able to retrieve its corresponding methods. Similarly, the following code can be run directly on PlayGround

  • The key point here is that we’re going to settypescriptCompiling and configuring
// This step is necessary, otherwise the import will not take effectmonaco.languages.typescript.typescriptDefaults.setCompilerOptions({ ... monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs
});

var models = [
  {
    code: `import * as t from 'test'; \n\nt.X; \nt.Y; `.language: 'typescript'.uri: 'file://root/file1.ts'}];var denpendencies = [
  {
    code: 'export const X = 1; \nexport const Y = 2; '.language: 'typescript'.uri: 'file://root/node_modules/test/index.d.ts'}];var myModel = monaco.editor.createModel(
  models[0].code,
  models[0].language,
  monaco.Uri.parse(models[0].uri)
);

denpendencies.forEach((denpendency) = > {
  monaco.editor.createModel(
    denpendency.code,
    denpendency.language,
    monaco.Uri.parse(denpendency.uri)
  );
});

var editor = monaco.editor.create(document.getElementById('container'), {
  value: ' '.language: 'typescript'
});

editor.setModel(myModel);
Copy the code

3.3 Adding custom viewZone and overlayWidget

OverlayWidget (viewZone, overlayWidget, contentWidget) overlayWidget (viewZone, overlayWidget, contentWidget

  • So the first thing I need to do is useonDomNodeToponComputedHeightTwo methods, these two methods are the key that we use for positioningoverlayWidgetSo that it can be well locatedviewZoneLet’s take a look at the code first
var jsCode = [
  '"use strict"; '.'function Person(age) {'.'	if (age) {'.'		this.age = age;'.'	}'.'} '.'Person.prototype.getAge = function () {'.' return this.age; '.'}; '
].join('\n');

var editor = monaco.editor.create(document.getElementById('container'), {
  value: jsCode,
  language: 'javascript'.glyphMargin: true.contextmenu: false
});

var overlayDom = document.createElement('div');
overlayDom.innerHTML = 'My overlay widget';
overlayDom.style.background = 'lightgreen';
overlayDom.style.top = '50px';
overlayDom.style.width = '100%';

var viewZoneId = null;
editor.changeViewZones(function (changeAccessor) {
  var domNode = document.createElement('div');
  viewZoneId = changeAccessor.addZone({
    afterLineNumber: 3.heightInPx: 100.domNode: domNode,
    onDomNodeTop: (top) = > {
      overlayDom.style.top = `${top}px`;
    },
    onComputedHeight: (height) = > {
      overlayDom.style.height = `${height}px`; }}); });// Add an overlay widget
var overlayWidget = {
  getId: () = > {
    return 'my.overlay.widget';
  },
  getDomNode: () = > {
    return overlayDom;
  },
  getPosition: function () {
    // Return NULL is required here because we will use viewZone to locate
    return null; }}; editor.addOverlayWidget(overlayWidget);Copy the code

In the figure, you can see that the green area spills over the scroll bar, which we don’t want, so we also need to calculate the width of the area, just add the code below

var editorLayoutInfo = editor.getLayoutInfo();
var domWidth = editorLayoutInfo.width - editorLayoutInfo.minimap.minimapWidth;
overlayDom.style.width = `${domWidth}px`;
Copy the code

Finally, again, the key point is that we need to use onDomNodeTop and onComputedHeight in addZone to perfectly locate our overlayWidget.

So why use the overlayWidget instead of using domNode directly in viewZone? In the official specification, viewZone’s purpose is to spread out an area, not to let you add a complex dom element to it, which is why it provides onDomNodeTop and onComputedHeight methods, included in vscode, The prompt box for defining references (shown below) is also implemented using this method.

These two methods can be used to locate not only the overlayWidget but also the contentWidget. Using viewZone directly can also be problematic:

  • The area below the line number isviewZoneTo reach the
  • ifdomElements with scroll bars cannot scroll

4 the last

Monaco API

The Monaco Editor provides many apis for developers to use. Check out the documentation and the demo. There are many more capabilities to discover!