summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/editor/source_editor.js
blob: 57e2b0da565732d78f4f2bc99b84b4745298d340 (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
import { editor as monacoEditor, Uri } from 'monaco-editor';
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import languages from '~/ide/lib/languages';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { uuids } from '~/lib/utils/uuids';
import {
  SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
  URI_PREFIX,
  EDITOR_READY_EVENT,
  EDITOR_TYPE_DIFF,
} from './constants';
import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils';
import EditorInstance from './source_editor_instance';

const instanceRemoveFromRegistry = (editor, instance) => {
  const index = editor.instances.findIndex((inst) => inst === instance);
  editor.instances.splice(index, 1);
};

const instanceDisposeModels = (editor, instance, model) => {
  const instanceModel = instance.getModel() || model;
  if (!instanceModel) {
    return;
  }
  if (instance.getEditorType() === EDITOR_TYPE_DIFF) {
    const { original, modified } = instanceModel;
    if (original) {
      original.dispose();
    }
    if (modified) {
      modified.dispose();
    }
  } else {
    instanceModel.dispose();
  }
};

export default class SourceEditor {
  /**
   * Constructs a global editor.
   * @param {Object} options - Monaco config options used to create the editor
   */
  constructor(options = {}) {
    this.instances = [];
    this.extensionsStore = new Map();
    this.options = {
      extraEditorClassName: 'gl-source-editor',
      ...defaultEditorOptions,
      ...options,
    };

    setupEditorTheme();

    registerLanguages(...languages);
  }

  static prepareInstance(el) {
    if (!el) {
      throw new Error(SOURCE_EDITOR_INSTANCE_ERROR_NO_EL);
    }

    clearDomElement(el);

    monacoEditor.onDidCreateEditor(() => {
      delete el.dataset.editorLoading;
    });
  }

  static createEditorModel({
    blobPath,
    blobContent,
    blobOriginalContent,
    blobGlobalId,
    instance,
    isDiff,
  } = {}) {
    if (!instance) {
      return null;
    }
    const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
    const uri = Uri.file(uriFilePath);
    const existingModel = monacoEditor.getModel(uri);
    const model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
    if (!isDiff) {
      instance.setModel(model);
      return model;
    }
    const diffModel = {
      original: monacoEditor.createModel(blobOriginalContent, getBlobLanguage(model.uri.path)),
      modified: model,
    };
    instance.setModel(diffModel);
    return diffModel;
  }

  /**
   * Creates a Source Editor Instance with the given options.
   * @param {Object} options Options used to initialize the instance.
   * @param {Element} options.el The element to attach the instance for.
   * @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language.
   * @param {string} options.blobContent The content to initialize the monacoEditor.
   * @param {string} options.blobOriginalContent The original blob's content. Is used when creating a Diff Instance.
   * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
   * @param {Boolean} options.isDiff Flag to enable creation of a Diff Instance?
   * @param {...*} options.instanceOptions Configuration options used to instantiate an instance.
   * @returns {EditorInstance}
   */
  createInstance({
    el = undefined,
    blobPath = '',
    blobContent = '',
    blobOriginalContent = '',
    blobGlobalId = uuids()[0],
    isDiff = false,
    ...instanceOptions
  } = {}) {
    SourceEditor.prepareInstance(el);

    const createEditorFn = isDiff ? 'createDiffEditor' : 'create';
    const instance = new EditorInstance(
      monacoEditor[createEditorFn].call(this, el, {
        ...this.options,
        ...instanceOptions,
      }),
      this.extensionsStore,
    );

    waitForCSSLoaded(() => {
      instance.layout();
    });

    let model;
    if (instanceOptions.model !== null) {
      model = SourceEditor.createEditorModel({
        blobGlobalId,
        blobOriginalContent,
        blobPath,
        blobContent,
        instance,
        isDiff,
      });
    }

    instance.onDidDispose(() => {
      instanceRemoveFromRegistry(this, instance);
      instanceDisposeModels(this, instance, model);
    });

    this.instances.push(instance);
    el.dispatchEvent(new CustomEvent(EDITOR_READY_EVENT, { instance }));
    return instance;
  }

  /**
   * Create a Diff Instance
   * @param {Object} args Options to be passed further down to createInstance() with the same signature
   * @returns {EditorInstance}
   */
  createDiffInstance(args) {
    return this.createInstance({
      ...args,
      isDiff: true,
    });
  }

  /**
   * Dispose global editor
   * Automatically disposes all the instances registered for this editor
   */
  dispose() {
    this.instances.forEach((instance) => instance.dispose());
  }
}