summaryrefslogtreecommitdiff
path: root/doc/development/fe_guide/source_editor.md
blob: b06e341630f2cfab8bfe3207ea94e6572bc68e81 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
---
stage: Create
group: Editor
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

# Source Editor **(FREE)**

**Source Editor** provides the editing experience at GitLab. This thin wrapper around
[the Monaco editor](https://microsoft.github.io/monaco-editor/) provides necessary
helpers and abstractions, and extends Monaco [using extensions](#extensions). Multiple
GitLab features use it, including:

- [Web IDE](../../user/project/web_ide/index.md)
- [CI Linter](../../ci/lint.md)
- [Snippets](../../user/snippets.md)
- [Web Editor](../../user/project/repository/web_editor.md)
- [Security Policies](../../user/application_security/policies/index.md)

## How to use Source Editor

Source Editor is framework-agnostic and can be used in any application, including both
Rails and Vue. To help with integration, we have the dedicated `<source-editor>`
Vue component, but the integration of Source Editor is generally straightforward:

1. Import Source Editor:

   ```javascript
   import SourceEditor from '~/editor/source_editor';
   ```

1. Initialize global editor for the view:

   ```javascript
   const editor = new SourceEditor({
     // Editor Options.
     // The list of all accepted options can be found at
     // https://microsoft.github.io/monaco-editor/api/enums/monaco.editor.EditorOption.html
   });
   ```

1. Create an editor's instance:

   ```javascript
   editor.createInstance({
     // Source Editor configuration options.
   })
   ```

An instance of Source Editor accepts the following configuration options:

| Option | Required? | Description |
| -------------- | ------- | ---- |
| `el`           | `true`  | `HTML Node`: The element on which to render the editor. |
| `blobPath`     | `false` | `String`: The name of a file to render in the editor, used to identify the correct syntax highlighter to use with that file, or another file type. Can accept wildcards like `*.js` when the actual filename isn't known or doesn't play any role. |
| `blobContent`  | `false` | `String`: The initial content to render in the editor. |
| `extensions`   | `false` | `Array`: Extensions to use in this instance. |
| `blobGlobalId` | `false` | `String`: An auto-generated property.<br>**Note:** This property may go away in the future. Do not pass `blobGlobalId` unless you know what you're doing.|
| Editor Options | `false` | `Object(s)`: Any property outside of the list above is treated as an Editor Option for this particular instance. Use this field to override global Editor Options on the instance level. A full [index of Editor Options](https://microsoft.github.io/monaco-editor/api/enums/monaco.editor.EditorOption.html) is available. |

## API

The editor uses the same public API as
[provided by Monaco editor](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneCodeEditor.html)
with additional functions on the instance level:

| Function              | Arguments | Description
| --------------------- | ----- | ----- |
| `updateModelLanguage` | `path`: String | Updates the instance's syntax highlighting to follow the extension of the passed `path`. Available only on the instance level.|
| `use`                 | Array of objects | Array of extensions to apply to the instance. Accepts only the array of _objects_. You must fetch the extensions' ES6 modules must be fetched and resolved in your views or components before they are passed to `use`. This property is available on _instance_ (applies extension to this particular instance) and _global editor_ (applies the same extension to all instances) levels. |
| Monaco Editor options | See [documentation](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneCodeEditor.html) | Default Monaco editor options |

## Tips

1. Editor's loading state.

   The loading state is built in to Source Editor, making spinners and loaders
   rarely needed in HTML. To benefit the built-in loading state, set the `data-editor-loading`
   property on the HTML element that should contain the editor. When bootstrapping,
   Source Editor shows the loader automatically.

   ![Source Editor: loading state](img/editor_lite_loading.png)

1. Update syntax highlighting if the filename changes.

   ```javascript
   // fileNameEl here is the HTML input element that contains the file name
   fileNameEl.addEventListener('change', () => {
     this.editor.updateModelLanguage(fileNameEl.value);
   });
   ```

1. Get the editor's content.

   We may set up listeners on the editor for every change, but it rapidly can become
   an expensive operation. Instead, get the editor's content when it's needed.
   For example, on a form's submission:

   ```javascript
   form.addEventListener('submit', () => {
     my_content_variable = this.editor.getValue();
   });
   ```

1. Performance

   Even though Source Editor itself is extremely slim, it still depends on Monaco editor,
   which adds weight. Every time you add Source Editor to a view, the JavaScript bundle's
   size significantly increases, affecting your view's loading performance. We recommend
   you import the editor on demand if either:

   - You're uncertain if the view needs the editor.
   - The editor is a secondary element of the view.

   Loading Source Editor on demand is handled like loading any other module:

   ```javascript
   someActionFunction() {
     import(/* webpackChunkName: 'SourceEditor' */ '~/editor/source_editor').
       then(({ default: SourceEditor }) => {
         const editor = new SourceEditor();
         ...
       });
     ...
   }
   ```

## Extensions

Source Editor provides a universal, extensible editing tool to the whole product,
and doesn't depend on any particular group. Even though the Source Editor's core is owned by
[Create::Editor FE Team](https://about.gitlab.com/handbook/engineering/development/dev/create-editor/),
any group can own the extensions—the main functional elements. The goal of
Source Editor extensions is to keep the editor's core slim and stable. Any
needed features can be added as extensions to this core. Any group can
build and own new editing features without worrying about changes to Source Editor
breaking or overriding them.

You can depend on other modules in your extensions. This organization helps keep
the size of Source Editor's core at bay by importing dependencies only when needed.

Structurally, the complete implementation of Source Editor can be presented as this diagram:

```mermaid
graph TD;
    B[Extension 1]---A[Source Editor]
    C[Extension 2]---A[Source Editor]
    D[Extension 3]---A[Source Editor]
    E[...]---A[Source Editor]
    F[Extension N]---A[Source Editor]
    A[Source Editor]---Z[Monaco]
```

An extension is an ES6 module that exports a JavaScript object:

```javascript
import { Position } from 'monaco-editor';

export default {
  navigateFileStart() {
    this.setPosition(new Position(1, 1));
  },
};

```

In the extension's functions, `this` refers to the current Source Editor instance.
Using `this`, you get access to the complete instance's API, such as the
`setPosition()` method in this particular case.

### Using an existing extension

Adding an extension to Source Editor's instance requires the following steps:

```javascript
import SourceEditor from '~/editor/source_editor';
import MyExtension from '~/my_extension';

const editor = new SourceEditor().createInstance({
  ...
});
editor.use(MyExtension);
```

### Creating an extension

Let's create our first Source Editor extension. Extensions are
[ES6 modules](https://hacks.mozilla.org/2015/08/es6-in-depth-modules/) exporting a
basic `Object`, used to extend Source Editor's features. As a test, let's
create an extension that extends Source Editor with a new function that, when called,
outputs the editor's content in `alert`.

`~/my_folder/my_fancy_extension.js:`

```javascript
export default {
  throwContentAtMe() {
    alert(this.getValue());
  },
};
```

In the code example, `this` refers to the instance. By referring to the instance,
we can access the complete underlying
[Monaco editor API](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneCodeEditor.html),
which includes functions like `getValue()`.

Now let's use our extension:

`~/my_folder/component_bundle.js`:

```javascript
import SourceEditor from '~/editor/source_editor';
import MyFancyExtension from './my_fancy_extension';

const editor = new SourceEditor().createInstance({
  ...
});
editor.use(MyFancyExtension);
...
someButton.addEventListener('click', () => {
  editor.throwContentAtMe();
});
```

First of all, we import Source Editor and our new extension. Then we create the
editor and its instance. By default Source Editor has no `throwContentAtMe` method.
But the `editor.use(MyFancyExtension)` line brings that method to our instance.
After that, we can use it any time we need it. In this case, we call it when some
theoretical button has been clicked.

This script would result in an alert containing the editor's content when `someButton` is clicked.

![Source Editor new extension's result](img/editor_lite_create_ext.png)

### Tips

1. Performance

   Just like Source Editor itself, any extension can be loaded on demand to not harm
   loading performance of the views:

   ```javascript
   const EditorPromise = import(
     /* webpackChunkName: 'SourceEditor' */ '~/editor/source_editor'
   );
   const MarkdownExtensionPromise = import('~/editor/source_editor_markdown_ext');

   Promise.all([EditorPromise, MarkdownExtensionPromise])
     .then(([{ default: SourceEditor }, { default: MarkdownExtension }]) => {
       const editor = new SourceEditor().createInstance({
         ...
       });
       editor.use(MarkdownExtension);
     });
   ```

1. Using multiple extensions

   Just pass the array of extensions to your `use` method:

   ```javascript
   editor.use([FileTemplateExtension, MyFancyExtension]);
   ```