Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
021bb329
Commit
021bb329
authored
Dec 15, 2021
by
Denys Mishunov
Committed by
David O'Regan
Dec 15, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Source Editor refactoring integration
parent
783ce2d8
Changes
27
Show whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
1060 additions
and
1148 deletions
+1060
-1148
app/assets/javascripts/blob_edit/edit_blob.js
app/assets/javascripts/blob_edit/edit_blob.js
+25
-18
app/assets/javascripts/editor/constants.js
app/assets/javascripts/editor/constants.js
+4
-0
app/assets/javascripts/editor/extensions/example_source_editor_extension.js
...ipts/editor/extensions/example_source_editor_extension.js
+10
-0
app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
...ascripts/editor/extensions/source_editor_ci_schema_ext.js
+21
-26
app/assets/javascripts/editor/extensions/source_editor_extension_base.js
...scripts/editor/extensions/source_editor_extension_base.js
+61
-50
app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js
...ipts/editor/extensions/source_editor_file_template_ext.js
+12
-4
app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
...vascripts/editor/extensions/source_editor_markdown_ext.js
+92
-87
app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
...itor/extensions/source_editor_markdown_livepreview_ext.js
+67
-54
app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
...javascripts/editor/extensions/source_editor_webide_ext.js
+147
-125
app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
...s/javascripts/editor/extensions/source_editor_yaml_ext.js
+147
-132
app/assets/javascripts/editor/source_editor.js
app/assets/javascripts/editor/source_editor.js
+54
-77
app/assets/javascripts/editor/source_editor_extension.js
app/assets/javascripts/editor/source_editor_extension.js
+1
-1
app/assets/javascripts/editor/source_editor_instance.js
app/assets/javascripts/editor/source_editor_instance.js
+39
-33
app/assets/javascripts/ide/components/repo_editor.vue
app/assets/javascripts/ide/components/repo_editor.vue
+21
-18
app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
...scripts/pipeline_editor/components/editor/text_editor.vue
+1
-1
spec/frontend/blob_edit/edit_blob_spec.js
spec/frontend/blob_edit/edit_blob_spec.js
+24
-11
spec/frontend/editor/helpers.js
spec/frontend/editor/helpers.js
+48
-24
spec/frontend/editor/source_editor_ci_schema_ext_spec.js
spec/frontend/editor/source_editor_ci_schema_ext_spec.js
+1
-1
spec/frontend/editor/source_editor_extension_base_spec.js
spec/frontend/editor/source_editor_extension_base_spec.js
+53
-108
spec/frontend/editor/source_editor_extension_spec.js
spec/frontend/editor/source_editor_extension_spec.js
+1
-1
spec/frontend/editor/source_editor_instance_spec.js
spec/frontend/editor/source_editor_instance_spec.js
+19
-6
spec/frontend/editor/source_editor_markdown_ext_spec.js
spec/frontend/editor/source_editor_markdown_ext_spec.js
+1
-4
spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
...end/editor/source_editor_markdown_livepreview_ext_spec.js
+63
-40
spec/frontend/editor/source_editor_spec.js
spec/frontend/editor/source_editor_spec.js
+64
-245
spec/frontend/editor/source_editor_yaml_ext_spec.js
spec/frontend/editor/source_editor_yaml_ext_spec.js
+48
-28
spec/frontend/ide/components/repo_editor_spec.js
spec/frontend/ide/components/repo_editor_spec.js
+36
-49
spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
...end/pipeline_editor/components/editor/text_editor_spec.js
+0
-5
No files found.
app/assets/javascripts/blob_edit/edit_blob.js
View file @
021bb329
import
$
from
'
jquery
'
;
import
$
from
'
jquery
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
FileTemplateExtension
}
from
'
~/editor/extensions/source_editor_file_template_ext
'
;
import
{
FileTemplateExtension
}
from
'
~/editor/extensions/source_editor_file_template_ext
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
{
getBlobLanguage
}
from
'
~/editor/utils
'
;
import
{
getBlobLanguage
}
from
'
~/editor/utils
'
;
...
@@ -26,23 +27,29 @@ export default class EditBlob {
...
@@ -26,23 +27,29 @@ export default class EditBlob {
this
.
editor
.
focus
();
this
.
editor
.
focus
();
}
}
fetchMarkdownExtension
()
{
async
fetchMarkdownExtension
()
{
import
(
'
~/editor/extensions/source_editor_markdown_ext
'
)
try
{
.
then
(({
EditorMarkdownExtension
:
MarkdownExtension
}
=
{})
=>
{
const
[
this
.
editor
.
use
(
{
EditorMarkdownExtension
:
MarkdownExtension
},
new
MarkdownExtension
({
{
EditorMarkdownPreviewExtension
:
MarkdownLivePreview
},
instance
:
this
.
editor
,
]
=
await
Promise
.
all
([
previewMarkdownPath
:
this
.
options
.
previewMarkdownPath
,
import
(
'
~/editor/extensions/source_editor_markdown_ext
'
),
}),
import
(
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
),
);
]);
this
.
hasMarkdownExtension
=
true
;
this
.
editor
.
use
([
addEditorMarkdownListeners
(
this
.
editor
);
{
definition
:
MarkdownExtension
},
})
{
.
catch
((
e
)
=>
definition
:
MarkdownLivePreview
,
setupOptions
:
{
previewMarkdownPath
:
this
.
options
.
previewMarkdownPath
},
},
]);
}
catch
(
e
)
{
createFlash
({
createFlash
({
message
:
`
${
BLOB_EDITOR_ERROR
}
:
${
e
}
`
,
message
:
`
${
BLOB_EDITOR_ERROR
}
:
${
e
}
`
,
}),
});
);
}
this
.
hasMarkdownExtension
=
true
;
addEditorMarkdownListeners
(
this
.
editor
);
}
}
configureMonacoEditor
()
{
configureMonacoEditor
()
{
...
@@ -60,7 +67,7 @@ export default class EditBlob {
...
@@ -60,7 +67,7 @@ export default class EditBlob {
blobPath
:
fileNameEl
.
value
,
blobPath
:
fileNameEl
.
value
,
blobContent
:
editorEl
.
innerText
,
blobContent
:
editorEl
.
innerText
,
});
});
this
.
editor
.
use
(
new
FileTemplateExtension
({
instance
:
this
.
editor
})
);
this
.
editor
.
use
(
[{
definition
:
SourceEditorExtension
},
{
definition
:
FileTemplateExtension
}]
);
fileNameEl
.
addEventListener
(
'
change
'
,
()
=>
{
fileNameEl
.
addEventListener
(
'
change
'
,
()
=>
{
this
.
editor
.
updateModelLanguage
(
fileNameEl
.
value
);
this
.
editor
.
updateModelLanguage
(
fileNameEl
.
value
);
...
...
app/assets/javascripts/editor/constants.js
View file @
021bb329
...
@@ -42,6 +42,10 @@ export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
...
@@ -42,6 +42,10 @@ export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
// EXTENSIONS' CONSTANTS
// EXTENSIONS' CONSTANTS
//
//
// Source Editor Base Extension
export
const
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
=
'
link-anchor
'
;
export
const
EXTENSION_BASE_LINE_NUMBERS_CLASS
=
'
line-numbers
'
;
// For CI config schemas the filename must match
// For CI config schemas the filename must match
// '*.gitlab-ci.yml' regardless of project configuration.
// '*.gitlab-ci.yml' regardless of project configuration.
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
...
...
app/assets/javascripts/editor/extensions/example_source_editor_extension.js
View file @
021bb329
...
@@ -6,6 +6,16 @@
...
@@ -6,6 +6,16 @@
//
//
export
class
MyFancyExtension
{
export
class
MyFancyExtension
{
/**
* A required getter returning the extension's name
* We have to provide it for every extension instead of relying on the built-in
* `name` prop because the prop does not survive the webpack's minification
* and the name mangling.
* @returns {string}
*/
static
get
extensionName
()
{
return
'
MyFancyExtension
'
;
}
/**
/**
* THE LIFE-CYCLE CALLBACKS
* THE LIFE-CYCLE CALLBACKS
*/
*/
...
...
app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
View file @
021bb329
import
ciSchemaPath
from
'
~/editor/schema/ci.json
'
;
import
ciSchemaPath
from
'
~/editor/schema/ci.json
'
;
import
{
registerSchema
}
from
'
~/ide/utils
'
;
import
{
registerSchema
}
from
'
~/ide/utils
'
;
import
{
SourceEditorExtension
}
from
'
./source_editor_extension_base
'
;
export
class
CiSchemaExtension
extends
SourceEditorExtension
{
export
class
CiSchemaExtension
{
/**
static
get
extensionName
()
{
* Registers a syntax schema to the editor based on project
return
'
CiSchema
'
;
* identifier and commit.
}
*
// eslint-disable-next-line class-methods-use-this
* The schema is added to the file that is currently edited
provides
()
{
* in the editor.
return
{
*
registerCiSchema
:
(
instance
)
=>
{
* @param {Object} opts
* @param {String} opts.projectNamespace
* @param {String} opts.projectPath
* @param {String?} opts.ref - Current ref. Defaults to main
*/
registerCiSchema
()
{
// In order for workers loaded from `data://` as the
// In order for workers loaded from `data://` as the
// ones loaded by monaco editor, we use absolute URLs
// ones loaded by monaco editor, we use absolute URLs
// to fetch schema files, hence the `gon.gitlab_url`
// to fetch schema files, hence the `gon.gitlab_url`
// reference. This prevents error:
// reference. This prevents error:
// "Failed to execute 'fetch' on 'WorkerGlobalScope'"
// "Failed to execute 'fetch' on 'WorkerGlobalScope'"
const
absoluteSchemaUrl
=
gon
.
gitlab_url
+
ciSchemaPath
;
const
absoluteSchemaUrl
=
gon
.
gitlab_url
+
ciSchemaPath
;
const
modelFileName
=
this
.
getModel
().
uri
.
path
.
split
(
'
/
'
).
pop
();
const
modelFileName
=
instance
.
getModel
().
uri
.
path
.
split
(
'
/
'
).
pop
();
registerSchema
({
registerSchema
({
uri
:
absoluteSchemaUrl
,
uri
:
absoluteSchemaUrl
,
fileMatch
:
[
modelFileName
],
fileMatch
:
[
modelFileName
],
});
});
},
};
}
}
}
}
app/assets/javascripts/editor/extensions/source_editor_extension_base.js
View file @
021bb329
import
{
Range
}
from
'
monaco-editor
'
;
import
{
Range
}
from
'
monaco-editor
'
;
import
{
waitForCSSLoaded
}
from
'
~/helpers/startup_css_helper
'
;
import
{
import
{
ERROR_INSTANCE_REQUIRED_FOR_EXTENSION
,
EDITOR_TYPE_CODE
}
from
'
../constants
'
;
EDITOR_TYPE_CODE
,
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
,
EXTENSION_BASE_LINE_NUMBERS_CLASS
,
}
from
'
../constants
'
;
const
hashRegexp
=
new
RegExp
(
'
#?L
'
,
'
g
'
);
const
hashRegexp
=
new
RegExp
(
'
#?L
'
,
'
g
'
);
const
createAnchor
=
(
href
)
=>
{
const
createAnchor
=
(
href
)
=>
{
const
fragment
=
new
DocumentFragment
();
const
fragment
=
new
DocumentFragment
();
const
el
=
document
.
createElement
(
'
a
'
);
const
el
=
document
.
createElement
(
'
a
'
);
el
.
classList
.
add
(
'
link-anchor
'
);
el
.
classList
.
add
(
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
);
el
.
href
=
href
;
el
.
href
=
href
;
fragment
.
appendChild
(
el
);
fragment
.
appendChild
(
el
);
el
.
addEventListener
(
'
contextmenu
'
,
(
e
)
=>
{
el
.
addEventListener
(
'
contextmenu
'
,
(
e
)
=>
{
...
@@ -17,38 +20,46 @@ const createAnchor = (href) => {
...
@@ -17,38 +20,46 @@ const createAnchor = (href) => {
};
};
export
class
SourceEditorExtension
{
export
class
SourceEditorExtension
{
constructor
({
instance
,
...
options
}
=
{})
{
static
get
extensionName
()
{
if
(
instance
)
{
return
'
BaseExtension
'
;
Object
.
assign
(
instance
,
options
);
}
// eslint-disable-next-line class-methods-use-this
onUse
(
instance
)
{
SourceEditorExtension
.
highlightLines
(
instance
);
SourceEditorExtension
.
highlightLines
(
instance
);
if
(
instance
.
getEditorType
&&
instance
.
getEditorType
()
===
EDITOR_TYPE_CODE
)
{
if
(
instance
.
getEditorType
&&
instance
.
getEditorType
()
===
EDITOR_TYPE_CODE
)
{
SourceEditorExtension
.
setupLineLinking
(
instance
);
SourceEditorExtension
.
setupLineLinking
(
instance
);
}
}
SourceEditorExtension
.
deferRerender
(
instance
);
}
else
if
(
Object
.
entries
(
options
).
length
)
{
throw
new
Error
(
ERROR_INSTANCE_REQUIRED_FOR_EXTENSION
);
}
}
}
static
deferRerender
(
instance
)
{
static
onMouseMoveHandler
(
e
)
{
waitForCSSLoaded
(()
=>
{
const
target
=
e
.
target
.
element
;
instance
.
layout
();
if
(
target
.
classList
.
contains
(
EXTENSION_BASE_LINE_NUMBERS_CLASS
))
{
});
const
lineNum
=
e
.
target
.
position
.
lineNumber
;
const
hrefAttr
=
`#L
${
lineNum
}
`
;
let
lineLink
=
target
.
querySelector
(
'
a
'
);
if
(
!
lineLink
)
{
lineLink
=
createAnchor
(
hrefAttr
);
target
.
appendChild
(
lineLink
);
}
}
}
}
static
removeHighlights
(
instance
)
{
static
setupLineLinking
(
instance
)
{
Object
.
assign
(
instance
,
{
instance
.
onMouseMove
(
SourceEditorExtension
.
onMouseMoveHandler
);
lineDecorations
:
instance
.
deltaDecorations
(
instance
.
lineDecorations
||
[],
[]),
instance
.
onMouseDown
((
e
)
=>
{
const
isCorrectAnchor
=
e
.
target
.
element
.
classList
.
contains
(
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
,
);
if
(
!
isCorrectAnchor
)
{
return
;
}
if
(
instance
.
lineDecorations
)
{
instance
.
deltaDecorations
(
instance
.
lineDecorations
,
[]);
}
});
});
}
}
/**
* Returns a function that can only be invoked once between
* each browser screen repaint.
* @param {Object} instance - The Source Editor instance
* @param {Array} bounds - The [start, end] array with start
* and end coordinates for highlighting
*/
static
highlightLines
(
instance
,
bounds
=
null
)
{
static
highlightLines
(
instance
,
bounds
=
null
)
{
const
[
start
,
end
]
=
const
[
start
,
end
]
=
bounds
&&
Array
.
isArray
(
bounds
)
bounds
&&
Array
.
isArray
(
bounds
)
...
@@ -74,29 +85,29 @@ export class SourceEditorExtension {
...
@@ -74,29 +85,29 @@ export class SourceEditorExtension {
}
}
}
}
static
onMouseMoveHandler
(
e
)
{
// eslint-disable-next-line class-methods-use-this
const
target
=
e
.
target
.
element
;
provides
()
{
if
(
target
.
classList
.
contains
(
'
line-numbers
'
))
{
return
{
const
lineNum
=
e
.
target
.
position
.
lineNumber
;
/**
const
hrefAttr
=
`#L
${
lineNum
}
`
;
* Removes existing line decorations and updates the reference on the instance
let
el
=
target
.
querySelector
(
'
a
'
);
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
if
(
!
el
)
{
*/
el
=
createAnchor
(
hrefAttr
);
removeHighlights
:
(
instance
)
=>
{
target
.
appendChild
(
el
);
Object
.
assign
(
instance
,
{
}
lineDecorations
:
instance
.
deltaDecorations
(
instance
.
lineDecorations
||
[],
[]),
}
}
static
setupLineLinking
(
instance
)
{
instance
.
onMouseMove
(
SourceEditorExtension
.
onMouseMoveHandler
);
instance
.
onMouseDown
((
e
)
=>
{
const
isCorrectAnchor
=
e
.
target
.
element
.
classList
.
contains
(
'
link-anchor
'
);
if
(
!
isCorrectAnchor
)
{
return
;
}
if
(
instance
.
lineDecorations
)
{
instance
.
deltaDecorations
(
instance
.
lineDecorations
,
[]);
}
});
});
},
/**
* Returns a function that can only be invoked once between
* each browser screen repaint.
* @param {Array} bounds - The [start, end] array with start
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* and end coordinates for highlighting
*/
highlightLines
(
instance
,
bounds
=
null
)
{
SourceEditorExtension
.
highlightLines
(
instance
,
bounds
);
},
};
}
}
}
}
app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js
View file @
021bb329
import
{
Position
}
from
'
monaco-editor
'
;
import
{
Position
}
from
'
monaco-editor
'
;
import
{
SourceEditorExtension
}
from
'
./source_editor_extension_base
'
;
export
class
FileTemplateExtension
extends
SourceEditorExtension
{
export
class
FileTemplateExtension
{
navigateFileStart
()
{
static
get
extensionName
()
{
this
.
setPosition
(
new
Position
(
1
,
1
));
return
'
FileTemplate
'
;
}
// eslint-disable-next-line class-methods-use-this
provides
()
{
return
{
navigateFileStart
:
(
instance
)
=>
{
instance
.
setPosition
(
new
Position
(
1
,
1
));
},
};
}
}
}
}
app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
View file @
021bb329
import
{
EditorMarkdownPreviewExtension
}
from
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
;
export
class
EditorMarkdownExtension
{
static
get
extensionName
()
{
return
'
EditorMarkdown
'
;
}
export
class
EditorMarkdownExtension
extends
EditorMarkdownPreviewExtension
{
// eslint-disable-next-line class-methods-use-this
getSelectedText
(
selection
=
this
.
getSelection
())
{
provides
()
{
return
{
getSelectedText
:
(
instance
,
selection
=
instance
.
getSelection
())
=>
{
const
{
startLineNumber
,
endLineNumber
,
startColumn
,
endColumn
}
=
selection
;
const
{
startLineNumber
,
endLineNumber
,
startColumn
,
endColumn
}
=
selection
;
const
valArray
=
this
.
getValue
().
split
(
'
\n
'
);
const
valArray
=
instance
.
getValue
().
split
(
'
\n
'
);
let
text
=
''
;
let
text
=
''
;
if
(
startLineNumber
===
endLineNumber
)
{
if
(
startLineNumber
===
endLineNumber
)
{
text
=
valArray
[
startLineNumber
-
1
].
slice
(
startColumn
-
1
,
endColumn
-
1
);
text
=
valArray
[
startLineNumber
-
1
].
slice
(
startColumn
-
1
,
endColumn
-
1
);
...
@@ -20,20 +25,17 @@ export class EditorMarkdownExtension extends EditorMarkdownPreviewExtension {
...
@@ -20,20 +25,17 @@ export class EditorMarkdownExtension extends EditorMarkdownPreviewExtension {
:
[
startLineText
,
endLineText
].
join
(
'
\n
'
);
:
[
startLineText
,
endLineText
].
join
(
'
\n
'
);
}
}
return
text
;
return
text
;
}
},
replaceSelectedText
:
(
instance
,
text
,
select
)
=>
{
replaceSelectedText
(
text
,
select
=
undefined
)
{
const
forceMoveMarkers
=
!
select
;
const
forceMoveMarkers
=
!
select
;
this
.
executeEdits
(
''
,
[{
range
:
this
.
getSelection
(),
text
,
forceMoveMarkers
}]);
instance
.
executeEdits
(
''
,
[{
range
:
instance
.
getSelection
(),
text
,
forceMoveMarkers
}]);
}
},
moveCursor
:
(
instance
,
dx
=
0
,
dy
=
0
)
=>
{
moveCursor
(
dx
=
0
,
dy
=
0
)
{
const
pos
=
instance
.
getPosition
();
const
pos
=
this
.
getPosition
();
pos
.
column
+=
dx
;
pos
.
column
+=
dx
;
pos
.
lineNumber
+=
dy
;
pos
.
lineNumber
+=
dy
;
this
.
setPosition
(
pos
);
instance
.
setPosition
(
pos
);
}
},
/**
/**
* Adjust existing selection to select text within the original selection.
* Adjust existing selection to select text within the original selection.
* - If `selectedText` is not supplied, we fetch selected text with
* - If `selectedText` is not supplied, we fetch selected text with
...
@@ -58,16 +60,17 @@ export class EditorMarkdownExtension extends EditorMarkdownPreviewExtension {
...
@@ -58,16 +60,17 @@ export class EditorMarkdownExtension extends EditorMarkdownPreviewExtension {
* 5. Adjust the start and end positions of the current selection
* 5. Adjust the start and end positions of the current selection
* 6. Re-set selection on the instance
* 6. Re-set selection on the instance
*
*
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance. Is passed automatically.
* @param {string} toSelect - New text to select within current selection.
* @param {string} toSelect - New text to select within current selection.
* @param {string} selectedText - Currently selected text. It's just a
* @param {string} selectedText - Currently selected text. It's just a
* shortcut: If it's not supplied, we fetch selected text from the instance
* shortcut: If it's not supplied, we fetch selected text from the instance
*/
*/
selectWithinSelection
(
toSelect
,
selectedText
)
{
selectWithinSelection
:
(
instance
,
toSelect
,
selectedText
)
=>
{
const
currentSelection
=
this
.
getSelection
();
const
currentSelection
=
instance
.
getSelection
();
if
(
currentSelection
.
isEmpty
()
||
!
toSelect
)
{
if
(
currentSelection
.
isEmpty
()
||
!
toSelect
)
{
return
;
return
;
}
}
const
text
=
selectedText
||
this
.
getSelectedText
(
currentSelection
);
const
text
=
selectedText
||
instance
.
getSelectedText
(
currentSelection
);
let
lineShift
;
let
lineShift
;
let
newStartLineNumber
;
let
newStartLineNumber
;
let
newStartColumn
;
let
newStartColumn
;
...
@@ -92,6 +95,8 @@ export class EditorMarkdownExtension extends EditorMarkdownPreviewExtension {
...
@@ -92,6 +95,8 @@ export class EditorMarkdownExtension extends EditorMarkdownPreviewExtension {
.
setStartPosition
(
newStartLineNumber
,
newStartColumn
)
.
setStartPosition
(
newStartLineNumber
,
newStartColumn
)
.
setEndPosition
(
newEndLineNumber
,
newEndColumn
);
.
setEndPosition
(
newEndLineNumber
,
newEndColumn
);
this
.
setSelection
(
newSelection
);
instance
.
setSelection
(
newSelection
);
},
};
}
}
}
}
app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
View file @
021bb329
...
@@ -12,9 +12,8 @@ import {
...
@@ -12,9 +12,8 @@ import {
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS
,
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS
,
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
,
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
,
}
from
'
../constants
'
;
}
from
'
../constants
'
;
import
{
SourceEditorExtension
}
from
'
./source_editor_extension_base
'
;
const
get
Preview
=
(
text
,
previewMarkdownPath
)
=>
{
const
fetch
Preview
=
(
text
,
previewMarkdownPath
)
=>
{
return
axios
return
axios
.
post
(
previewMarkdownPath
,
{
.
post
(
previewMarkdownPath
,
{
text
,
text
,
...
@@ -34,19 +33,20 @@ const setupDomElement = ({ injectToEl = null } = {}) => {
...
@@ -34,19 +33,20 @@ const setupDomElement = ({ injectToEl = null } = {}) => {
return
previewEl
;
return
previewEl
;
};
};
export
class
EditorMarkdownPreviewExtension
extends
SourceEditorExtension
{
export
class
EditorMarkdownPreviewExtension
{
constructor
({
instance
,
previewMarkdownPath
,
...
args
}
=
{})
{
static
get
extensionName
()
{
super
({
instance
,
...
args
});
return
'
EditorMarkdownPreview
'
;
Object
.
assign
(
instance
,
{
}
previewMarkdownPath
,
preview
:
{
onSetup
(
instance
,
setupOptions
)
{
this
.
preview
=
{
el
:
undefined
,
el
:
undefined
,
action
:
undefined
,
action
:
undefined
,
shown
:
false
,
shown
:
false
,
modelChangeListener
:
undefined
,
modelChangeListener
:
undefined
,
}
,
path
:
setupOptions
.
previewMarkdownPath
,
}
)
;
};
this
.
setupPreviewAction
.
call
(
instance
);
this
.
setupPreviewAction
(
instance
);
instance
.
getModel
().
onDidChangeLanguage
(({
newLanguage
,
oldLanguage
}
=
{})
=>
{
instance
.
getModel
().
onDidChangeLanguage
(({
newLanguage
,
oldLanguage
}
=
{})
=>
{
if
(
newLanguage
===
'
markdown
'
&&
oldLanguage
!==
newLanguage
)
{
if
(
newLanguage
===
'
markdown
'
&&
oldLanguage
!==
newLanguage
)
{
...
@@ -68,43 +68,31 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
...
@@ -68,43 +68,31 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
});
});
}
}
static
togglePreviewLayout
(
)
{
togglePreviewLayout
(
instance
)
{
const
{
width
,
height
}
=
this
.
getLayoutInfo
();
const
{
width
,
height
}
=
instance
.
getLayoutInfo
();
const
newWidth
=
this
.
preview
.
shown
const
newWidth
=
this
.
preview
.
shown
?
width
/
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
?
width
/
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
:
width
*
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
;
:
width
*
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
;
this
.
layout
({
width
:
newWidth
,
height
});
instance
.
layout
({
width
:
newWidth
,
height
});
}
}
static
togglePreviewPanel
(
)
{
togglePreviewPanel
(
instance
)
{
const
parentEl
=
this
.
getDomNode
().
parentElement
;
const
parentEl
=
instance
.
getDomNode
().
parentElement
;
const
{
el
:
previewEl
}
=
this
.
preview
;
const
{
el
:
previewEl
}
=
this
.
preview
;
parentEl
.
classList
.
toggle
(
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS
);
parentEl
.
classList
.
toggle
(
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS
);
if
(
previewEl
.
style
.
display
===
'
none
'
)
{
if
(
previewEl
.
style
.
display
===
'
none
'
)
{
// Show the preview panel
// Show the preview panel
this
.
fetchPreview
();
this
.
fetchPreview
(
instance
);
}
else
{
}
else
{
// Hide the preview panel
// Hide the preview panel
previewEl
.
style
.
display
=
'
none
'
;
previewEl
.
style
.
display
=
'
none
'
;
}
}
}
}
cleanup
()
{
fetchPreview
(
instance
)
{
if
(
this
.
preview
.
modelChangeListener
)
{
this
.
preview
.
modelChangeListener
.
dispose
();
}
this
.
preview
.
action
.
dispose
();
if
(
this
.
preview
.
shown
)
{
EditorMarkdownPreviewExtension
.
togglePreviewPanel
.
call
(
this
);
EditorMarkdownPreviewExtension
.
togglePreviewLayout
.
call
(
this
);
}
this
.
preview
.
shown
=
false
;
}
fetchPreview
()
{
const
{
el
:
previewEl
}
=
this
.
preview
;
const
{
el
:
previewEl
}
=
this
.
preview
;
getPreview
(
this
.
getValue
(),
this
.
previewMarkdownP
ath
)
fetchPreview
(
instance
.
getValue
(),
this
.
preview
.
p
ath
)
.
then
((
data
)
=>
{
.
then
((
data
)
=>
{
previewEl
.
innerHTML
=
sanitize
(
data
);
previewEl
.
innerHTML
=
sanitize
(
data
);
syntaxHighlight
(
previewEl
.
querySelectorAll
(
'
.js-syntax-highlight
'
));
syntaxHighlight
(
previewEl
.
querySelectorAll
(
'
.js-syntax-highlight
'
));
...
@@ -113,10 +101,10 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
...
@@ -113,10 +101,10 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
.
catch
(()
=>
createFlash
(
BLOB_PREVIEW_ERROR
));
.
catch
(()
=>
createFlash
(
BLOB_PREVIEW_ERROR
));
}
}
setupPreviewAction
()
{
setupPreviewAction
(
instance
)
{
if
(
this
.
getAction
(
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID
))
return
;
if
(
instance
.
getAction
(
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID
))
return
;
this
.
preview
.
action
=
this
.
addAction
({
this
.
preview
.
action
=
instance
.
addAction
({
id
:
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID
,
id
:
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID
,
label
:
__
(
'
Preview Markdown
'
),
label
:
__
(
'
Preview Markdown
'
),
keybindings
:
[
keybindings
:
[
...
@@ -128,27 +116,52 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
...
@@ -128,27 +116,52 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
// Method that will be executed when the action is triggered.
// Method that will be executed when the action is triggered.
// @param ed The editor instance is passed in as a convenience
// @param ed The editor instance is passed in as a convenience
run
(
inst
ance
)
{
run
(
inst
)
{
inst
ance
.
togglePreview
();
inst
.
togglePreview
();
},
},
});
});
}
}
togglePreview
()
{
provides
()
{
return
{
markdownPreview
:
this
.
preview
,
cleanup
:
(
instance
)
=>
{
if
(
this
.
preview
.
modelChangeListener
)
{
this
.
preview
.
modelChangeListener
.
dispose
();
}
this
.
preview
.
action
.
dispose
();
if
(
this
.
preview
.
shown
)
{
this
.
togglePreviewPanel
(
instance
);
this
.
togglePreviewLayout
(
instance
);
}
this
.
preview
.
shown
=
false
;
},
fetchPreview
:
(
instance
)
=>
this
.
fetchPreview
(
instance
),
setupPreviewAction
:
(
instance
)
=>
this
.
setupPreviewAction
(
instance
),
togglePreview
:
(
instance
)
=>
{
if
(
!
this
.
preview
?.
el
)
{
if
(
!
this
.
preview
?.
el
)
{
this
.
preview
.
el
=
setupDomElement
({
injectToEl
:
this
.
getDomNode
().
parentElement
});
this
.
preview
.
el
=
setupDomElement
({
injectToEl
:
instance
.
getDomNode
().
parentElement
});
}
}
EditorMarkdownPreviewExtension
.
togglePreviewLayout
.
call
(
this
);
this
.
togglePreviewLayout
(
instance
);
EditorMarkdownPreviewExtension
.
togglePreviewPanel
.
call
(
this
);
this
.
togglePreviewPanel
(
instance
);
if
(
!
this
.
preview
?.
shown
)
{
if
(
!
this
.
preview
?.
shown
)
{
this
.
preview
.
modelChangeListener
=
this
.
onDidChangeModelContent
(
this
.
preview
.
modelChangeListener
=
instance
.
onDidChangeModelContent
(
debounce
(
this
.
fetchPreview
.
bind
(
this
),
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
),
debounce
(
this
.
fetchPreview
.
bind
(
this
,
instance
),
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
,
),
);
);
}
else
{
}
else
{
this
.
preview
.
modelChangeListener
.
dispose
();
this
.
preview
.
modelChangeListener
.
dispose
();
}
}
this
.
preview
.
shown
=
!
this
.
preview
?.
shown
;
this
.
preview
.
shown
=
!
this
.
preview
?.
shown
;
},
};
}
}
}
}
app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
View file @
021bb329
/**
* A WebIDE Extension options for Source Editor
* @typedef {Object} WebIDEExtensionOptions
* @property {Object} modelManager The root manager for WebIDE models
* @property {Object} store The state store for communication
* @property {Object} file
* @property {Object} options The Monaco editor options
*/
import
{
debounce
}
from
'
lodash
'
;
import
{
debounce
}
from
'
lodash
'
;
import
{
KeyCode
,
KeyMod
,
Range
}
from
'
monaco-editor
'
;
import
{
KeyCode
,
KeyMod
,
Range
}
from
'
monaco-editor
'
;
import
{
EDITOR_TYPE_DIFF
}
from
'
~/editor/constants
'
;
import
{
EDITOR_TYPE_DIFF
}
from
'
~/editor/constants
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
Disposable
from
'
~/ide/lib/common/disposable
'
;
import
Disposable
from
'
~/ide/lib/common/disposable
'
;
import
{
editorOptions
}
from
'
~/ide/lib/editor_options
'
;
import
{
editorOptions
}
from
'
~/ide/lib/editor_options
'
;
import
keymap
from
'
~/ide/lib/keymap.json
'
;
import
keymap
from
'
~/ide/lib/keymap.json
'
;
...
@@ -11,41 +19,14 @@ const isDiffEditorType = (instance) => {
...
@@ -11,41 +19,14 @@ const isDiffEditorType = (instance) => {
};
};
export
const
UPDATE_DIMENSIONS_DELAY
=
200
;
export
const
UPDATE_DIMENSIONS_DELAY
=
200
;
const
defaultOptions
=
{
modelManager
:
undefined
,
store
:
undefined
,
file
:
undefined
,
options
:
{},
};
export
class
EditorWebIdeExtension
extends
SourceEditorExtension
{
const
addActions
=
(
instance
,
store
)
=>
{
constructor
({
instance
,
modelManager
,
...
options
}
=
{})
{
super
({
instance
,
...
options
,
modelManager
,
disposable
:
new
Disposable
(),
debouncedUpdate
:
debounce
(()
=>
{
instance
.
updateDimensions
();
},
UPDATE_DIMENSIONS_DELAY
),
});
window
.
addEventListener
(
'
resize
'
,
instance
.
debouncedUpdate
,
false
);
instance
.
onDidDispose
(()
=>
{
window
.
removeEventListener
(
'
resize
'
,
instance
.
debouncedUpdate
);
// catch any potential errors with disposing the error
// this is mainly for tests caused by elements not existing
try
{
instance
.
disposable
.
dispose
();
}
catch
(
e
)
{
if
(
process
.
env
.
NODE_ENV
!==
'
test
'
)
{
// eslint-disable-next-line no-console
console
.
error
(
e
);
}
}
});
EditorWebIdeExtension
.
addActions
(
instance
);
}
static
addActions
(
instance
)
{
const
{
store
}
=
instance
;
const
getKeyCode
=
(
key
)
=>
{
const
getKeyCode
=
(
key
)
=>
{
const
monacoKeyMod
=
key
.
indexOf
(
'
KEY_
'
)
===
0
;
const
monacoKeyMod
=
key
.
indexOf
(
'
KEY_
'
)
===
0
;
...
@@ -72,15 +53,76 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
...
@@ -72,15 +53,76 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
},
},
});
});
});
});
};
const
renderSideBySide
=
(
domElement
)
=>
{
return
domElement
.
offsetWidth
>=
700
;
};
const
updateInstanceDimensions
=
(
instance
)
=>
{
instance
.
layout
();
if
(
isDiffEditorType
(
instance
))
{
instance
.
updateOptions
({
renderSideBySide
:
renderSideBySide
(
instance
.
getDomNode
()),
});
}
}
};
createModel
(
file
,
head
=
null
)
{
export
class
EditorWebIdeExtension
{
return
this
.
modelManager
.
addModel
(
file
,
head
);
static
get
extensionName
()
{
return
'
EditorWebIde
'
;
}
/**
* Set up the WebIDE extension for Source Editor
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @param {WebIDEExtensionOptions} setupOptions
*/
onSetup
(
instance
,
setupOptions
=
defaultOptions
)
{
this
.
modelManager
=
setupOptions
.
modelManager
;
this
.
store
=
setupOptions
.
store
;
this
.
file
=
setupOptions
.
file
;
this
.
options
=
setupOptions
.
options
;
this
.
disposable
=
new
Disposable
();
this
.
debouncedUpdate
=
debounce
(()
=>
{
updateInstanceDimensions
(
instance
);
},
UPDATE_DIMENSIONS_DELAY
);
addActions
(
instance
,
setupOptions
.
store
);
}
onUse
(
instance
)
{
window
.
addEventListener
(
'
resize
'
,
this
.
debouncedUpdate
,
false
);
instance
.
onDidDispose
(()
=>
{
this
.
onUnuse
();
});
}
onUnuse
()
{
window
.
removeEventListener
(
'
resize
'
,
this
.
debouncedUpdate
);
// catch any potential errors with disposing the error
// this is mainly for tests caused by elements not existing
try
{
this
.
disposable
.
dispose
();
}
catch
(
e
)
{
if
(
process
.
env
.
NODE_ENV
!==
'
test
'
)
{
// eslint-disable-next-line no-console
console
.
error
(
e
);
}
}
}
}
attachModel
(
model
)
{
provides
()
{
if
(
isDiffEditorType
(
this
))
{
return
{
this
.
setModel
({
createModel
:
(
instance
,
file
,
head
=
null
)
=>
{
return
this
.
modelManager
.
addModel
(
file
,
head
);
},
attachModel
:
(
instance
,
model
)
=>
{
if
(
isDiffEditorType
(
instance
))
{
instance
.
setModel
({
original
:
model
.
getOriginalModel
(),
original
:
model
.
getOriginalModel
(),
modified
:
model
.
getModel
(),
modified
:
model
.
getModel
(),
});
});
...
@@ -88,9 +130,9 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
...
@@ -88,9 +130,9 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
return
;
return
;
}
}
this
.
setModel
(
model
.
getModel
());
instance
.
setModel
(
model
.
getModel
());
this
.
updateOptions
(
instance
.
updateOptions
(
editorOptions
.
reduce
((
acc
,
obj
)
=>
{
editorOptions
.
reduce
((
acc
,
obj
)
=>
{
Object
.
keys
(
obj
).
forEach
((
key
)
=>
{
Object
.
keys
(
obj
).
forEach
((
key
)
=>
{
Object
.
assign
(
acc
,
{
Object
.
assign
(
acc
,
{
...
@@ -100,51 +142,33 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
...
@@ -100,51 +142,33 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
return
acc
;
return
acc
;
},
{}),
},
{}),
);
);
}
},
attachMergeRequestModel
:
(
instance
,
model
)
=>
{
attachMergeRequestModel
(
model
)
{
instance
.
setModel
({
this
.
setModel
({
original
:
model
.
getBaseModel
(),
original
:
model
.
getBaseModel
(),
modified
:
model
.
getModel
(),
modified
:
model
.
getModel
(),
});
});
}
},
updateDimensions
:
(
instance
)
=>
updateInstanceDimensions
(
instance
),
updateDimensions
()
{
setPos
:
(
instance
,
{
lineNumber
,
column
})
=>
{
this
.
layout
();
instance
.
revealPositionInCenter
({
this
.
updateDiffView
();
}
setPos
({
lineNumber
,
column
})
{
this
.
revealPositionInCenter
({
lineNumber
,
lineNumber
,
column
,
column
,
});
});
this
.
setPosition
({
instance
.
setPosition
({
lineNumber
,
lineNumber
,
column
,
column
,
});
});
}
},
onPositionChange
:
(
instance
,
cb
)
=>
{
onPositionChange
(
cb
)
{
if
(
typeof
instance
.
onDidChangeCursorPosition
!==
'
function
'
)
{
if
(
!
this
.
onDidChangeCursorPosition
)
{
return
;
}
this
.
disposable
.
add
(
this
.
onDidChangeCursorPosition
((
e
)
=>
cb
(
this
,
e
)));
}
updateDiffView
()
{
if
(
!
isDiffEditorType
(
this
))
{
return
;
return
;
}
}
this
.
updateOptions
({
this
.
disposable
.
add
(
instance
.
onDidChangeCursorPosition
((
e
)
=>
cb
(
instance
,
e
)));
renderSideBySide
:
EditorWebIdeExtension
.
renderSideBySide
(
this
.
getDomNode
()),
},
});
replaceSelectedText
:
(
instance
,
text
)
=>
{
}
let
selection
=
instance
.
getSelection
();
replaceSelectedText
(
text
)
{
let
selection
=
this
.
getSelection
();
const
range
=
new
Range
(
const
range
=
new
Range
(
selection
.
startLineNumber
,
selection
.
startLineNumber
,
selection
.
startColumn
,
selection
.
startColumn
,
...
@@ -152,13 +176,11 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
...
@@ -152,13 +176,11 @@ export class EditorWebIdeExtension extends SourceEditorExtension {
selection
.
endColumn
,
selection
.
endColumn
,
);
);
this
.
executeEdits
(
''
,
[{
range
,
text
}]);
instance
.
executeEdits
(
''
,
[{
range
,
text
}]);
selection
=
this
.
getSelection
();
selection
=
instance
.
getSelection
();
this
.
setPosition
({
lineNumber
:
selection
.
endLineNumber
,
column
:
selection
.
endColumn
});
instance
.
setPosition
({
lineNumber
:
selection
.
endLineNumber
,
column
:
selection
.
endColumn
});
}
},
};
static
renderSideBySide
(
domElement
)
{
return
domElement
.
offsetWidth
>=
700
;
}
}
}
}
app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
View file @
021bb329
/**
* A Yaml Editor Extension options for Source Editor
* @typedef {Object} YamlEditorExtensionOptions
* @property { boolean } enableComments Convert model nodes with the comment
* pattern to comments?
* @property { string } highlightPath Add a line highlight to the
* node specified by this e.g. `"foo.bar[0]"`
* @property { * } model Any JS Object that will be stringified and used as the
* editor's value. Equivalent to using `setDataModel()`
* @property options SourceEditorExtension Options
*/
import
{
toPath
}
from
'
lodash
'
;
import
{
toPath
}
from
'
lodash
'
;
import
{
parseDocument
,
Document
,
visit
,
isScalar
,
isCollection
,
isMap
}
from
'
yaml
'
;
import
{
parseDocument
,
Document
,
visit
,
isScalar
,
isCollection
,
isMap
}
from
'
yaml
'
;
import
{
findPair
}
from
'
yaml/util
'
;
import
{
findPair
}
from
'
yaml/util
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
export
class
YamlEditorExtension
extends
SourceEditorExtension
{
export
class
YamlEditorExtension
{
static
get
extensionName
()
{
return
'
YamlEditor
'
;
}
/**
/**
* Extends the source editor with capabilities for yaml files.
* Extends the source editor with capabilities for yaml files.
*
*
* @param { Instance } instance Source Editor Instance
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @param { boolean } enableComments Convert model nodes with the comment
* @param {YamlEditorExtensionOptions} setupOptions
* pattern to comments?
* @param { string } highlightPath Add a line highlight to the
* node specified by this e.g. `"foo.bar[0]"`
* @param { * } model Any JS Object that will be stringified and used as the
* editor's value. Equivalent to using `setDataModel()`
* @param options SourceEditorExtension Options
*/
*/
constructor
({
onSetup
(
instance
,
setupOptions
=
{})
{
instance
,
const
{
enableComments
=
false
,
highlightPath
=
null
,
model
=
null
}
=
setupOptions
;
enableComments
=
false
,
this
.
enableComments
=
enableComments
;
highlightPath
=
null
,
this
.
highlightPath
=
highlightPath
;
model
=
null
,
this
.
model
=
model
;
...
options
}
=
{})
{
super
({
instance
,
options
:
{
...
options
,
enableComments
,
highlightPath
,
},
});
if
(
model
)
{
if
(
model
)
{
YamlEditorExtension
.
initFromModel
(
instance
,
model
);
this
.
initFromModel
(
instance
,
model
);
}
}
instance
.
onDidChangeModelContent
(()
=>
instance
.
onUpdate
());
instance
.
onDidChangeModelContent
(()
=>
instance
.
onUpdate
());
}
}
/**
initFromModel
(
instance
,
model
)
{
* @private
*/
static
initFromModel
(
instance
,
model
)
{
const
doc
=
new
Document
(
model
);
const
doc
=
new
Document
(
model
);
if
(
instance
.
option
s
.
enableComments
)
{
if
(
thi
s
.
enableComments
)
{
YamlEditorExtension
.
transformComments
(
doc
);
YamlEditorExtension
.
transformComments
(
doc
);
}
}
instance
.
setValue
(
doc
.
toString
());
instance
.
setValue
(
doc
.
toString
());
...
@@ -160,68 +156,118 @@ export class YamlEditorExtension extends SourceEditorExtension {
...
@@ -160,68 +156,118 @@ export class YamlEditorExtension extends SourceEditorExtension {
return
doc
;
return
doc
;
}
}
static
getDoc
(
instance
)
{
return
parseDocument
(
instance
.
getValue
());
}
static
locate
(
instance
,
path
)
{
if
(
!
path
)
throw
Error
(
`No path provided.`
);
const
blob
=
instance
.
getValue
();
const
doc
=
parseDocument
(
blob
);
const
pathArray
=
toPath
(
path
);
if
(
!
doc
.
getIn
(
pathArray
))
{
throw
Error
(
`The node
${
path
}
could not be found inside the document.`
);
}
const
parentNode
=
doc
.
getIn
(
pathArray
.
slice
(
0
,
pathArray
.
length
-
1
));
let
startChar
;
let
endChar
;
if
(
isMap
(
parentNode
))
{
const
node
=
parentNode
.
items
.
find
(
(
item
)
=>
item
.
key
.
value
===
pathArray
[
pathArray
.
length
-
1
],
);
[
startChar
]
=
node
.
key
.
range
;
[,
,
endChar
]
=
node
.
value
.
range
;
}
else
{
const
node
=
doc
.
getIn
(
pathArray
);
[
startChar
,
,
endChar
]
=
node
.
range
;
}
const
startSlice
=
blob
.
slice
(
0
,
startChar
);
const
endSlice
=
blob
.
slice
(
0
,
endChar
);
const
startLine
=
(
startSlice
.
match
(
/
\n
/g
)
||
[]).
length
+
1
;
const
endLine
=
(
endSlice
.
match
(
/
\n
/g
)
||
[]).
length
;
return
[
startLine
,
endLine
];
}
setDoc
(
instance
,
doc
)
{
if
(
this
.
enableComments
)
{
YamlEditorExtension
.
transformComments
(
doc
);
}
if
(
!
instance
.
getValue
())
{
instance
.
setValue
(
doc
.
toString
());
}
else
{
instance
.
updateValue
(
doc
.
toString
());
}
}
highlight
(
instance
,
path
)
{
// IMPORTANT
// removeHighlight and highlightLines both come from
// SourceEditorExtension. So it has to be installed prior to this extension
if
(
this
.
highlightPath
===
path
)
return
;
if
(
!
path
)
{
instance
.
removeHighlights
();
}
else
{
const
res
=
YamlEditorExtension
.
locate
(
instance
,
path
);
instance
.
highlightLines
(
res
);
}
this
.
highlightPath
=
path
||
null
;
}
provides
()
{
return
{
/**
/**
* Get the editor's value parsed as a `Document` as defined by the `yaml`
* Get the editor's value parsed as a `Document` as defined by the `yaml`
* package
* package
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @returns {Document}
* @returns {Document}
*/
*/
getDoc
()
{
getDoc
:
(
instance
)
=>
YamlEditorExtension
.
getDoc
(
instance
),
return
parseDocument
(
this
.
getValue
());
}
/**
/**
* Accepts a `Document` as defined by the `yaml` package and
* Accepts a `Document` as defined by the `yaml` package and
* sets the Editor's value to a stringified version of it.
* sets the Editor's value to a stringified version of it.
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @param { Document } doc
* @param { Document } doc
*/
*/
setDoc
(
doc
)
{
setDoc
:
(
instance
,
doc
)
=>
this
.
setDoc
(
instance
,
doc
),
if
(
this
.
options
.
enableComments
)
{
YamlEditorExtension
.
transformComments
(
doc
);
}
if
(
!
this
.
getValue
())
{
this
.
setValue
(
doc
.
toString
());
}
else
{
this
.
updateValue
(
doc
.
toString
());
}
}
/**
/**
* Returns the parsed value of the Editor's content as JS.
* Returns the parsed value of the Editor's content as JS.
* @returns {*}
* @returns {*}
*/
*/
getDataModel
()
{
getDataModel
:
(
instance
)
=>
YamlEditorExtension
.
getDoc
(
instance
).
toJS
(),
return
this
.
getDoc
().
toJS
();
}
/**
/**
* Accepts any JS Object and sets the Editor's value to a stringified version
* Accepts any JS Object and sets the Editor's value to a stringified version
* of that value.
* of that value.
*
*
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @param value
* @param value
*/
*/
setDataModel
(
value
)
{
setDataModel
:
(
instance
,
value
)
=>
this
.
setDoc
(
instance
,
new
Document
(
value
)),
this
.
setDoc
(
new
Document
(
value
));
}
/**
/**
* Method to be executed when the Editor's <TextModel> was updated
* Method to be executed when the Editor's <TextModel> was updated
*/
*/
onUpdate
()
{
onUpdate
:
(
instance
)
=>
{
if
(
this
.
options
.
highlightPath
)
{
if
(
this
.
highlightPath
)
{
this
.
highlight
(
this
.
options
.
highlightPath
);
this
.
highlight
(
instance
,
this
.
highlightPath
);
}
}
}
},
/**
/**
* Set the editors content to the input without recreating the content model.
* Set the editors content to the input without recreating the content model.
*
*
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @param blob
* @param blob
*/
*/
updateValue
(
blob
)
{
updateValue
:
(
instance
,
blob
)
=>
{
// Using applyEdits() instead of setValue() ensures that tokens such as
// Using applyEdits() instead of setValue() ensures that tokens such as
// highlighted lines aren't deleted/recreated which causes a flicker.
// highlighted lines aren't deleted/recreated which causes a flicker.
const
model
=
this
.
getModel
();
const
model
=
instance
.
getModel
();
model
.
applyEdits
([
model
.
applyEdits
([
{
{
// A nice improvement would be to replace getFullModelRange() with
// A nice improvement would be to replace getFullModelRange() with
...
@@ -231,63 +277,32 @@ export class YamlEditorExtension extends SourceEditorExtension {
...
@@ -231,63 +277,32 @@ export class YamlEditorExtension extends SourceEditorExtension {
text
:
blob
,
text
:
blob
,
},
},
]);
]);
}
},
/**
/**
* Add a line highlight style to the node specified by the path.
* Add a line highlight style to the node specified by the path.
*
*
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @param {string|null|false} path A path to a node of the Editor's value,
* @param {string|null|false} path A path to a node of the Editor's value,
* e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all
* e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all
* highlights.
* highlights.
*/
*/
highlight
(
path
)
{
highlight
:
(
instance
,
path
)
=>
this
.
highlight
(
instance
,
path
),
if
(
this
.
options
.
highlightPath
===
path
)
return
;
if
(
!
path
)
{
SourceEditorExtension
.
removeHighlights
(
this
);
}
else
{
const
res
=
this
.
locate
(
path
);
SourceEditorExtension
.
highlightLines
(
this
,
res
);
}
this
.
options
.
highlightPath
=
path
||
null
;
}
/**
/**
* Return the line numbers of a certain node identified by `path` within
* Return the line numbers of a certain node identified by `path` within
* the yaml.
* the yaml.
*
*
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
* @param {string} path A path to a node, eg. `foo.bar[0]`
* @param {string} path A path to a node, eg. `foo.bar[0]`
* @returns {number[]} Array following the schema `[firstLine, lastLine]`
* @returns {number[]} Array following the schema `[firstLine, lastLine]`
* (both inclusive)
* (both inclusive)
*
*
* @throws {Error} Will throw if the path is not found inside the document
* @throws {Error} Will throw if the path is not found inside the document
*/
*/
locate
(
path
)
{
locate
:
(
instance
,
path
)
=>
YamlEditorExtension
.
locate
(
instance
,
path
),
if
(
!
path
)
throw
Error
(
`No path provided.`
);
const
blob
=
this
.
getValue
();
const
doc
=
parseDocument
(
blob
);
const
pathArray
=
toPath
(
path
);
if
(
!
doc
.
getIn
(
pathArray
))
{
initFromModel
:
(
instance
,
model
)
=>
this
.
initFromModel
(
instance
,
model
),
throw
Error
(
`The node
${
path
}
could not be found inside the document.`
);
};
}
const
parentNode
=
doc
.
getIn
(
pathArray
.
slice
(
0
,
pathArray
.
length
-
1
));
let
startChar
;
let
endChar
;
if
(
isMap
(
parentNode
))
{
const
node
=
parentNode
.
items
.
find
(
(
item
)
=>
item
.
key
.
value
===
pathArray
[
pathArray
.
length
-
1
],
);
[
startChar
]
=
node
.
key
.
range
;
[,
,
endChar
]
=
node
.
value
.
range
;
}
else
{
const
node
=
doc
.
getIn
(
pathArray
);
[
startChar
,
,
endChar
]
=
node
.
range
;
}
const
startSlice
=
blob
.
slice
(
0
,
startChar
);
const
endSlice
=
blob
.
slice
(
0
,
endChar
);
const
startLine
=
(
startSlice
.
match
(
/
\n
/g
)
||
[]).
length
+
1
;
const
endLine
=
(
endSlice
.
match
(
/
\n
/g
)
||
[]).
length
;
return
[
startLine
,
endLine
];
}
}
}
}
app/assets/javascripts/editor/source_editor.js
View file @
021bb329
import
{
editor
as
monacoEditor
,
Uri
}
from
'
monaco-editor
'
;
import
{
editor
as
monacoEditor
,
Uri
}
from
'
monaco-editor
'
;
import
{
waitForCSSLoaded
}
from
'
~/helpers/startup_css_helper
'
;
import
{
defaultEditorOptions
}
from
'
~/ide/lib/editor_options
'
;
import
{
defaultEditorOptions
}
from
'
~/ide/lib/editor_options
'
;
import
languages
from
'
~/ide/lib/languages
'
;
import
languages
from
'
~/ide/lib/languages
'
;
import
{
registerLanguages
}
from
'
~/ide/utils
'
;
import
{
registerLanguages
}
from
'
~/ide/utils
'
;
...
@@ -11,10 +12,39 @@ import {
...
@@ -11,10 +12,39 @@ import {
EDITOR_TYPE_DIFF
,
EDITOR_TYPE_DIFF
,
}
from
'
./constants
'
;
}
from
'
./constants
'
;
import
{
clearDomElement
,
setupEditorTheme
,
getBlobLanguage
}
from
'
./utils
'
;
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
{
export
default
class
SourceEditor
{
/**
* Constructs a global editor.
* @param {Object} options - Monaco config options used to create the editor
*/
constructor
(
options
=
{})
{
constructor
(
options
=
{})
{
this
.
instances
=
[];
this
.
instances
=
[];
this
.
extensionsStore
=
new
Map
();
this
.
options
=
{
this
.
options
=
{
extraEditorClassName
:
'
gl-source-editor
'
,
extraEditorClassName
:
'
gl-source-editor
'
,
...
defaultEditorOptions
,
...
defaultEditorOptions
,
...
@@ -26,19 +56,6 @@ export default class SourceEditor {
...
@@ -26,19 +56,6 @@ export default class SourceEditor {
registerLanguages
(...
languages
);
registerLanguages
(...
languages
);
}
}
static
mixIntoInstance
(
source
,
inst
)
{
if
(
!
inst
)
{
return
;
}
const
isClassInstance
=
source
.
constructor
.
prototype
!==
Object
.
prototype
;
const
sanitizedSource
=
isClassInstance
?
source
.
constructor
.
prototype
:
source
;
Object
.
getOwnPropertyNames
(
sanitizedSource
).
forEach
((
prop
)
=>
{
if
(
prop
!==
'
constructor
'
)
{
Object
.
assign
(
inst
,
{
[
prop
]:
source
[
prop
]
});
}
});
}
static
prepareInstance
(
el
)
{
static
prepareInstance
(
el
)
{
if
(
!
el
)
{
if
(
!
el
)
{
throw
new
Error
(
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL
);
throw
new
Error
(
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL
);
...
@@ -78,71 +95,17 @@ export default class SourceEditor {
...
@@ -78,71 +95,17 @@ export default class SourceEditor {
return
diffModel
;
return
diffModel
;
}
}
static
convertMonacoToELInstance
=
(
inst
)
=>
{
const
sourceEditorInstanceAPI
=
{
updateModelLanguage
:
(
path
)
=>
{
return
SourceEditor
.
instanceUpdateLanguage
(
inst
,
path
);
},
use
:
(
exts
=
[])
=>
{
return
SourceEditor
.
instanceApplyExtension
(
inst
,
exts
);
},
};
const
handler
=
{
get
(
target
,
prop
,
receiver
)
{
if
(
Reflect
.
has
(
sourceEditorInstanceAPI
,
prop
))
{
return
sourceEditorInstanceAPI
[
prop
];
}
return
Reflect
.
get
(
target
,
prop
,
receiver
);
},
};
return
new
Proxy
(
inst
,
handler
);
};
static
instanceUpdateLanguage
(
inst
,
path
)
{
const
lang
=
getBlobLanguage
(
path
);
const
model
=
inst
.
getModel
();
return
monacoEditor
.
setModelLanguage
(
model
,
lang
);
}
static
instanceApplyExtension
(
inst
,
exts
=
[])
{
const
extensions
=
[].
concat
(
exts
);
extensions
.
forEach
((
extension
)
=>
{
SourceEditor
.
mixIntoInstance
(
extension
,
inst
);
});
return
inst
;
}
static
instanceRemoveFromRegistry
(
editor
,
instance
)
{
const
index
=
editor
.
instances
.
findIndex
((
inst
)
=>
inst
===
instance
);
editor
.
instances
.
splice
(
index
,
1
);
}
static
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
();
}
}
/**
/**
* Creates a monaco instance with the given options.
* Creates a Source Editor Instance with the given options.
*
* @param {Object} options Options used to initialize the instance.
* @param {Object} options Options used to initialize monaco.
* @param {Element} options.el The element to attach the instance for.
* @param {Element} options.el The element which will be used to create the monacoEditor.
* @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.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.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 {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
({
createInstance
({
el
=
undefined
,
el
=
undefined
,
...
@@ -156,13 +119,18 @@ export default class SourceEditor {
...
@@ -156,13 +119,18 @@ export default class SourceEditor {
SourceEditor
.
prepareInstance
(
el
);
SourceEditor
.
prepareInstance
(
el
);
const
createEditorFn
=
isDiff
?
'
createDiffEditor
'
:
'
create
'
;
const
createEditorFn
=
isDiff
?
'
createDiffEditor
'
:
'
create
'
;
const
instance
=
SourceEditor
.
convertMonacoToEL
Instance
(
const
instance
=
new
Editor
Instance
(
monacoEditor
[
createEditorFn
].
call
(
this
,
el
,
{
monacoEditor
[
createEditorFn
].
call
(
this
,
el
,
{
...
this
.
options
,
...
this
.
options
,
...
instanceOptions
,
...
instanceOptions
,
}),
}),
this
.
extensionsStore
,
);
);
waitForCSSLoaded
(()
=>
{
instance
.
layout
();
});
let
model
;
let
model
;
if
(
instanceOptions
.
model
!==
null
)
{
if
(
instanceOptions
.
model
!==
null
)
{
model
=
SourceEditor
.
createEditorModel
({
model
=
SourceEditor
.
createEditorModel
({
...
@@ -176,8 +144,8 @@ export default class SourceEditor {
...
@@ -176,8 +144,8 @@ export default class SourceEditor {
}
}
instance
.
onDidDispose
(()
=>
{
instance
.
onDidDispose
(()
=>
{
SourceEditor
.
instanceRemoveFromRegistry
(
this
,
instance
);
instanceRemoveFromRegistry
(
this
,
instance
);
SourceEditor
.
instanceDisposeModels
(
this
,
instance
,
model
);
instanceDisposeModels
(
this
,
instance
,
model
);
});
});
this
.
instances
.
push
(
instance
);
this
.
instances
.
push
(
instance
);
...
@@ -185,6 +153,11 @@ export default class SourceEditor {
...
@@ -185,6 +153,11 @@ export default class SourceEditor {
return
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
)
{
createDiffInstance
(
args
)
{
return
this
.
createInstance
({
return
this
.
createInstance
({
...
args
,
...
args
,
...
@@ -192,6 +165,10 @@ export default class SourceEditor {
...
@@ -192,6 +165,10 @@ export default class SourceEditor {
});
});
}
}
/**
* Dispose global editor
* Automatically disposes all the instances registered for this editor
*/
dispose
()
{
dispose
()
{
this
.
instances
.
forEach
((
instance
)
=>
instance
.
dispose
());
this
.
instances
.
forEach
((
instance
)
=>
instance
.
dispose
());
}
}
...
...
app/assets/javascripts/editor/source_editor_extension.js
View file @
021bb329
...
@@ -5,10 +5,10 @@ export default class EditorExtension {
...
@@ -5,10 +5,10 @@ export default class EditorExtension {
if
(
typeof
definition
!==
'
function
'
)
{
if
(
typeof
definition
!==
'
function
'
)
{
throw
new
Error
(
EDITOR_EXTENSION_DEFINITION_ERROR
);
throw
new
Error
(
EDITOR_EXTENSION_DEFINITION_ERROR
);
}
}
this
.
name
=
definition
.
name
;
// both class- and fn-based extensions have a name
this
.
setupOptions
=
setupOptions
;
this
.
setupOptions
=
setupOptions
;
// eslint-disable-next-line new-cap
// eslint-disable-next-line new-cap
this
.
obj
=
new
definition
();
this
.
obj
=
new
definition
();
this
.
extensionName
=
definition
.
extensionName
||
this
.
obj
.
extensionName
;
// both class- and fn-based extensions have a name
}
}
get
api
()
{
get
api
()
{
...
...
app/assets/javascripts/editor/source_editor_instance.js
View file @
021bb329
...
@@ -13,7 +13,7 @@
...
@@ -13,7 +13,7 @@
* A Source Editor Extension
* A Source Editor Extension
* @typedef {Object} SourceEditorExtension
* @typedef {Object} SourceEditorExtension
* @property {Object} obj
* @property {Object} obj
* @property {string}
n
ame
* @property {string}
extensionN
ame
* @property {Object} api
* @property {Object} api
*/
*/
...
@@ -43,12 +43,12 @@ const utils = {
...
@@ -43,12 +43,12 @@ const utils = {
}
}
},
},
getStoredExtension
:
(
extensionsStore
,
n
ame
)
=>
{
getStoredExtension
:
(
extensionsStore
,
extensionN
ame
)
=>
{
if
(
!
extensionsStore
)
{
if
(
!
extensionsStore
)
{
logError
(
EDITOR_EXTENSION_STORE_IS_MISSING_ERROR
);
logError
(
EDITOR_EXTENSION_STORE_IS_MISSING_ERROR
);
return
undefined
;
return
undefined
;
}
}
return
extensionsStore
.
get
(
n
ame
);
return
extensionsStore
.
get
(
extensionN
ame
);
},
},
};
};
...
@@ -73,30 +73,18 @@ export default class EditorInstance {
...
@@ -73,30 +73,18 @@ export default class EditorInstance {
if
(
methodExtension
)
{
if
(
methodExtension
)
{
const
extension
=
extensionsStore
.
get
(
methodExtension
);
const
extension
=
extensionsStore
.
get
(
methodExtension
);
return
(...
args
)
=>
extension
.
api
[
prop
].
call
(
seInstance
,
receiver
,
...
args
);
if
(
typeof
extension
.
api
[
prop
]
===
'
function
'
)
{
return
extension
.
api
[
prop
].
bind
(
extension
.
obj
,
receiver
);
}
return
extension
.
api
[
prop
];
}
}
return
Reflect
.
get
(
seInstance
[
prop
]
?
seInstance
:
target
,
prop
,
receiver
);
return
Reflect
.
get
(
seInstance
[
prop
]
?
seInstance
:
target
,
prop
,
receiver
);
},
},
set
(
target
,
prop
,
value
)
{
Object
.
assign
(
seInstance
,
{
[
prop
]:
value
,
});
return
true
;
},
};
};
const
instProxy
=
new
Proxy
(
rootInstance
,
getHandler
);
const
instProxy
=
new
Proxy
(
rootInstance
,
getHandler
);
/**
this
.
dispatchExtAction
=
EditorInstance
.
useUnuse
.
bind
(
instProxy
,
extensionsStore
);
* Main entry point to apply an extension to the instance
* @param {SourceEditorExtensionDefinition}
*/
this
.
use
=
EditorInstance
.
useUnuse
.
bind
(
instProxy
,
extensionsStore
,
this
.
useExtension
);
/**
* Main entry point to un-use an extension and remove it from the instance
* @param {SourceEditorExtension}
*/
this
.
unuse
=
EditorInstance
.
useUnuse
.
bind
(
instProxy
,
extensionsStore
,
this
.
unuseExtension
);
return
instProxy
;
return
instProxy
;
}
}
...
@@ -141,7 +129,7 @@ export default class EditorInstance {
...
@@ -141,7 +129,7 @@ export default class EditorInstance {
}
}
// Existing Extension Path
// Existing Extension Path
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
definition
.
n
ame
);
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
definition
.
extensionN
ame
);
if
(
existingExt
)
{
if
(
existingExt
)
{
if
(
isEqual
(
extension
.
setupOptions
,
existingExt
.
setupOptions
))
{
if
(
isEqual
(
extension
.
setupOptions
,
existingExt
.
setupOptions
))
{
return
existingExt
;
return
existingExt
;
...
@@ -168,14 +156,14 @@ export default class EditorInstance {
...
@@ -168,14 +156,14 @@ export default class EditorInstance {
* @param {Map} extensionsStore - The global registry for the extension instances
* @param {Map} extensionsStore - The global registry for the extension instances
*/
*/
registerExtension
(
extension
,
extensionsStore
)
{
registerExtension
(
extension
,
extensionsStore
)
{
const
{
n
ame
}
=
extension
;
const
{
extensionN
ame
}
=
extension
;
const
hasExtensionRegistered
=
const
hasExtensionRegistered
=
extensionsStore
.
has
(
n
ame
)
&&
extensionsStore
.
has
(
extensionN
ame
)
&&
isEqual
(
extension
.
setupOptions
,
extensionsStore
.
get
(
n
ame
).
setupOptions
);
isEqual
(
extension
.
setupOptions
,
extensionsStore
.
get
(
extensionN
ame
).
setupOptions
);
if
(
hasExtensionRegistered
)
{
if
(
hasExtensionRegistered
)
{
return
;
return
;
}
}
extensionsStore
.
set
(
n
ame
,
extension
);
extensionsStore
.
set
(
extensionN
ame
,
extension
);
const
{
obj
:
extensionObj
}
=
extension
;
const
{
obj
:
extensionObj
}
=
extension
;
if
(
extensionObj
.
onUse
)
{
if
(
extensionObj
.
onUse
)
{
extensionObj
.
onUse
(
this
);
extensionObj
.
onUse
(
this
);
...
@@ -187,7 +175,7 @@ export default class EditorInstance {
...
@@ -187,7 +175,7 @@ export default class EditorInstance {
* @param {SourceEditorExtension} extension - Instance of Source Editor extension
* @param {SourceEditorExtension} extension - Instance of Source Editor extension
*/
*/
registerExtensionMethods
(
extension
)
{
registerExtensionMethods
(
extension
)
{
const
{
api
,
n
ame
}
=
extension
;
const
{
api
,
extensionN
ame
}
=
extension
;
if
(
!
api
)
{
if
(
!
api
)
{
return
;
return
;
...
@@ -197,7 +185,7 @@ export default class EditorInstance {
...
@@ -197,7 +185,7 @@ export default class EditorInstance {
if
(
this
[
prop
])
{
if
(
this
[
prop
])
{
logError
(
sprintf
(
EDITOR_EXTENSION_NAMING_CONFLICT_ERROR
,
{
prop
}));
logError
(
sprintf
(
EDITOR_EXTENSION_NAMING_CONFLICT_ERROR
,
{
prop
}));
}
else
{
}
else
{
this
.
methods
[
prop
]
=
n
ame
;
this
.
methods
[
prop
]
=
extensionN
ame
;
}
}
},
this
);
},
this
);
}
}
...
@@ -215,10 +203,10 @@ export default class EditorInstance {
...
@@ -215,10 +203,10 @@ export default class EditorInstance {
if
(
!
extension
)
{
if
(
!
extension
)
{
throw
new
Error
(
EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR
);
throw
new
Error
(
EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR
);
}
}
const
{
n
ame
}
=
extension
;
const
{
extensionN
ame
}
=
extension
;
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
n
ame
);
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
extensionN
ame
);
if
(
!
existingExt
)
{
if
(
!
existingExt
)
{
throw
new
Error
(
sprintf
(
EDITOR_EXTENSION_NOT_REGISTERED_ERROR
,
{
n
ame
}));
throw
new
Error
(
sprintf
(
EDITOR_EXTENSION_NOT_REGISTERED_ERROR
,
{
extensionN
ame
}));
}
}
const
{
obj
:
extensionObj
}
=
existingExt
;
const
{
obj
:
extensionObj
}
=
existingExt
;
if
(
extensionObj
.
onBeforeUnuse
)
{
if
(
extensionObj
.
onBeforeUnuse
)
{
...
@@ -235,12 +223,12 @@ export default class EditorInstance {
...
@@ -235,12 +223,12 @@ export default class EditorInstance {
* @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
* @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
*/
*/
unregisterExtensionMethods
(
extension
)
{
unregisterExtensionMethods
(
extension
)
{
const
{
api
,
n
ame
}
=
extension
;
const
{
api
,
extensionN
ame
}
=
extension
;
if
(
!
api
)
{
if
(
!
api
)
{
return
;
return
;
}
}
Object
.
keys
(
api
).
forEach
((
method
)
=>
{
Object
.
keys
(
api
).
forEach
((
method
)
=>
{
utils
.
removeExtFromMethod
(
method
,
n
ame
,
this
.
methods
);
utils
.
removeExtFromMethod
(
method
,
extensionN
ame
,
this
.
methods
);
});
});
}
}
...
@@ -259,6 +247,24 @@ export default class EditorInstance {
...
@@ -259,6 +247,24 @@ export default class EditorInstance {
monacoEditor
.
setModelLanguage
(
model
,
lang
);
monacoEditor
.
setModelLanguage
(
model
,
lang
);
}
}
/**
* Main entry point to apply an extension to the instance
* @param {SourceEditorExtensionDefinition[]|SourceEditorExtensionDefinition} extDefs - The extension(s) to use
* @returns {EditorExtension|*}
*/
use
(
extDefs
)
{
return
this
.
dispatchExtAction
(
this
.
useExtension
,
extDefs
);
}
/**
* Main entry point to remove an extension to the instance
* @param {SourceEditorExtension[]|SourceEditorExtension} exts -
* @returns {*}
*/
unuse
(
exts
)
{
return
this
.
dispatchExtAction
(
this
.
unuseExtension
,
exts
);
}
/**
/**
* Get the methods returned by extensions.
* Get the methods returned by extensions.
* @returns {Array}
* @returns {Array}
...
...
app/assets/javascripts/ide/components/repo_editor.vue
View file @
021bb329
...
@@ -7,6 +7,7 @@ import {
...
@@ -7,6 +7,7 @@ import {
EDITOR_CODE_INSTANCE_FN
,
EDITOR_CODE_INSTANCE_FN
,
EDITOR_DIFF_INSTANCE_FN
,
EDITOR_DIFF_INSTANCE_FN
,
}
from
'
~/editor/constants
'
;
}
from
'
~/editor/constants
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
EditorWebIdeExtension
}
from
'
~/editor/extensions/source_editor_webide_ext
'
;
import
{
EditorWebIdeExtension
}
from
'
~/editor/extensions/source_editor_webide_ext
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
...
@@ -302,30 +303,32 @@ export default {
...
@@ -302,30 +303,32 @@ export default {
...
instanceOptions
,
...
instanceOptions
,
...
this
.
editorOptions
,
...
this
.
editorOptions
,
});
});
this
.
editor
.
use
([
this
.
editor
.
use
(
{
new
EditorWebIdeExtension
({
definition
:
SourceEditorExtension
,
instance
:
this
.
editor
,
},
{
definition
:
EditorWebIdeExtension
,
setupOptions
:
{
modelManager
:
this
.
modelManager
,
modelManager
:
this
.
modelManager
,
store
:
this
.
$store
,
store
:
this
.
$store
,
file
:
this
.
file
,
file
:
this
.
file
,
options
:
this
.
editorOptions
,
options
:
this
.
editorOptions
,
}),
},
);
},
]);
if
(
if
(
this
.
fileType
===
MARKDOWN_FILE_TYPE
&&
this
.
fileType
===
MARKDOWN_FILE_TYPE
&&
this
.
editor
?.
getEditorType
()
===
EDITOR_TYPE_CODE
&&
this
.
editor
?.
getEditorType
()
===
EDITOR_TYPE_CODE
&&
this
.
previewMarkdownPath
this
.
previewMarkdownPath
)
{
)
{
import
(
'
~/editor/extensions/source_editor_markdown_ext
'
)
import
(
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
)
.
then
(({
EditorMarkdownExtension
:
MarkdownExtension
}
=
{})
=>
{
.
then
(({
EditorMarkdownPreviewExtension
:
MarkdownLivePreview
})
=>
{
this
.
editor
.
use
(
this
.
editor
.
use
({
new
MarkdownExtension
({
definition
:
MarkdownLivePreview
,
instance
:
this
.
editor
,
setupOptions
:
{
previewMarkdownPath
:
this
.
previewMarkdownPath
},
previewMarkdownPath
:
this
.
previewMarkdownPath
,
});
}),
);
})
})
.
catch
((
e
)
=>
.
catch
((
e
)
=>
createFlash
({
createFlash
({
...
...
app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
View file @
021bb329
...
@@ -19,7 +19,7 @@ export default {
...
@@ -19,7 +19,7 @@ export default {
if
(
this
.
glFeatures
.
schemaLinting
)
{
if
(
this
.
glFeatures
.
schemaLinting
)
{
const
editorInstance
=
this
.
$refs
.
editor
.
getEditor
();
const
editorInstance
=
this
.
$refs
.
editor
.
getEditor
();
editorInstance
.
use
(
new
CiSchemaExtension
({
instance
:
editorInstance
})
);
editorInstance
.
use
(
{
definition
:
CiSchemaExtension
}
);
editorInstance
.
registerCiSchema
();
editorInstance
.
registerCiSchema
();
}
}
},
},
...
...
spec/frontend/blob_edit/edit_blob_spec.js
View file @
021bb329
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
EditBlob
from
'
~/blob_edit/edit_blob
'
;
import
EditBlob
from
'
~/blob_edit/edit_blob
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
FileTemplateExtension
}
from
'
~/editor/extensions/source_editor_file_template_ext
'
;
import
{
FileTemplateExtension
}
from
'
~/editor/extensions/source_editor_file_template_ext
'
;
import
{
EditorMarkdownExtension
}
from
'
~/editor/extensions/source_editor_markdown_ext
'
;
import
{
EditorMarkdownExtension
}
from
'
~/editor/extensions/source_editor_markdown_ext
'
;
import
{
EditorMarkdownPreviewExtension
}
from
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
jest
.
mock
(
'
~/editor/source_editor
'
);
jest
.
mock
(
'
~/editor/source_editor
'
);
jest
.
mock
(
'
~/editor/extensions/source_editor_
markdown_ext
'
);
jest
.
mock
(
'
~/editor/extensions/source_editor_
extension_base
'
);
jest
.
mock
(
'
~/editor/extensions/source_editor_file_template_ext
'
);
jest
.
mock
(
'
~/editor/extensions/source_editor_file_template_ext
'
);
jest
.
mock
(
'
~/editor/extensions/source_editor_markdown_ext
'
);
jest
.
mock
(
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
);
const
PREVIEW_MARKDOWN_PATH
=
'
/foo/bar/preview_markdown
'
;
const
PREVIEW_MARKDOWN_PATH
=
'
/foo/bar/preview_markdown
'
;
const
defaultExtensions
=
[
{
definition
:
SourceEditorExtension
},
{
definition
:
FileTemplateExtension
},
];
const
markdownExtensions
=
[
{
definition
:
EditorMarkdownExtension
},
{
definition
:
EditorMarkdownPreviewExtension
,
setupOptions
:
{
previewMarkdownPath
:
PREVIEW_MARKDOWN_PATH
},
},
];
describe
(
'
Blob Editing
'
,
()
=>
{
describe
(
'
Blob Editing
'
,
()
=>
{
const
useMock
=
jest
.
fn
();
const
useMock
=
jest
.
fn
();
...
@@ -29,7 +44,9 @@ describe('Blob Editing', () => {
...
@@ -29,7 +44,9 @@ describe('Blob Editing', () => {
jest
.
spyOn
(
SourceEditor
.
prototype
,
'
createInstance
'
).
mockReturnValue
(
mockInstance
);
jest
.
spyOn
(
SourceEditor
.
prototype
,
'
createInstance
'
).
mockReturnValue
(
mockInstance
);
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
SourceEditorExtension
.
mockClear
();
EditorMarkdownExtension
.
mockClear
();
EditorMarkdownExtension
.
mockClear
();
EditorMarkdownPreviewExtension
.
mockClear
();
FileTemplateExtension
.
mockClear
();
FileTemplateExtension
.
mockClear
();
});
});
...
@@ -45,26 +62,22 @@ describe('Blob Editing', () => {
...
@@ -45,26 +62,22 @@ describe('Blob Editing', () => {
await
waitForPromises
();
await
waitForPromises
();
};
};
it
(
'
loads FileTemplateExtension by default
'
,
async
()
=>
{
it
(
'
loads
SourceEditorExtension and
FileTemplateExtension by default
'
,
async
()
=>
{
await
initEditor
();
await
initEditor
();
expect
(
useMock
).
toHaveBeenCalledWith
(
expect
.
any
(
FileTemplateExtension
));
expect
(
useMock
).
toHaveBeenCalledWith
(
defaultExtensions
);
expect
(
FileTemplateExtension
).
toHaveBeenCalledTimes
(
1
);
});
});
describe
(
'
Markdown
'
,
()
=>
{
describe
(
'
Markdown
'
,
()
=>
{
it
(
'
does not load MarkdownExtension by default
'
,
async
()
=>
{
it
(
'
does not load MarkdownExtension
s
by default
'
,
async
()
=>
{
await
initEditor
();
await
initEditor
();
expect
(
EditorMarkdownExtension
).
not
.
toHaveBeenCalled
();
expect
(
EditorMarkdownExtension
).
not
.
toHaveBeenCalled
();
expect
(
EditorMarkdownPreviewExtension
).
not
.
toHaveBeenCalled
();
});
});
it
(
'
loads MarkdownExtension only for the markdown files
'
,
async
()
=>
{
it
(
'
loads MarkdownExtension only for the markdown files
'
,
async
()
=>
{
await
initEditor
(
true
);
await
initEditor
(
true
);
expect
(
useMock
).
toHaveBeenCalledWith
(
expect
.
any
(
EditorMarkdownExtension
));
expect
(
useMock
).
toHaveBeenCalledTimes
(
2
);
expect
(
EditorMarkdownExtension
).
toHaveBeenCalledTimes
(
1
);
expect
(
useMock
.
mock
.
calls
[
1
]).
toEqual
([
markdownExtensions
]);
expect
(
EditorMarkdownExtension
).
toHaveBeenCalledWith
({
instance
:
mockInstance
,
previewMarkdownPath
:
PREVIEW_MARKDOWN_PATH
,
});
});
});
});
});
...
...
spec/frontend/editor/helpers.js
View file @
021bb329
/* eslint-disable max-classes-per-file */
// Helpers
export
const
spyOnApi
=
(
extension
,
spiesObj
=
{})
=>
{
const
origApi
=
extension
.
api
;
if
(
extension
?.
obj
)
{
jest
.
spyOn
(
extension
.
obj
,
'
provides
'
).
mockReturnValue
({
...
origApi
,
...
spiesObj
,
});
}
};
// Dummy Extensions
export
class
SEClassExtension
{
export
class
SEClassExtension
{
static
get
extensionName
()
{
return
'
SEClassExtension
'
;
}
// eslint-disable-next-line class-methods-use-this
// eslint-disable-next-line class-methods-use-this
provides
()
{
provides
()
{
return
{
return
{
...
@@ -10,6 +28,7 @@ export class SEClassExtension {
...
@@ -10,6 +28,7 @@ export class SEClassExtension {
export
function
SEFnExtension
()
{
export
function
SEFnExtension
()
{
return
{
return
{
extensionName
:
'
SEFnExtension
'
,
fnExtMethod
:
()
=>
'
fn own method
'
,
fnExtMethod
:
()
=>
'
fn own method
'
,
provides
:
()
=>
{
provides
:
()
=>
{
return
{
return
{
...
@@ -21,6 +40,7 @@ export function SEFnExtension() {
...
@@ -21,6 +40,7 @@ export function SEFnExtension() {
export
const
SEConstExt
=
()
=>
{
export
const
SEConstExt
=
()
=>
{
return
{
return
{
extensionName
:
'
SEConstExt
'
,
provides
:
()
=>
{
provides
:
()
=>
{
return
{
return
{
constExtMethod
:
()
=>
'
const own method
'
,
constExtMethod
:
()
=>
'
const own method
'
,
...
@@ -29,9 +49,12 @@ export const SEConstExt = () => {
...
@@ -29,9 +49,12 @@ export const SEConstExt = () => {
};
};
};
};
export
function
SEWithSetupExt
()
{
export
class
SEWithSetupExt
{
return
{
static
get
extensionName
()
{
onSetup
:
(
instance
,
setupOptions
=
{})
=>
{
return
'
SEWithSetupExt
'
;
}
// eslint-disable-next-line class-methods-use-this
onSetup
(
instance
,
setupOptions
=
{})
{
if
(
setupOptions
&&
!
Array
.
isArray
(
setupOptions
))
{
if
(
setupOptions
&&
!
Array
.
isArray
(
setupOptions
))
{
Object
.
entries
(
setupOptions
).
forEach
(([
key
,
value
])
=>
{
Object
.
entries
(
setupOptions
).
forEach
(([
key
,
value
])
=>
{
Object
.
assign
(
instance
,
{
Object
.
assign
(
instance
,
{
...
@@ -39,8 +62,8 @@ export function SEWithSetupExt() {
...
@@ -39,8 +62,8 @@ export function SEWithSetupExt() {
});
});
});
});
}
}
},
}
provides
:
()
=>
{
provides
()
{
return
{
return
{
returnInstanceAndProps
:
(
instance
,
stringProp
,
objProp
=
{})
=>
{
returnInstanceAndProps
:
(
instance
,
stringProp
,
objProp
=
{})
=>
{
return
[
stringProp
,
objProp
,
instance
];
return
[
stringProp
,
objProp
,
instance
];
...
@@ -52,13 +75,13 @@ export function SEWithSetupExt() {
...
@@ -52,13 +75,13 @@ export function SEWithSetupExt() {
return
this
;
return
this
;
},
},
};
};
},
}
};
}
}
export
const
conflictingExtensions
=
{
export
const
conflictingExtensions
=
{
WithInstanceExt
:
()
=>
{
WithInstanceExt
:
()
=>
{
return
{
return
{
extensionName
:
'
WithInstanceExt
'
,
provides
:
()
=>
{
provides
:
()
=>
{
return
{
return
{
use
:
()
=>
'
A conflict with instance
'
,
use
:
()
=>
'
A conflict with instance
'
,
...
@@ -69,6 +92,7 @@ export const conflictingExtensions = {
...
@@ -69,6 +92,7 @@ export const conflictingExtensions = {
},
},
WithAnotherExt
:
()
=>
{
WithAnotherExt
:
()
=>
{
return
{
return
{
extensionName
:
'
WithAnotherExt
'
,
provides
:
()
=>
{
provides
:
()
=>
{
return
{
return
{
shared
:
()
=>
'
A conflict with extension
'
,
shared
:
()
=>
'
A conflict with extension
'
,
...
...
spec/frontend/editor/source_editor_ci_schema_ext_spec.js
View file @
021bb329
...
@@ -23,7 +23,7 @@ describe('~/editor/editor_ci_config_ext', () => {
...
@@ -23,7 +23,7 @@ describe('~/editor/editor_ci_config_ext', () => {
blobPath
,
blobPath
,
blobContent
:
''
,
blobContent
:
''
,
});
});
instance
.
use
(
new
CiSchemaExtension
()
);
instance
.
use
(
{
definition
:
CiSchemaExtension
}
);
};
};
beforeAll
(()
=>
{
beforeAll
(()
=>
{
...
...
spec/frontend/editor/source_editor_extension_base_spec.js
View file @
021bb329
...
@@ -2,40 +2,25 @@ import { Range } from 'monaco-editor';
...
@@ -2,40 +2,25 @@ import { Range } from 'monaco-editor';
import
{
useFakeRequestAnimationFrame
}
from
'
helpers/fake_request_animation_frame
'
;
import
{
useFakeRequestAnimationFrame
}
from
'
helpers/fake_request_animation_frame
'
;
import
setWindowLocation
from
'
helpers/set_window_location_helper
'
;
import
setWindowLocation
from
'
helpers/set_window_location_helper
'
;
import
{
import
{
ERROR_INSTANCE_REQUIRED_FOR_EXTENSION
,
EDITOR_TYPE_CODE
,
EDITOR_TYPE_CODE
,
EDITOR_TYPE_DIFF
,
EDITOR_TYPE_DIFF
,
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
,
EXTENSION_BASE_LINE_NUMBERS_CLASS
,
}
from
'
~/editor/constants
'
;
}
from
'
~/editor/constants
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
EditorInstance
from
'
~/editor/source_editor_instance
'
;
jest
.
mock
(
'
~/helpers/startup_css_helper
'
,
()
=>
{
return
{
waitForCSSLoaded
:
jest
.
fn
().
mockImplementation
((
cb
)
=>
{
// We have to artificially put the callback's execution
// to the end of the current call stack to be able to
// test that the callback is called after waitForCSSLoaded.
// setTimeout with 0 delay does exactly that.
// Otherwise we might end up with false positive results
setTimeout
(()
=>
{
cb
.
apply
();
},
0
);
}),
};
});
describe
(
'
The basis for an Source Editor extension
'
,
()
=>
{
describe
(
'
The basis for an Source Editor extension
'
,
()
=>
{
const
defaultLine
=
3
;
const
defaultLine
=
3
;
let
ext
;
let
event
;
let
event
;
const
defaultOptions
=
{
foo
:
'
bar
'
};
const
findLine
=
(
num
)
=>
{
const
findLine
=
(
num
)
=>
{
return
document
.
querySelector
(
`.
line-numbers
:nth-child(
${
num
}
)`
);
return
document
.
querySelector
(
`.
${
EXTENSION_BASE_LINE_NUMBERS_CLASS
}
:nth-child(
${
num
}
)`
);
};
};
const
generateLines
=
()
=>
{
const
generateLines
=
()
=>
{
let
res
=
''
;
let
res
=
''
;
for
(
let
line
=
1
,
lines
=
5
;
line
<=
lines
;
line
+=
1
)
{
for
(
let
line
=
1
,
lines
=
5
;
line
<=
lines
;
line
+=
1
)
{
res
+=
`<div class="
line-numbers
">
${
line
}
</div>`
;
res
+=
`<div class="
${
EXTENSION_BASE_LINE_NUMBERS_CLASS
}
">
${
line
}
</div>`
;
}
}
return
res
;
return
res
;
};
};
...
@@ -49,6 +34,9 @@ describe('The basis for an Source Editor extension', () => {
...
@@ -49,6 +34,9 @@ describe('The basis for an Source Editor extension', () => {
},
},
};
};
};
};
const
createInstance
=
(
baseInstance
=
{})
=>
{
return
new
EditorInstance
(
baseInstance
);
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
setFixtures
(
generateLines
());
setFixtures
(
generateLines
());
...
@@ -59,95 +47,47 @@ describe('The basis for an Source Editor extension', () => {
...
@@ -59,95 +47,47 @@ describe('The basis for an Source Editor extension', () => {
jest
.
clearAllMocks
();
jest
.
clearAllMocks
();
});
});
describe
(
'
constructor
'
,
()
=>
{
describe
(
'
onUse callback
'
,
()
=>
{
it
(
'
resets the layout in waitForCSSLoaded callback
'
,
async
()
=>
{
const
instance
=
{
layout
:
jest
.
fn
(),
};
ext
=
new
SourceEditorExtension
({
instance
});
expect
(
instance
.
layout
).
not
.
toHaveBeenCalled
();
// We're waiting for the waitForCSSLoaded mock to kick in
await
jest
.
runOnlyPendingTimers
();
expect
(
instance
.
layout
).
toHaveBeenCalled
();
});
it
.
each
`
description | instance | options
${
'
accepts configuration options and instance
'
}
|
${{}}
|
$
{
defaultOptions
}
${
'
leaves instance intact if no options are passed
'
}
|
${{}}
|
$
{
undefined
}
${
'
does not fail if both instance and the options are omitted
'
}
|
${
undefined
}
|
${
undefined
}
${
'
throws if only options are passed
'
}
|
${
undefined
}
|
${
defaultOptions
}
`
(
'
$description
'
,
({
instance
,
options
}
=
{})
=>
{
SourceEditorExtension
.
deferRerender
=
jest
.
fn
();
const
originalInstance
=
{
...
instance
};
if
(
instance
)
{
if
(
options
)
{
Object
.
entries
(
options
).
forEach
((
prop
)
=>
{
expect
(
instance
[
prop
]).
toBeUndefined
();
});
// Both instance and options are passed
ext
=
new
SourceEditorExtension
({
instance
,
...
options
});
Object
.
entries
(
options
).
forEach
(([
prop
,
value
])
=>
{
expect
(
ext
[
prop
]).
toBeUndefined
();
expect
(
instance
[
prop
]).
toBe
(
value
);
});
}
else
{
ext
=
new
SourceEditorExtension
({
instance
});
expect
(
instance
).
toEqual
(
originalInstance
);
}
}
else
if
(
options
)
{
// Options are passed without instance
expect
(()
=>
{
ext
=
new
SourceEditorExtension
({
...
options
});
}).
toThrow
(
ERROR_INSTANCE_REQUIRED_FOR_EXTENSION
);
}
else
{
// Neither options nor instance are passed
expect
(()
=>
{
ext
=
new
SourceEditorExtension
();
}).
not
.
toThrow
();
}
});
it
(
'
initializes the line highlighting
'
,
()
=>
{
it
(
'
initializes the line highlighting
'
,
()
=>
{
SourceEditorExtension
.
deferRerender
=
jest
.
fn
();
const
instance
=
createInstance
();
const
spy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
highlightLines
'
);
const
spy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
highlightLines
'
);
ext
=
new
SourceEditorExtension
({
instance
:
{}
});
instance
.
use
({
definition
:
SourceEditorExtension
});
expect
(
spy
).
toHaveBeenCalled
();
expect
(
spy
).
toHaveBeenCalled
();
});
});
it
(
'
sets up the line linking for code instance
'
,
()
=>
{
it
.
each
`
SourceEditorExtension
.
deferRerender
=
jest
.
fn
();
description | instanceType | shouldBeCalled
const
spy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
setupLineLinking
'
);
${
'
Sets up
'
}
|
${
EDITOR_TYPE_CODE
}
|
${
true
}
const
instance
=
{
${
'
Does not set up
'
}
|
${
EDITOR_TYPE_DIFF
}
|
${
false
}
getEditorType
:
jest
.
fn
().
mockReturnValue
(
EDITOR_TYPE_CODE
),
`
(
'
$description the line linking for $instanceType instance
'
,
({
instanceType
,
shouldBeCalled
})
=>
{
const
instance
=
createInstance
({
getEditorType
:
jest
.
fn
().
mockReturnValue
(
instanceType
),
onMouseMove
:
jest
.
fn
(),
onMouseMove
:
jest
.
fn
(),
onMouseDown
:
jest
.
fn
(),
onMouseDown
:
jest
.
fn
(),
};
ext
=
new
SourceEditorExtension
({
instance
});
expect
(
spy
).
toHaveBeenCalledWith
(
instance
);
});
});
it
(
'
does not set up the line linking for diff instance
'
,
()
=>
{
SourceEditorExtension
.
deferRerender
=
jest
.
fn
();
const
spy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
setupLineLinking
'
);
const
spy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
setupLineLinking
'
);
const
instance
=
{
getEditorType
:
jest
.
fn
().
mockReturnValue
(
EDITOR_TYPE_DIFF
),
instance
.
use
({
definition
:
SourceEditorExtension
});
};
if
(
shouldBeCalled
)
{
ext
=
new
SourceEditorExtension
({
instance
});
expect
(
spy
).
toHaveBeenCalledWith
(
instance
);
}
else
{
expect
(
spy
).
not
.
toHaveBeenCalled
();
expect
(
spy
).
not
.
toHaveBeenCalled
();
});
}
},
);
});
});
describe
(
'
highlightLines
'
,
()
=>
{
describe
(
'
highlightLines
'
,
()
=>
{
const
revealSpy
=
jest
.
fn
();
const
revealSpy
=
jest
.
fn
();
const
decorationsSpy
=
jest
.
fn
();
const
decorationsSpy
=
jest
.
fn
();
const
instance
=
{
const
instance
=
createInstance
(
{
revealLineInCenter
:
revealSpy
,
revealLineInCenter
:
revealSpy
,
deltaDecorations
:
decorationsSpy
,
deltaDecorations
:
decorationsSpy
,
};
});
instance
.
use
({
definition
:
SourceEditorExtension
});
const
defaultDecorationOptions
=
{
const
defaultDecorationOptions
=
{
isWholeLine
:
true
,
isWholeLine
:
true
,
className
:
'
active-line-text
'
,
className
:
'
active-line-text
'
,
...
@@ -175,7 +115,7 @@ describe('The basis for an Source Editor extension', () => {
...
@@ -175,7 +115,7 @@ describe('The basis for an Source Editor extension', () => {
${
'
uses bounds if both hash and bounds exist
'
}
|
${
'
#L7-42
'
}
|
${[
3
,
5
]}
|
${
true
}
|
${[
3
,
1
,
5
,
1
]}
${
'
uses bounds if both hash and bounds exist
'
}
|
${
'
#L7-42
'
}
|
${[
3
,
5
]}
|
${
true
}
|
${[
3
,
1
,
5
,
1
]}
`
(
'
$desc
'
,
({
hash
,
bounds
,
shouldReveal
,
expectedRange
}
=
{})
=>
{
`
(
'
$desc
'
,
({
hash
,
bounds
,
shouldReveal
,
expectedRange
}
=
{})
=>
{
window
.
location
.
hash
=
hash
;
window
.
location
.
hash
=
hash
;
SourceEditorExtension
.
highlightLines
(
instance
,
bounds
);
instance
.
highlightLines
(
bounds
);
if
(
!
shouldReveal
)
{
if
(
!
shouldReveal
)
{
expect
(
revealSpy
).
not
.
toHaveBeenCalled
();
expect
(
revealSpy
).
not
.
toHaveBeenCalled
();
expect
(
decorationsSpy
).
not
.
toHaveBeenCalled
();
expect
(
decorationsSpy
).
not
.
toHaveBeenCalled
();
...
@@ -197,7 +137,7 @@ describe('The basis for an Source Editor extension', () => {
...
@@ -197,7 +137,7 @@ describe('The basis for an Source Editor extension', () => {
decorationsSpy
.
mockReturnValue
(
'
foo
'
);
decorationsSpy
.
mockReturnValue
(
'
foo
'
);
window
.
location
.
hash
=
'
#L10
'
;
window
.
location
.
hash
=
'
#L10
'
;
expect
(
instance
.
lineDecorations
).
toBeUndefined
();
expect
(
instance
.
lineDecorations
).
toBeUndefined
();
SourceEditorExtension
.
highlightLines
(
instance
);
instance
.
highlightLines
(
);
expect
(
instance
.
lineDecorations
).
toBe
(
'
foo
'
);
expect
(
instance
.
lineDecorations
).
toBe
(
'
foo
'
);
});
});
...
@@ -215,7 +155,7 @@ describe('The basis for an Source Editor extension', () => {
...
@@ -215,7 +155,7 @@ describe('The basis for an Source Editor extension', () => {
},
},
];
];
instance
.
lineDecorations
=
oldLineDecorations
;
instance
.
lineDecorations
=
oldLineDecorations
;
SourceEditorExtension
.
highlightLines
(
instance
,
[
7
,
10
]);
instance
.
highlightLines
(
[
7
,
10
]);
expect
(
decorationsSpy
).
toHaveBeenCalledWith
(
oldLineDecorations
,
newLineDecorations
);
expect
(
decorationsSpy
).
toHaveBeenCalledWith
(
oldLineDecorations
,
newLineDecorations
);
});
});
});
});
...
@@ -228,13 +168,18 @@ describe('The basis for an Source Editor extension', () => {
...
@@ -228,13 +168,18 @@ describe('The basis for an Source Editor extension', () => {
options
:
{
isWholeLine
:
true
,
className
:
'
active-line-text
'
},
options
:
{
isWholeLine
:
true
,
className
:
'
active-line-text
'
},
},
},
];
];
const
instance
=
{
let
instance
;
beforeEach
(()
=>
{
instance
=
createInstance
({
deltaDecorations
:
decorationsSpy
,
deltaDecorations
:
decorationsSpy
,
lineDecorations
,
lineDecorations
,
};
});
instance
.
use
({
definition
:
SourceEditorExtension
});
});
it
(
'
removes all existing decorations
'
,
()
=>
{
it
(
'
removes all existing decorations
'
,
()
=>
{
SourceEditorExtension
.
removeHighlights
(
instance
);
instance
.
removeHighlights
(
);
expect
(
decorationsSpy
).
toHaveBeenCalledWith
(
lineDecorations
,
[]);
expect
(
decorationsSpy
).
toHaveBeenCalledWith
(
lineDecorations
,
[]);
});
});
});
});
...
@@ -263,7 +208,7 @@ describe('The basis for an Source Editor extension', () => {
...
@@ -263,7 +208,7 @@ describe('The basis for an Source Editor extension', () => {
it
.
each
`
it
.
each
`
desc | eventTrigger | shouldRemove
desc | eventTrigger | shouldRemove
${
'
does not remove the line decorations if the event is triggered on a wrong node
'
}
|
${
null
}
|
${
false
}
${
'
does not remove the line decorations if the event is triggered on a wrong node
'
}
|
${
null
}
|
${
false
}
${
'
removes existing line decorations when clicking a line number
'
}
|
${
'
.link-anchor
'
}
|
${
true
}
${
'
removes existing line decorations when clicking a line number
'
}
|
${
`.
${
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
}
`
}
|
${
true
}
`
(
'
$desc
'
,
({
eventTrigger
,
shouldRemove
}
=
{})
=>
{
`
(
'
$desc
'
,
({
eventTrigger
,
shouldRemove
}
=
{})
=>
{
event
=
generateEventMock
({
el
:
eventTrigger
?
document
.
querySelector
(
eventTrigger
)
:
null
});
event
=
generateEventMock
({
el
:
eventTrigger
?
document
.
querySelector
(
eventTrigger
)
:
null
});
instance
.
onMouseDown
.
mockImplementation
((
fn
)
=>
{
instance
.
onMouseDown
.
mockImplementation
((
fn
)
=>
{
...
...
spec/frontend/editor/source_editor_extension_spec.js
View file @
021bb329
...
@@ -40,7 +40,7 @@ describe('Editor Extension', () => {
...
@@ -40,7 +40,7 @@ describe('Editor Extension', () => {
expect
(
extension
).
toEqual
(
expect
(
extension
).
toEqual
(
expect
.
objectContaining
({
expect
.
objectContaining
({
n
ame
:
expectedName
,
extensionN
ame
:
expectedName
,
setupOptions
,
setupOptions
,
}),
}),
);
);
...
...
spec/frontend/editor/source_editor_instance_spec.js
View file @
021bb329
...
@@ -32,11 +32,17 @@ describe('Source Editor Instance', () => {
...
@@ -32,11 +32,17 @@ describe('Source Editor Instance', () => {
];
];
const
fooFn
=
jest
.
fn
();
const
fooFn
=
jest
.
fn
();
const
fooProp
=
'
foo
'
;
class
DummyExt
{
class
DummyExt
{
// eslint-disable-next-line class-methods-use-this
get
extensionName
()
{
return
'
DummyExt
'
;
}
// eslint-disable-next-line class-methods-use-this
// eslint-disable-next-line class-methods-use-this
provides
()
{
provides
()
{
return
{
return
{
fooFn
,
fooFn
,
fooProp
,
};
};
}
}
}
}
...
@@ -64,7 +70,7 @@ describe('Source Editor Instance', () => {
...
@@ -64,7 +70,7 @@ describe('Source Editor Instance', () => {
});
});
describe
(
'
proxy
'
,
()
=>
{
describe
(
'
proxy
'
,
()
=>
{
it
(
'
returns
prop
from an extension if extension provides it
'
,
()
=>
{
it
(
'
returns
a method
from an extension if extension provides it
'
,
()
=>
{
seInstance
=
new
SourceEditorInstance
();
seInstance
=
new
SourceEditorInstance
();
seInstance
.
use
({
definition
:
DummyExt
});
seInstance
.
use
({
definition
:
DummyExt
});
...
@@ -73,6 +79,13 @@ describe('Source Editor Instance', () => {
...
@@ -73,6 +79,13 @@ describe('Source Editor Instance', () => {
expect
(
fooFn
).
toHaveBeenCalled
();
expect
(
fooFn
).
toHaveBeenCalled
();
});
});
it
(
'
returns a prop from an extension if extension provides it
'
,
()
=>
{
seInstance
=
new
SourceEditorInstance
();
seInstance
.
use
({
definition
:
DummyExt
});
expect
(
seInstance
.
fooProp
).
toBe
(
'
foo
'
);
});
it
.
each
`
it
.
each
`
stringPropToPass | objPropToPass | setupOptions
stringPropToPass | objPropToPass | setupOptions
${
undefined
}
|
${
undefined
}
|
${
undefined
}
${
undefined
}
|
${
undefined
}
|
${
undefined
}
...
@@ -118,20 +131,20 @@ describe('Source Editor Instance', () => {
...
@@ -118,20 +131,20 @@ describe('Source Editor Instance', () => {
it
(
"
correctly sets the context of the 'this' keyword for the extension's methods
"
,
()
=>
{
it
(
"
correctly sets the context of the 'this' keyword for the extension's methods
"
,
()
=>
{
seInstance
=
new
SourceEditorInstance
();
seInstance
=
new
SourceEditorInstance
();
seInstance
.
use
({
definition
:
SEWithSetupExt
});
const
extension
=
seInstance
.
use
({
definition
:
SEWithSetupExt
});
expect
(
seInstance
.
giveMeContext
()
.
constructor
).
toEqual
(
SEWithSetupExt
);
expect
(
seInstance
.
giveMeContext
()
).
toEqual
(
extension
.
obj
);
});
});
it
(
'
returns props from SE instance itself if no extension provides the prop
'
,
()
=>
{
it
(
'
returns props from SE instance itself if no extension provides the prop
'
,
()
=>
{
seInstance
=
new
SourceEditorInstance
({
seInstance
=
new
SourceEditorInstance
({
use
:
fooFn
,
use
:
fooFn
,
});
});
jest
.
spyOn
(
seInstanc
e
,
'
use
'
).
mockImplementation
(()
=>
{});
const
spy
=
jest
.
spyOn
(
seInstance
.
constructor
.
prototyp
e
,
'
use
'
).
mockImplementation
(()
=>
{});
expect
(
s
eInstance
.
use
).
not
.
toHaveBeenCalled
();
expect
(
s
py
).
not
.
toHaveBeenCalled
();
expect
(
fooFn
).
not
.
toHaveBeenCalled
();
expect
(
fooFn
).
not
.
toHaveBeenCalled
();
seInstance
.
use
();
seInstance
.
use
();
expect
(
s
eInstance
.
use
).
toHaveBeenCalled
();
expect
(
s
py
).
toHaveBeenCalled
();
expect
(
fooFn
).
not
.
toHaveBeenCalled
();
expect
(
fooFn
).
not
.
toHaveBeenCalled
();
});
});
...
...
spec/frontend/editor/source_editor_markdown_ext_spec.js
View file @
021bb329
...
@@ -9,7 +9,6 @@ describe('Markdown Extension for Source Editor', () => {
...
@@ -9,7 +9,6 @@ describe('Markdown Extension for Source Editor', () => {
let
instance
;
let
instance
;
let
editorEl
;
let
editorEl
;
let
mockAxios
;
let
mockAxios
;
const
previewMarkdownPath
=
'
/gitlab/fooGroup/barProj/preview_markdown
'
;
const
firstLine
=
'
This is a
'
;
const
firstLine
=
'
This is a
'
;
const
secondLine
=
'
multiline
'
;
const
secondLine
=
'
multiline
'
;
const
thirdLine
=
'
string with some **markup**
'
;
const
thirdLine
=
'
string with some **markup**
'
;
...
@@ -36,7 +35,7 @@ describe('Markdown Extension for Source Editor', () => {
...
@@ -36,7 +35,7 @@ describe('Markdown Extension for Source Editor', () => {
blobPath
:
markdownPath
,
blobPath
:
markdownPath
,
blobContent
:
text
,
blobContent
:
text
,
});
});
instance
.
use
(
new
EditorMarkdownExtension
({
instance
,
previewMarkdownPath
})
);
instance
.
use
(
{
definition
:
EditorMarkdownExtension
}
);
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -164,13 +163,11 @@ describe('Markdown Extension for Source Editor', () => {
...
@@ -164,13 +163,11 @@ describe('Markdown Extension for Source Editor', () => {
});
});
it
(
'
does not fail when only `toSelect` is supplied and fetches the text from selection
'
,
()
=>
{
it
(
'
does not fail when only `toSelect` is supplied and fetches the text from selection
'
,
()
=>
{
jest
.
spyOn
(
instance
,
'
getSelectedText
'
);
const
toSelect
=
'
string
'
;
const
toSelect
=
'
string
'
;
selectSecondAndThirdLines
();
selectSecondAndThirdLines
();
instance
.
selectWithinSelection
(
toSelect
);
instance
.
selectWithinSelection
(
toSelect
);
expect
(
instance
.
getSelectedText
).
toHaveBeenCalled
();
expect
(
selectionToString
()).
toBe
(
`[3,1 -> 3,
${
toSelect
.
length
+
1
}
]`
);
expect
(
selectionToString
()).
toBe
(
`[3,1 -> 3,
${
toSelect
.
length
+
1
}
]`
);
});
});
...
...
spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
View file @
021bb329
...
@@ -13,6 +13,7 @@ import SourceEditor from '~/editor/source_editor';
...
@@ -13,6 +13,7 @@ import SourceEditor from '~/editor/source_editor';
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
{
spyOnApi
}
from
'
./helpers
'
;
jest
.
mock
(
'
~/syntax_highlight
'
);
jest
.
mock
(
'
~/syntax_highlight
'
);
jest
.
mock
(
'
~/flash
'
);
jest
.
mock
(
'
~/flash
'
);
...
@@ -23,6 +24,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -23,6 +24,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let
editorEl
;
let
editorEl
;
let
panelSpy
;
let
panelSpy
;
let
mockAxios
;
let
mockAxios
;
let
extension
;
const
previewMarkdownPath
=
'
/gitlab/fooGroup/barProj/preview_markdown
'
;
const
previewMarkdownPath
=
'
/gitlab/fooGroup/barProj/preview_markdown
'
;
const
firstLine
=
'
This is a
'
;
const
firstLine
=
'
This is a
'
;
const
secondLine
=
'
multiline
'
;
const
secondLine
=
'
multiline
'
;
...
@@ -47,8 +49,11 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -47,8 +49,11 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
blobPath
:
markdownPath
,
blobPath
:
markdownPath
,
blobContent
:
text
,
blobContent
:
text
,
});
});
instance
.
use
(
new
EditorMarkdownPreviewExtension
({
instance
,
previewMarkdownPath
}));
extension
=
instance
.
use
({
panelSpy
=
jest
.
spyOn
(
EditorMarkdownPreviewExtension
,
'
togglePreviewPanel
'
);
definition
:
EditorMarkdownPreviewExtension
,
setupOptions
:
{
previewMarkdownPath
},
});
panelSpy
=
jest
.
spyOn
(
extension
.
obj
.
constructor
.
prototype
,
'
togglePreviewPanel
'
);
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -57,14 +62,14 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -57,14 +62,14 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios
.
restore
();
mockAxios
.
restore
();
});
});
it
(
'
sets up the instance
'
,
()
=>
{
it
(
'
sets up the
preview on the
instance
'
,
()
=>
{
expect
(
instance
.
p
review
).
toEqual
({
expect
(
instance
.
markdownP
review
).
toEqual
({
el
:
undefined
,
el
:
undefined
,
action
:
expect
.
any
(
Object
),
action
:
expect
.
any
(
Object
),
shown
:
false
,
shown
:
false
,
modelChangeListener
:
undefined
,
modelChangeListener
:
undefined
,
path
:
previewMarkdownPath
,
});
});
expect
(
instance
.
previewMarkdownPath
).
toBe
(
previewMarkdownPath
);
});
});
describe
(
'
model language changes listener
'
,
()
=>
{
describe
(
'
model language changes listener
'
,
()
=>
{
...
@@ -72,14 +77,22 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -72,14 +77,22 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let
actionSpy
;
let
actionSpy
;
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
cleanupSpy
=
jest
.
spyOn
(
instance
,
'
cleanup
'
);
cleanupSpy
=
jest
.
fn
();
actionSpy
=
jest
.
spyOn
(
instance
,
'
setupPreviewAction
'
);
actionSpy
=
jest
.
fn
();
spyOnApi
(
extension
,
{
cleanup
:
cleanupSpy
,
setupPreviewAction
:
actionSpy
,
});
await
togglePreview
();
await
togglePreview
();
});
});
afterEach
(()
=>
{
jest
.
clearAllMocks
();
});
it
(
'
cleans up when switching away from markdown
'
,
()
=>
{
it
(
'
cleans up when switching away from markdown
'
,
()
=>
{
expect
(
instance
.
cleanup
).
not
.
toHaveBeenCalled
();
expect
(
cleanupSpy
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
setupPreviewAction
).
not
.
toHaveBeenCalled
();
expect
(
actionSpy
).
not
.
toHaveBeenCalled
();
instance
.
updateModelLanguage
(
plaintextPath
);
instance
.
updateModelLanguage
(
plaintextPath
);
...
@@ -110,8 +123,12 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -110,8 +123,12 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let
actionSpy
;
let
actionSpy
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
cleanupSpy
=
jest
.
spyOn
(
instance
,
'
cleanup
'
);
cleanupSpy
=
jest
.
fn
();
actionSpy
=
jest
.
spyOn
(
instance
,
'
setupPreviewAction
'
);
actionSpy
=
jest
.
fn
();
spyOnApi
(
extension
,
{
cleanup
:
cleanupSpy
,
setupPreviewAction
:
actionSpy
,
});
instance
.
togglePreview
();
instance
.
togglePreview
();
});
});
...
@@ -153,14 +170,17 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -153,14 +170,17 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
});
it
(
'
disposes the modelChange listener and does not fetch preview on content changes
'
,
()
=>
{
it
(
'
disposes the modelChange listener and does not fetch preview on content changes
'
,
()
=>
{
expect
(
instance
.
preview
.
modelChangeListener
).
toBeDefined
();
expect
(
instance
.
markdownPreview
.
modelChangeListener
).
toBeDefined
();
jest
.
spyOn
(
instance
,
'
fetchPreview
'
);
const
fetchPreviewSpy
=
jest
.
fn
();
spyOnApi
(
extension
,
{
fetchPreview
:
fetchPreviewSpy
,
});
instance
.
cleanup
();
instance
.
cleanup
();
instance
.
setValue
(
'
Foo Bar
'
);
instance
.
setValue
(
'
Foo Bar
'
);
jest
.
advanceTimersByTime
(
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
);
jest
.
advanceTimersByTime
(
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
);
expect
(
instance
.
fetchPreview
).
not
.
toHaveBeenCalled
();
expect
(
fetchPreviewSpy
).
not
.
toHaveBeenCalled
();
});
});
it
(
'
removes the contextual menu action
'
,
()
=>
{
it
(
'
removes the contextual menu action
'
,
()
=>
{
...
@@ -172,13 +192,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -172,13 +192,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
});
it
(
'
toggles the `shown` flag
'
,
()
=>
{
it
(
'
toggles the `shown` flag
'
,
()
=>
{
expect
(
instance
.
p
review
.
shown
).
toBe
(
true
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
true
);
instance
.
cleanup
();
instance
.
cleanup
();
expect
(
instance
.
p
review
.
shown
).
toBe
(
false
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
false
);
});
});
it
(
'
toggles the panel only if the preview is visible
'
,
()
=>
{
it
(
'
toggles the panel only if the preview is visible
'
,
()
=>
{
const
{
el
:
previewEl
}
=
instance
.
p
review
;
const
{
el
:
previewEl
}
=
instance
.
markdownP
review
;
const
parentEl
=
previewEl
.
parentElement
;
const
parentEl
=
previewEl
.
parentElement
;
expect
(
previewEl
).
toBeVisible
();
expect
(
previewEl
).
toBeVisible
();
...
@@ -200,7 +220,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -200,7 +220,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it
(
'
toggles the layout only if the preview is visible
'
,
()
=>
{
it
(
'
toggles the layout only if the preview is visible
'
,
()
=>
{
const
{
width
}
=
instance
.
getLayoutInfo
();
const
{
width
}
=
instance
.
getLayoutInfo
();
expect
(
instance
.
p
review
.
shown
).
toBe
(
true
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
true
);
instance
.
cleanup
();
instance
.
cleanup
();
...
@@ -234,13 +254,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -234,13 +254,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
});
it
(
'
puts the fetched content into the preview DOM element
'
,
async
()
=>
{
it
(
'
puts the fetched content into the preview DOM element
'
,
async
()
=>
{
instance
.
p
review
.
el
=
editorEl
.
parentElement
;
instance
.
markdownP
review
.
el
=
editorEl
.
parentElement
;
await
fetchPreview
();
await
fetchPreview
();
expect
(
instance
.
p
review
.
el
.
innerHTML
).
toEqual
(
responseData
);
expect
(
instance
.
markdownP
review
.
el
.
innerHTML
).
toEqual
(
responseData
);
});
});
it
(
'
applies syntax highlighting to the preview content
'
,
async
()
=>
{
it
(
'
applies syntax highlighting to the preview content
'
,
async
()
=>
{
instance
.
p
review
.
el
=
editorEl
.
parentElement
;
instance
.
markdownP
review
.
el
=
editorEl
.
parentElement
;
await
fetchPreview
();
await
fetchPreview
();
expect
(
syntaxHighlight
).
toHaveBeenCalled
();
expect
(
syntaxHighlight
).
toHaveBeenCalled
();
});
});
...
@@ -266,14 +286,17 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -266,14 +286,17 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
});
it
(
'
toggles preview when the action is triggered
'
,
()
=>
{
it
(
'
toggles preview when the action is triggered
'
,
()
=>
{
jest
.
spyOn
(
instance
,
'
togglePreview
'
).
mockImplementation
();
const
togglePreviewSpy
=
jest
.
fn
();
spyOnApi
(
extension
,
{
togglePreview
:
togglePreviewSpy
,
});
expect
(
instance
.
togglePreview
).
not
.
toHaveBeenCalled
();
expect
(
togglePreviewSpy
).
not
.
toHaveBeenCalled
();
const
action
=
instance
.
getAction
(
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID
);
const
action
=
instance
.
getAction
(
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID
);
action
.
run
();
action
.
run
();
expect
(
instance
.
togglePreview
).
toHaveBeenCalled
();
expect
(
togglePreviewSpy
).
toHaveBeenCalled
();
});
});
});
});
...
@@ -283,39 +306,39 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -283,39 +306,39 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
});
it
(
'
toggles preview flag on instance
'
,
()
=>
{
it
(
'
toggles preview flag on instance
'
,
()
=>
{
expect
(
instance
.
p
review
.
shown
).
toBe
(
false
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
false
);
instance
.
togglePreview
();
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
shown
).
toBe
(
true
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
true
);
instance
.
togglePreview
();
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
shown
).
toBe
(
false
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
false
);
});
});
describe
(
'
panel DOM element set up
'
,
()
=>
{
describe
(
'
panel DOM element set up
'
,
()
=>
{
it
(
'
sets up an element to contain the preview and stores it on instance
'
,
()
=>
{
it
(
'
sets up an element to contain the preview and stores it on instance
'
,
()
=>
{
expect
(
instance
.
p
review
.
el
).
toBeUndefined
();
expect
(
instance
.
markdownP
review
.
el
).
toBeUndefined
();
instance
.
togglePreview
();
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
el
).
toBeDefined
();
expect
(
instance
.
markdownP
review
.
el
).
toBeDefined
();
expect
(
instance
.
preview
.
el
.
classList
.
contains
(
EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS
)).
toBe
(
expect
(
true
,
instance
.
markdownPreview
.
el
.
classList
.
contains
(
EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS
)
,
);
)
.
toBe
(
true
)
;
});
});
it
(
'
re-uses existing preview DOM element on repeated calls
'
,
()
=>
{
it
(
'
re-uses existing preview DOM element on repeated calls
'
,
()
=>
{
instance
.
togglePreview
();
instance
.
togglePreview
();
const
origPreviewEl
=
instance
.
p
review
.
el
;
const
origPreviewEl
=
instance
.
markdownP
review
.
el
;
instance
.
togglePreview
();
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
el
).
toBe
(
origPreviewEl
);
expect
(
instance
.
markdownP
review
.
el
).
toBe
(
origPreviewEl
);
});
});
it
(
'
hides the preview DOM element by default
'
,
()
=>
{
it
(
'
hides the preview DOM element by default
'
,
()
=>
{
panelSpy
.
mockImplementation
();
panelSpy
.
mockImplementation
();
instance
.
togglePreview
();
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
el
.
style
.
display
).
toBe
(
'
none
'
);
expect
(
instance
.
markdownP
review
.
el
.
style
.
display
).
toBe
(
'
none
'
);
});
});
});
});
...
@@ -350,9 +373,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -350,9 +373,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it
(
'
toggles visibility of the preview DOM element
'
,
async
()
=>
{
it
(
'
toggles visibility of the preview DOM element
'
,
async
()
=>
{
await
togglePreview
();
await
togglePreview
();
expect
(
instance
.
p
review
.
el
.
style
.
display
).
toBe
(
'
block
'
);
expect
(
instance
.
markdownP
review
.
el
.
style
.
display
).
toBe
(
'
block
'
);
await
togglePreview
();
await
togglePreview
();
expect
(
instance
.
p
review
.
el
.
style
.
display
).
toBe
(
'
none
'
);
expect
(
instance
.
markdownP
review
.
el
.
style
.
display
).
toBe
(
'
none
'
);
});
});
describe
(
'
hidden preview DOM element
'
,
()
=>
{
describe
(
'
hidden preview DOM element
'
,
()
=>
{
...
@@ -367,9 +390,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -367,9 +390,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
});
it
(
'
stores disposable listener for model changes
'
,
async
()
=>
{
it
(
'
stores disposable listener for model changes
'
,
async
()
=>
{
expect
(
instance
.
p
review
.
modelChangeListener
).
toBeUndefined
();
expect
(
instance
.
markdownP
review
.
modelChangeListener
).
toBeUndefined
();
await
togglePreview
();
await
togglePreview
();
expect
(
instance
.
p
review
.
modelChangeListener
).
toBeDefined
();
expect
(
instance
.
markdownP
review
.
modelChangeListener
).
toBeDefined
();
});
});
});
});
...
@@ -386,7 +409,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
...
@@ -386,7 +409,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it
(
'
disposes the model change event listener
'
,
()
=>
{
it
(
'
disposes the model change event listener
'
,
()
=>
{
const
disposeSpy
=
jest
.
fn
();
const
disposeSpy
=
jest
.
fn
();
instance
.
p
review
.
modelChangeListener
=
{
instance
.
markdownP
review
.
modelChangeListener
=
{
dispose
:
disposeSpy
,
dispose
:
disposeSpy
,
};
};
instance
.
togglePreview
();
instance
.
togglePreview
();
...
...
spec/frontend/editor/source_editor_spec.js
View file @
021bb329
/* eslint-disable max-classes-per-file */
import
{
editor
as
monacoEditor
,
languages
as
monacoLanguages
}
from
'
monaco-editor
'
;
import
{
editor
as
monacoEditor
,
languages
as
monacoLanguages
}
from
'
monaco-editor
'
;
import
{
import
{
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL
,
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL
,
URI_PREFIX
,
URI_PREFIX
,
EDITOR_READY_EVENT
,
EDITOR_READY_EVENT
,
}
from
'
~/editor/constants
'
;
}
from
'
~/editor/constants
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
{
DEFAULT_THEME
,
themes
}
from
'
~/ide/lib/themes
'
;
import
{
DEFAULT_THEME
,
themes
}
from
'
~/ide/lib/themes
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
jest
.
mock
(
'
~/helpers/startup_css_helper
'
,
()
=>
{
return
{
waitForCSSLoaded
:
jest
.
fn
().
mockImplementation
((
cb
)
=>
{
// We have to artificially put the callback's execution
// to the end of the current call stack to be able to
// test that the callback is called after waitForCSSLoaded.
// setTimeout with 0 delay does exactly that.
// Otherwise we might end up with false positive results
setTimeout
(()
=>
{
cb
.
apply
();
},
0
);
}),
};
});
describe
(
'
Base editor
'
,
()
=>
{
describe
(
'
Base editor
'
,
()
=>
{
let
editorEl
;
let
editorEl
;
let
editor
;
let
editor
;
...
@@ -18,7 +31,6 @@ describe('Base editor', () => {
...
@@ -18,7 +31,6 @@ describe('Base editor', () => {
const
blobContent
=
'
Foo Bar
'
;
const
blobContent
=
'
Foo Bar
'
;
const
blobPath
=
'
test.md
'
;
const
blobPath
=
'
test.md
'
;
const
blobGlobalId
=
'
snippet_777
'
;
const
blobGlobalId
=
'
snippet_777
'
;
const
fakeModel
=
{
foo
:
'
bar
'
,
dispose
:
jest
.
fn
()
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
setFixtures
(
'
<div id="editor" data-editor-loading></div>
'
);
setFixtures
(
'
<div id="editor" data-editor-loading></div>
'
);
...
@@ -51,16 +63,6 @@ describe('Base editor', () => {
...
@@ -51,16 +63,6 @@ describe('Base editor', () => {
describe
(
'
instance of the Source Editor
'
,
()
=>
{
describe
(
'
instance of the Source Editor
'
,
()
=>
{
let
modelSpy
;
let
modelSpy
;
let
instanceSpy
;
let
instanceSpy
;
const
setModel
=
jest
.
fn
();
const
dispose
=
jest
.
fn
();
const
mockModelReturn
=
(
res
=
fakeModel
)
=>
{
modelSpy
=
jest
.
spyOn
(
monacoEditor
,
'
createModel
'
).
mockImplementation
(()
=>
res
);
};
const
mockDecorateInstance
=
(
decorations
=
{})
=>
{
jest
.
spyOn
(
SourceEditor
,
'
convertMonacoToELInstance
'
).
mockImplementation
((
inst
)
=>
{
return
Object
.
assign
(
inst
,
decorations
);
});
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
modelSpy
=
jest
.
spyOn
(
monacoEditor
,
'
createModel
'
);
modelSpy
=
jest
.
spyOn
(
monacoEditor
,
'
createModel
'
);
...
@@ -72,46 +74,38 @@ describe('Base editor', () => {
...
@@ -72,46 +74,38 @@ describe('Base editor', () => {
});
});
it
(
'
throws an error if no dom element is supplied
'
,
()
=>
{
it
(
'
throws an error if no dom element is supplied
'
,
()
=>
{
mockDecorateInstance
();
const
create
=
()
=>
{
expect
(()
=>
{
editor
.
createInstance
();
editor
.
createInstance
();
}).
toThrow
(
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL
);
};
expect
(
create
).
toThrow
(
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL
);
expect
(
modelSpy
).
not
.
toHaveBeenCalled
();
expect
(
modelSpy
).
not
.
toHaveBeenCalled
();
expect
(
instanceSpy
).
not
.
toHaveBeenCalled
();
expect
(
instanceSpy
).
not
.
toHaveBeenCalled
();
expect
(
SourceEditor
.
convertMonacoToELInstance
).
not
.
toHaveBeenCalled
();
});
});
it
(
'
creates model to be supplied to Monaco editor
'
,
()
=>
{
it
(
'
creates model and attaches it to the instance
'
,
()
=>
{
mockModelReturn
();
jest
.
spyOn
(
monacoEditor
,
'
createModel
'
);
mockDecorateInstance
({
const
instance
=
editor
.
createInstance
(
defaultArguments
);
setModel
,
});
editor
.
createInstance
(
defaultArguments
);
expect
(
mo
delSpy
).
toHaveBeenCalledWith
(
expect
(
mo
nacoEditor
.
createModel
).
toHaveBeenCalledWith
(
blobContent
,
blobContent
,
undefined
,
undefined
,
expect
.
objectContaining
({
expect
.
objectContaining
({
path
:
uriFilePath
,
path
:
uriFilePath
,
}),
}),
);
);
expect
(
setModel
).
toHaveBeenCalledWith
(
fakeModel
);
expect
(
instance
.
getModel
().
getValue
()).
toEqual
(
defaultArguments
.
blobContent
);
});
});
it
(
'
does not create a model automatically if model is passed as `null`
'
,
()
=>
{
it
(
'
does not create a model automatically if model is passed as `null`
'
,
()
=>
{
mockDecorateInstance
({
const
instance
=
editor
.
createInstance
({
...
defaultArguments
,
model
:
null
});
setModel
,
expect
(
instance
.
getModel
()).
toBeNull
();
});
editor
.
createInstance
({
...
defaultArguments
,
model
:
null
});
expect
(
modelSpy
).
not
.
toHaveBeenCalled
();
expect
(
setModel
).
not
.
toHaveBeenCalled
();
});
});
it
(
'
initializes the instance on a supplied DOM node
'
,
()
=>
{
it
(
'
initializes the instance on a supplied DOM node
'
,
()
=>
{
editor
.
createInstance
({
el
:
editorEl
});
editor
.
createInstance
({
el
:
editorEl
});
expect
(
editor
.
editorEl
).
not
.
toBe
(
null
);
expect
(
editor
.
editorEl
).
not
.
toBe
Null
(
);
expect
(
instanceSpy
).
toHaveBeenCalledWith
(
editorEl
,
expect
.
anything
());
expect
(
instanceSpy
).
toHaveBeenCalledWith
(
editorEl
,
expect
.
anything
());
});
});
...
@@ -142,32 +136,43 @@ describe('Base editor', () => {
...
@@ -142,32 +136,43 @@ describe('Base editor', () => {
});
});
it
(
'
disposes instance when the global editor is disposed
'
,
()
=>
{
it
(
'
disposes instance when the global editor is disposed
'
,
()
=>
{
mockDecorateInstance
({
const
instance
=
editor
.
createInstance
(
defaultArguments
);
dispose
,
instance
.
dispose
=
jest
.
fn
();
});
editor
.
createInstance
(
defaultArguments
);
expect
(
dispose
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
dispose
).
not
.
toHaveBeenCalled
();
editor
.
dispose
();
editor
.
dispose
();
expect
(
dispose
).
toHaveBeenCalled
();
expect
(
instance
.
dispose
).
toHaveBeenCalled
();
});
});
it
(
"
removes the disposed instance from the global editor's storage and disposes the associated model
"
,
()
=>
{
it
(
"
removes the disposed instance from the global editor's storage and disposes the associated model
"
,
()
=>
{
mockModelReturn
();
mockDecorateInstance
({
setModel
,
});
const
instance
=
editor
.
createInstance
(
defaultArguments
);
const
instance
=
editor
.
createInstance
(
defaultArguments
);
expect
(
editor
.
instances
).
toHaveLength
(
1
);
expect
(
editor
.
instances
).
toHaveLength
(
1
);
expect
(
fakeModel
.
dispose
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
getModel
()).
not
.
toBeNull
();
instance
.
dispose
();
instance
.
dispose
();
expect
(
editor
.
instances
).
toHaveLength
(
0
);
expect
(
editor
.
instances
).
toHaveLength
(
0
);
expect
(
fakeModel
.
dispose
).
toHaveBeenCalled
();
expect
(
instance
.
getModel
()).
toBeNull
();
});
it
(
'
resets the layout in waitForCSSLoaded callback
'
,
async
()
=>
{
const
layoutSpy
=
jest
.
fn
();
jest
.
spyOn
(
monacoEditor
,
'
create
'
).
mockReturnValue
({
layout
:
layoutSpy
,
setModel
:
jest
.
fn
(),
onDidDispose
:
jest
.
fn
(),
dispose
:
jest
.
fn
(),
});
editor
.
createInstance
(
defaultArguments
);
expect
(
layoutSpy
).
not
.
toHaveBeenCalled
();
// We're waiting for the waitForCSSLoaded mock to kick in
await
jest
.
runOnlyPendingTimers
();
expect
(
layoutSpy
).
toHaveBeenCalled
();
});
});
});
});
...
@@ -213,26 +218,17 @@ describe('Base editor', () => {
...
@@ -213,26 +218,17 @@ describe('Base editor', () => {
});
});
it
(
'
correctly disposes the diff editor model
'
,
()
=>
{
it
(
'
correctly disposes the diff editor model
'
,
()
=>
{
const
modifiedModel
=
fakeModel
;
const
originalModel
=
{
...
fakeModel
};
mockDecorateInstance
({
getModel
:
jest
.
fn
().
mockReturnValue
({
original
:
originalModel
,
modified
:
modifiedModel
,
}),
});
const
instance
=
editor
.
createDiffInstance
({
...
defaultArguments
,
blobOriginalContent
});
const
instance
=
editor
.
createDiffInstance
({
...
defaultArguments
,
blobOriginalContent
});
expect
(
editor
.
instances
).
toHaveLength
(
1
);
expect
(
editor
.
instances
).
toHaveLength
(
1
);
expect
(
originalModel
.
dispose
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
getOriginalEditor
().
getModel
()).
not
.
toBeNull
();
expect
(
modifiedModel
.
dispose
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
getModifiedEditor
().
getModel
()).
not
.
toBeNull
();
instance
.
dispose
();
instance
.
dispose
();
expect
(
editor
.
instances
).
toHaveLength
(
0
);
expect
(
editor
.
instances
).
toHaveLength
(
0
);
expect
(
originalModel
.
dispose
).
toHaveBeenCalled
();
expect
(
instance
.
getOriginalEditor
().
getModel
()).
toBeNull
();
expect
(
modifiedModel
.
dispose
).
toHaveBeenCalled
();
expect
(
instance
.
getModifiedEditor
().
getModel
()).
toBeNull
();
});
});
});
});
});
});
...
@@ -354,198 +350,21 @@ describe('Base editor', () => {
...
@@ -354,198 +350,21 @@ describe('Base editor', () => {
expect
(
instance
.
getValue
()).
toBe
(
blobContent
);
expect
(
instance
.
getValue
()).
toBe
(
blobContent
);
});
});
it
(
'
is capable of changing the language of the model
'
,
()
=>
{
// ignore warnings and errors Monaco posts during setup
// (due to being called from Jest/Node.js environment)
jest
.
spyOn
(
console
,
'
warn
'
).
mockImplementation
(()
=>
{});
jest
.
spyOn
(
console
,
'
error
'
).
mockImplementation
(()
=>
{});
const
blobRenamedPath
=
'
test.js
'
;
expect
(
instance
.
getModel
().
getLanguageIdentifier
().
language
).
toBe
(
'
markdown
'
);
instance
.
updateModelLanguage
(
blobRenamedPath
);
expect
(
instance
.
getModel
().
getLanguageIdentifier
().
language
).
toBe
(
'
javascript
'
);
});
it
(
'
falls back to plaintext if there is no language associated with an extension
'
,
()
=>
{
const
blobRenamedPath
=
'
test.myext
'
;
const
spy
=
jest
.
spyOn
(
console
,
'
error
'
).
mockImplementation
(()
=>
{});
instance
.
updateModelLanguage
(
blobRenamedPath
);
expect
(
spy
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
getModel
().
getLanguageIdentifier
().
language
).
toBe
(
'
plaintext
'
);
});
});
describe
(
'
extensions
'
,
()
=>
{
let
instance
;
const
alphaRes
=
jest
.
fn
();
const
betaRes
=
jest
.
fn
();
const
fooRes
=
jest
.
fn
();
const
barRes
=
jest
.
fn
();
class
AlphaClass
{
constructor
()
{
this
.
res
=
alphaRes
;
}
alpha
()
{
return
this
?.
nonExistentProp
||
alphaRes
;
}
}
class
BetaClass
{
beta
()
{
return
this
?.
nonExistentProp
||
betaRes
;
}
}
class
WithStaticMethod
{
constructor
({
instance
:
inst
,
...
options
}
=
{})
{
Object
.
assign
(
inst
,
options
);
}
static
computeBoo
(
a
)
{
return
a
+
1
;
}
boo
()
{
return
WithStaticMethod
.
computeBoo
(
this
.
base
);
}
}
class
WithStaticMethodExtended
extends
SourceEditorExtension
{
static
computeBoo
(
a
)
{
return
a
+
1
;
}
boo
()
{
return
WithStaticMethodExtended
.
computeBoo
(
this
.
base
);
}
}
const
AlphaExt
=
new
AlphaClass
();
const
BetaExt
=
new
BetaClass
();
const
FooObjExt
=
{
foo
()
{
return
fooRes
;
},
};
const
BarObjExt
=
{
bar
()
{
return
barRes
;
},
};
describe
(
'
basic functionality
'
,
()
=>
{
beforeEach
(()
=>
{
instance
=
editor
.
createInstance
({
el
:
editorEl
,
blobPath
,
blobContent
});
});
it
(
'
does not fail if no extensions supplied
'
,
()
=>
{
const
spy
=
jest
.
spyOn
(
global
.
console
,
'
error
'
);
instance
.
use
();
expect
(
spy
).
not
.
toHaveBeenCalled
();
});
it
(
"
does not extend instance with extension's constructor
"
,
()
=>
{
expect
(
instance
.
constructor
).
toBeDefined
();
const
{
constructor
}
=
instance
;
expect
(
AlphaExt
.
constructor
).
toBeDefined
();
expect
(
AlphaExt
.
constructor
).
not
.
toEqual
(
constructor
);
instance
.
use
(
AlphaExt
);
expect
(
instance
.
constructor
).
toBe
(
constructor
);
});
it
.
each
`
type | extensions | methods | expectations
${
'
ES6 classes
'
}
|
${
AlphaExt
}
|
${[
'
alpha
'
]}
|
${[
alphaRes
]}
${
'
multiple ES6 classes
'
}
|
${[
AlphaExt
,
BetaExt
]}
|
${[
'
alpha
'
,
'
beta
'
]}
|
${[
alphaRes
,
betaRes
]}
${
'
simple objects
'
}
|
${
FooObjExt
}
|
${[
'
foo
'
]}
|
${[
fooRes
]}
${
'
multiple simple objects
'
}
|
${[
FooObjExt
,
BarObjExt
]}
|
${[
'
foo
'
,
'
bar
'
]}
|
${[
fooRes
,
barRes
]}
${
'
combination of ES6 classes and objects
'
}
|
${[
AlphaExt
,
BarObjExt
]}
|
${[
'
alpha
'
,
'
bar
'
]}
|
${[
alphaRes
,
barRes
]}
`
(
'
is extensible with $type
'
,
({
extensions
,
methods
,
expectations
}
=
{})
=>
{
methods
.
forEach
((
method
)
=>
{
expect
(
instance
[
method
]).
toBeUndefined
();
});
instance
.
use
(
extensions
);
methods
.
forEach
((
method
)
=>
{
expect
(
instance
[
method
]).
toBeDefined
();
});
expectations
.
forEach
((
expectation
,
i
)
=>
{
expect
(
instance
[
methods
[
i
]].
call
()).
toEqual
(
expectation
);
});
});
it
(
'
does not extend instance with private data of an extension
'
,
()
=>
{
const
ext
=
new
WithStaticMethod
({
instance
});
ext
.
staticMethod
=
()
=>
{
return
'
foo
'
;
};
ext
.
staticProp
=
'
bar
'
;
expect
(
instance
.
boo
).
toBeUndefined
();
expect
(
instance
.
staticMethod
).
toBeUndefined
();
expect
(
instance
.
staticProp
).
toBeUndefined
();
instance
.
use
(
ext
);
expect
(
instance
.
boo
).
toBeDefined
();
expect
(
instance
.
staticMethod
).
toBeUndefined
();
expect
(
instance
.
staticProp
).
toBeUndefined
();
});
it
.
each
([
WithStaticMethod
,
WithStaticMethodExtended
])(
'
properly resolves data for an extension with private data
'
,
(
ExtClass
)
=>
{
const
base
=
1
;
expect
(
instance
.
base
).
toBeUndefined
();
expect
(
instance
.
boo
).
toBeUndefined
();
const
ext
=
new
ExtClass
({
instance
,
base
});
instance
.
use
(
ext
);
expect
(
instance
.
base
).
toBe
(
1
);
expect
(
instance
.
boo
()).
toBe
(
2
);
},
);
it
(
'
uses the last definition of a method in case of an overlap
'
,
()
=>
{
const
FooObjExt2
=
{
foo
:
'
foo2
'
};
instance
.
use
([
FooObjExt
,
BarObjExt
,
FooObjExt2
]);
expect
(
instance
).
toMatchObject
({
foo
:
'
foo2
'
,
...
BarObjExt
,
});
});
it
(
'
correctly resolves references withing extensions
'
,
()
=>
{
const
FunctionExt
=
{
inst
()
{
return
this
;
},
mod
()
{
return
this
.
getModel
();
},
};
instance
.
use
(
FunctionExt
);
expect
(
instance
.
inst
()).
toEqual
(
editor
.
instances
[
0
]);
});
it
(
'
emits the EDITOR_READY_EVENT event after setting up the instance
'
,
()
=>
{
it
(
'
emits the EDITOR_READY_EVENT event after setting up the instance
'
,
()
=>
{
jest
.
spyOn
(
monacoEditor
,
'
create
'
).
mockImplementation
(()
=>
{
jest
.
spyOn
(
monacoEditor
,
'
create
'
).
mockImplementation
(()
=>
{
return
{
return
{
setModel
:
jest
.
fn
(),
setModel
:
jest
.
fn
(),
onDidDispose
:
jest
.
fn
(),
onDidDispose
:
jest
.
fn
(),
layout
:
jest
.
fn
(),
};
};
});
});
const
eventSpy
=
jest
.
fn
();
const
eventSpy
=
jest
.
fn
();
editorEl
.
addEventListener
(
EDITOR_READY_EVENT
,
eventSpy
);
editorEl
.
addEventListener
(
EDITOR_READY_EVENT
,
eventSpy
);
expect
(
eventSpy
).
not
.
toHaveBeenCalled
();
expect
(
eventSpy
).
not
.
toHaveBeenCalled
();
instance
=
editor
.
createInstance
({
el
:
editorEl
});
editor
.
createInstance
({
el
:
editorEl
});
expect
(
eventSpy
).
toHaveBeenCalled
();
expect
(
eventSpy
).
toHaveBeenCalled
();
});
});
});
});
});
describe
(
'
languages
'
,
()
=>
{
describe
(
'
languages
'
,
()
=>
{
it
(
'
registers custom languages defined with Monaco
'
,
()
=>
{
it
(
'
registers custom languages defined with Monaco
'
,
()
=>
{
...
...
spec/frontend/editor/source_editor_yaml_ext_spec.js
View file @
021bb329
...
@@ -2,6 +2,10 @@ import { Document } from 'yaml';
...
@@ -2,6 +2,10 @@ import { Document } from 'yaml';
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
{
YamlEditorExtension
}
from
'
~/editor/extensions/source_editor_yaml_ext
'
;
import
{
YamlEditorExtension
}
from
'
~/editor/extensions/source_editor_yaml_ext
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
spyOnApi
}
from
'
jest/editor/helpers
'
;
let
baseExtension
;
let
yamlExtension
;
const
getEditorInstance
=
(
editorInstanceOptions
=
{})
=>
{
const
getEditorInstance
=
(
editorInstanceOptions
=
{})
=>
{
setFixtures
(
'
<div id="editor"></div>
'
);
setFixtures
(
'
<div id="editor"></div>
'
);
...
@@ -16,7 +20,10 @@ const getEditorInstance = (editorInstanceOptions = {}) => {
...
@@ -16,7 +20,10 @@ const getEditorInstance = (editorInstanceOptions = {}) => {
const
getEditorInstanceWithExtension
=
(
extensionOptions
=
{},
editorInstanceOptions
=
{})
=>
{
const
getEditorInstanceWithExtension
=
(
extensionOptions
=
{},
editorInstanceOptions
=
{})
=>
{
setFixtures
(
'
<div id="editor"></div>
'
);
setFixtures
(
'
<div id="editor"></div>
'
);
const
instance
=
getEditorInstance
(
editorInstanceOptions
);
const
instance
=
getEditorInstance
(
editorInstanceOptions
);
instance
.
use
(
new
YamlEditorExtension
({
instance
,
...
extensionOptions
}));
[
baseExtension
,
yamlExtension
]
=
instance
.
use
([
{
definition
:
SourceEditorExtension
},
{
definition
:
YamlEditorExtension
,
setupOptions
:
extensionOptions
},
]);
// Remove the below once
// Remove the below once
// https://gitlab.com/gitlab-org/gitlab/-/issues/325992 is resolved
// https://gitlab.com/gitlab-org/gitlab/-/issues/325992 is resolved
...
@@ -29,19 +36,16 @@ const getEditorInstanceWithExtension = (extensionOptions = {}, editorInstanceOpt
...
@@ -29,19 +36,16 @@ const getEditorInstanceWithExtension = (extensionOptions = {}, editorInstanceOpt
describe
(
'
YamlCreatorExtension
'
,
()
=>
{
describe
(
'
YamlCreatorExtension
'
,
()
=>
{
describe
(
'
constructor
'
,
()
=>
{
describe
(
'
constructor
'
,
()
=>
{
it
(
'
saves constructor options
'
,
()
=>
{
it
(
'
saves setupOptions options on the extension, but does not expose those to instance
'
,
()
=>
{
const
highlightPath
=
'
foo
'
;
const
instance
=
getEditorInstanceWithExtension
({
const
instance
=
getEditorInstanceWithExtension
({
highlightPath
:
'
foo
'
,
highlightPath
,
enableComments
:
true
,
enableComments
:
true
,
});
});
expect
(
instance
).
toEqual
(
expect
(
yamlExtension
.
obj
.
highlightPath
).
toBe
(
highlightPath
);
expect
.
objectContaining
({
expect
(
yamlExtension
.
obj
.
enableComments
).
toBe
(
true
);
options
:
expect
.
objectContaining
({
expect
(
instance
.
highlightPath
).
toBeUndefined
();
highlightPath
:
'
foo
'
,
expect
(
instance
.
enableComments
).
toBeUndefined
();
enableComments
:
true
,
}),
}),
);
});
});
it
(
'
dumps values loaded with the model constructor options
'
,
()
=>
{
it
(
'
dumps values loaded with the model constructor options
'
,
()
=>
{
...
@@ -55,7 +59,7 @@ describe('YamlCreatorExtension', () => {
...
@@ -55,7 +59,7 @@ describe('YamlCreatorExtension', () => {
it
(
'
registers the onUpdate() function
'
,
()
=>
{
it
(
'
registers the onUpdate() function
'
,
()
=>
{
const
instance
=
getEditorInstance
();
const
instance
=
getEditorInstance
();
const
onDidChangeModelContent
=
jest
.
spyOn
(
instance
,
'
onDidChangeModelContent
'
);
const
onDidChangeModelContent
=
jest
.
spyOn
(
instance
,
'
onDidChangeModelContent
'
);
instance
.
use
(
new
YamlEditorExtension
({
instance
})
);
instance
.
use
(
{
definition
:
YamlEditorExtension
}
);
expect
(
onDidChangeModelContent
).
toHaveBeenCalledWith
(
expect
.
any
(
Function
));
expect
(
onDidChangeModelContent
).
toHaveBeenCalledWith
(
expect
.
any
(
Function
));
});
});
...
@@ -82,21 +86,21 @@ describe('YamlCreatorExtension', () => {
...
@@ -82,21 +86,21 @@ describe('YamlCreatorExtension', () => {
it
(
'
should call transformComments if enableComments is true
'
,
()
=>
{
it
(
'
should call transformComments if enableComments is true
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
({
enableComments
:
true
});
const
instance
=
getEditorInstanceWithExtension
({
enableComments
:
true
});
const
transformComments
=
jest
.
spyOn
(
YamlEditorExtension
,
'
transformComments
'
);
const
transformComments
=
jest
.
spyOn
(
YamlEditorExtension
,
'
transformComments
'
);
YamlEditorExtension
.
initFromModel
(
instance
,
model
);
instance
.
initFromModel
(
model
);
expect
(
transformComments
).
toHaveBeenCalled
();
expect
(
transformComments
).
toHaveBeenCalled
();
});
});
it
(
'
should not call transformComments if enableComments is false
'
,
()
=>
{
it
(
'
should not call transformComments if enableComments is false
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
({
enableComments
:
false
});
const
instance
=
getEditorInstanceWithExtension
({
enableComments
:
false
});
const
transformComments
=
jest
.
spyOn
(
YamlEditorExtension
,
'
transformComments
'
);
const
transformComments
=
jest
.
spyOn
(
YamlEditorExtension
,
'
transformComments
'
);
YamlEditorExtension
.
initFromModel
(
instance
,
model
);
instance
.
initFromModel
(
model
);
expect
(
transformComments
).
not
.
toHaveBeenCalled
();
expect
(
transformComments
).
not
.
toHaveBeenCalled
();
});
});
it
(
'
should call setValue with the stringified model
'
,
()
=>
{
it
(
'
should call setValue with the stringified model
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
();
const
instance
=
getEditorInstanceWithExtension
();
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
YamlEditorExtension
.
initFromModel
(
instance
,
model
);
instance
.
initFromModel
(
model
);
expect
(
setValue
).
toHaveBeenCalledWith
(
doc
.
toString
());
expect
(
setValue
).
toHaveBeenCalledWith
(
doc
.
toString
());
});
});
});
});
...
@@ -240,26 +244,35 @@ foo:
...
@@ -240,26 +244,35 @@ foo:
it
(
"
should call setValue with the stringified doc if the editor's value is empty
"
,
()
=>
{
it
(
"
should call setValue with the stringified doc if the editor's value is empty
"
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
();
const
instance
=
getEditorInstanceWithExtension
();
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
const
updateValue
=
jest
.
spyOn
(
instance
,
'
updateValue
'
);
const
updateValueSpy
=
jest
.
fn
();
spyOnApi
(
yamlExtension
,
{
updateValue
:
updateValueSpy
,
});
instance
.
setDoc
(
doc
);
instance
.
setDoc
(
doc
);
expect
(
setValue
).
toHaveBeenCalledWith
(
doc
.
toString
());
expect
(
setValue
).
toHaveBeenCalledWith
(
doc
.
toString
());
expect
(
updateValue
).
not
.
toHaveBeenCalled
();
expect
(
updateValue
Spy
).
not
.
toHaveBeenCalled
();
});
});
it
(
"
should call updateValue with the stringified doc if the editor's value is not empty
"
,
()
=>
{
it
(
"
should call updateValue with the stringified doc if the editor's value is not empty
"
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
({},
{
value
:
'
asjkdhkasjdh
'
});
const
instance
=
getEditorInstanceWithExtension
({},
{
value
:
'
asjkdhkasjdh
'
});
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
const
updateValue
=
jest
.
spyOn
(
instance
,
'
updateValue
'
);
const
updateValueSpy
=
jest
.
fn
();
spyOnApi
(
yamlExtension
,
{
updateValue
:
updateValueSpy
,
});
instance
.
setDoc
(
doc
);
instance
.
setDoc
(
doc
);
expect
(
setValue
).
not
.
toHaveBeenCalled
();
expect
(
setValue
).
not
.
toHaveBeenCalled
();
expect
(
updateValue
).
toHaveBeenCalledWith
(
doc
.
toString
());
expect
(
updateValue
Spy
).
toHaveBeenCalledWith
(
instance
,
doc
.
toString
());
});
});
it
(
'
should trigger the onUpdate method
'
,
()
=>
{
it
(
'
should trigger the onUpdate method
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
();
const
instance
=
getEditorInstanceWithExtension
();
const
onUpdate
=
jest
.
spyOn
(
instance
,
'
onUpdate
'
);
const
onUpdateSpy
=
jest
.
fn
();
spyOnApi
(
yamlExtension
,
{
onUpdate
:
onUpdateSpy
,
});
instance
.
setDoc
(
doc
);
instance
.
setDoc
(
doc
);
expect
(
onUpdate
).
toHaveBeenCalled
();
expect
(
onUpdate
Spy
).
toHaveBeenCalled
();
});
});
});
});
...
@@ -320,9 +333,12 @@ foo:
...
@@ -320,9 +333,12 @@ foo:
it
(
'
calls highlight
'
,
()
=>
{
it
(
'
calls highlight
'
,
()
=>
{
const
highlightPath
=
'
foo
'
;
const
highlightPath
=
'
foo
'
;
const
instance
=
getEditorInstanceWithExtension
({
highlightPath
});
const
instance
=
getEditorInstanceWithExtension
({
highlightPath
});
instance
.
highlight
=
jest
.
fn
();
// Here we do not spy on the public API method of the extension, but rather
// the public method of the extension's instance.
// This is required based on how `onUpdate` works
const
highlightSpy
=
jest
.
spyOn
(
yamlExtension
.
obj
,
'
highlight
'
);
instance
.
onUpdate
();
instance
.
onUpdate
();
expect
(
instance
.
highlight
).
toHaveBeenCalledWith
(
highlightPath
);
expect
(
highlightSpy
).
toHaveBeenCalledWith
(
instance
,
highlightPath
);
});
});
});
});
...
@@ -350,8 +366,12 @@ foo:
...
@@ -350,8 +366,12 @@ foo:
beforeEach
(()
=>
{
beforeEach
(()
=>
{
instance
=
getEditorInstanceWithExtension
({
highlightPath
:
highlightPathOnSetup
},
{
value
});
instance
=
getEditorInstanceWithExtension
({
highlightPath
:
highlightPathOnSetup
},
{
value
});
highlightLinesSpy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
highlightLines
'
);
highlightLinesSpy
=
jest
.
fn
();
removeHighlightsSpy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
removeHighlights
'
);
removeHighlightsSpy
=
jest
.
fn
();
spyOnApi
(
baseExtension
,
{
highlightLines
:
highlightLinesSpy
,
removeHighlights
:
removeHighlightsSpy
,
});
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -361,7 +381,7 @@ foo:
...
@@ -361,7 +381,7 @@ foo:
it
(
'
saves the highlighted path in highlightPath
'
,
()
=>
{
it
(
'
saves the highlighted path in highlightPath
'
,
()
=>
{
const
path
=
'
foo.bar
'
;
const
path
=
'
foo.bar
'
;
instance
.
highlight
(
path
);
instance
.
highlight
(
path
);
expect
(
instance
.
options
.
highlightPath
).
toEqual
(
path
);
expect
(
yamlExtension
.
obj
.
highlightPath
).
toEqual
(
path
);
});
});
it
(
'
calls highlightLines with a number of lines
'
,
()
=>
{
it
(
'
calls highlightLines with a number of lines
'
,
()
=>
{
...
@@ -374,14 +394,14 @@ foo:
...
@@ -374,14 +394,14 @@ foo:
instance
.
highlight
(
null
);
instance
.
highlight
(
null
);
expect
(
removeHighlightsSpy
).
toHaveBeenCalledWith
(
instance
);
expect
(
removeHighlightsSpy
).
toHaveBeenCalledWith
(
instance
);
expect
(
highlightLinesSpy
).
not
.
toHaveBeenCalled
();
expect
(
highlightLinesSpy
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
options
.
highlightPath
).
toBeNull
();
expect
(
yamlExtension
.
obj
.
highlightPath
).
toBeNull
();
});
});
it
(
'
throws an error if path is invalid and does not change the highlighted path
'
,
()
=>
{
it
(
'
throws an error if path is invalid and does not change the highlighted path
'
,
()
=>
{
expect
(()
=>
instance
.
highlight
(
'
invalidPath[0]
'
)).
toThrow
(
expect
(()
=>
instance
.
highlight
(
'
invalidPath[0]
'
)).
toThrow
(
'
The node invalidPath[0] could not be found inside the document.
'
,
'
The node invalidPath[0] could not be found inside the document.
'
,
);
);
expect
(
instance
.
options
.
highlightPath
).
toEqual
(
highlightPathOnSetup
);
expect
(
yamlExtension
.
obj
.
highlightPath
).
toEqual
(
highlightPathOnSetup
);
expect
(
highlightLinesSpy
).
not
.
toHaveBeenCalled
();
expect
(
highlightLinesSpy
).
not
.
toHaveBeenCalled
();
expect
(
removeHighlightsSpy
).
not
.
toHaveBeenCalled
();
expect
(
removeHighlightsSpy
).
not
.
toHaveBeenCalled
();
});
});
...
...
spec/frontend/ide/components/repo_editor_spec.js
View file @
021bb329
...
@@ -9,7 +9,7 @@ import waitUsingRealTimer from 'helpers/wait_using_real_timer';
...
@@ -9,7 +9,7 @@ import waitUsingRealTimer from 'helpers/wait_using_real_timer';
import
{
exampleConfigs
,
exampleFiles
}
from
'
jest/ide/lib/editorconfig/mock_data
'
;
import
{
exampleConfigs
,
exampleFiles
}
from
'
jest/ide/lib/editorconfig/mock_data
'
;
import
{
EDITOR_CODE_INSTANCE_FN
,
EDITOR_DIFF_INSTANCE_FN
}
from
'
~/editor/constants
'
;
import
{
EDITOR_CODE_INSTANCE_FN
,
EDITOR_DIFF_INSTANCE_FN
}
from
'
~/editor/constants
'
;
import
{
EditorMarkdownExtension
}
from
'
~/editor/extensions/source_editor_markdown_ext
'
;
import
{
EditorMarkdownExtension
}
from
'
~/editor/extensions/source_editor_markdown_ext
'
;
import
{
Editor
WebIdeExtension
}
from
'
~/editor/extensions/source_editor_webide
_ext
'
;
import
{
Editor
MarkdownPreviewExtension
}
from
'
~/editor/extensions/source_editor_markdown_livepreview
_ext
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
RepoEditor
from
'
~/ide/components/repo_editor.vue
'
;
import
RepoEditor
from
'
~/ide/components/repo_editor.vue
'
;
import
{
import
{
...
@@ -23,6 +23,8 @@ import service from '~/ide/services';
...
@@ -23,6 +23,8 @@ import service from '~/ide/services';
import
{
createStoreOptions
}
from
'
~/ide/stores
'
;
import
{
createStoreOptions
}
from
'
~/ide/stores
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
ContentViewer
from
'
~/vue_shared/components/content_viewer/content_viewer.vue
'
;
import
ContentViewer
from
'
~/vue_shared/components/content_viewer/content_viewer.vue
'
;
import
SourceEditorInstance
from
'
~/editor/source_editor_instance
'
;
import
{
spyOnApi
}
from
'
jest/editor/helpers
'
;
import
{
file
}
from
'
../helpers
'
;
import
{
file
}
from
'
../helpers
'
;
const
PREVIEW_MARKDOWN_PATH
=
'
/foo/bar/preview_markdown
'
;
const
PREVIEW_MARKDOWN_PATH
=
'
/foo/bar/preview_markdown
'
;
...
@@ -101,6 +103,7 @@ describe('RepoEditor', () => {
...
@@ -101,6 +103,7 @@ describe('RepoEditor', () => {
let
createDiffInstanceSpy
;
let
createDiffInstanceSpy
;
let
createModelSpy
;
let
createModelSpy
;
let
applyExtensionSpy
;
let
applyExtensionSpy
;
let
extensionsStore
;
const
waitForEditorSetup
=
()
=>
const
waitForEditorSetup
=
()
=>
new
Promise
((
resolve
)
=>
{
new
Promise
((
resolve
)
=>
{
...
@@ -120,6 +123,7 @@ describe('RepoEditor', () => {
...
@@ -120,6 +123,7 @@ describe('RepoEditor', () => {
});
});
await
waitForPromises
();
await
waitForPromises
();
vm
=
wrapper
.
vm
;
vm
=
wrapper
.
vm
;
extensionsStore
=
wrapper
.
vm
.
globalEditor
.
extensionsStore
;
jest
.
spyOn
(
vm
,
'
getFileData
'
).
mockResolvedValue
();
jest
.
spyOn
(
vm
,
'
getFileData
'
).
mockResolvedValue
();
jest
.
spyOn
(
vm
,
'
getRawFileData
'
).
mockResolvedValue
();
jest
.
spyOn
(
vm
,
'
getRawFileData
'
).
mockResolvedValue
();
};
};
...
@@ -127,28 +131,12 @@ describe('RepoEditor', () => {
...
@@ -127,28 +131,12 @@ describe('RepoEditor', () => {
const
findEditor
=
()
=>
wrapper
.
find
(
'
[data-testid="editor-container"]
'
);
const
findEditor
=
()
=>
wrapper
.
find
(
'
[data-testid="editor-container"]
'
);
const
findTabs
=
()
=>
wrapper
.
findAll
(
'
.ide-mode-tabs .nav-links li
'
);
const
findTabs
=
()
=>
wrapper
.
findAll
(
'
.ide-mode-tabs .nav-links li
'
);
const
findPreviewTab
=
()
=>
wrapper
.
find
(
'
[data-testid="preview-tab"]
'
);
const
findPreviewTab
=
()
=>
wrapper
.
find
(
'
[data-testid="preview-tab"]
'
);
const
expectEditorMarkdownExtension
=
(
shouldHaveExtension
)
=>
{
if
(
shouldHaveExtension
)
{
expect
(
applyExtensionSpy
).
toHaveBeenCalledWith
(
wrapper
.
vm
.
editor
,
expect
.
any
(
EditorMarkdownExtension
),
);
// TODO: spying on extensions causes Jest to blow up, so we have to assert on
// the public property the extension adds, as opposed to the args passed to the ctor
expect
(
wrapper
.
vm
.
editor
.
previewMarkdownPath
).
toBe
(
PREVIEW_MARKDOWN_PATH
);
}
else
{
expect
(
applyExtensionSpy
).
not
.
toHaveBeenCalledWith
(
wrapper
.
vm
.
editor
,
expect
.
any
(
EditorMarkdownExtension
),
);
}
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
createInstanceSpy
=
jest
.
spyOn
(
SourceEditor
.
prototype
,
EDITOR_CODE_INSTANCE_FN
);
createInstanceSpy
=
jest
.
spyOn
(
SourceEditor
.
prototype
,
EDITOR_CODE_INSTANCE_FN
);
createDiffInstanceSpy
=
jest
.
spyOn
(
SourceEditor
.
prototype
,
EDITOR_DIFF_INSTANCE_FN
);
createDiffInstanceSpy
=
jest
.
spyOn
(
SourceEditor
.
prototype
,
EDITOR_DIFF_INSTANCE_FN
);
createModelSpy
=
jest
.
spyOn
(
monacoEditor
,
'
createModel
'
);
createModelSpy
=
jest
.
spyOn
(
monacoEditor
,
'
createModel
'
);
applyExtensionSpy
=
jest
.
spyOn
(
SourceEditor
,
'
instanceApplyExtension
'
);
applyExtensionSpy
=
jest
.
spyOn
(
SourceEditor
Instance
.
prototype
,
'
use
'
);
jest
.
spyOn
(
service
,
'
getFileData
'
).
mockResolvedValue
();
jest
.
spyOn
(
service
,
'
getFileData
'
).
mockResolvedValue
();
jest
.
spyOn
(
service
,
'
getRawFileData
'
).
mockResolvedValue
();
jest
.
spyOn
(
service
,
'
getRawFileData
'
).
mockResolvedValue
();
});
});
...
@@ -275,13 +263,12 @@ describe('RepoEditor', () => {
...
@@ -275,13 +263,12 @@ describe('RepoEditor', () => {
);
);
it
(
'
installs the WebIDE extension
'
,
async
()
=>
{
it
(
'
installs the WebIDE extension
'
,
async
()
=>
{
const
extensionSpy
=
jest
.
spyOn
(
SourceEditor
,
'
instanceApplyExtension
'
);
await
createComponent
();
await
createComponent
();
expect
(
e
xtensionSpy
).
toHaveBeenCalled
();
expect
(
applyE
xtensionSpy
).
toHaveBeenCalled
();
Reflect
.
ownKeys
(
EditorWebIdeExtension
.
prototype
)
const
ideExtensionApi
=
extensionsStore
.
get
(
'
EditorWebIde
'
).
api
;
.
filter
((
fn
)
=>
fn
!==
'
constructor
'
)
Reflect
.
ownKeys
(
ideExtensionApi
).
forEach
((
fn
)
=>
{
.
forEach
((
fn
)
=>
{
expect
(
vm
.
editor
[
fn
]).
toBeDefined
();
expect
(
vm
.
editor
[
fn
]).
toBe
(
EditorWebIdeExtension
.
prototype
[
fn
]
);
expect
(
vm
.
editor
.
methods
[
fn
]).
toBe
(
'
EditorWebIde
'
);
});
});
});
});
...
@@ -301,7 +288,20 @@ describe('RepoEditor', () => {
...
@@ -301,7 +288,20 @@ describe('RepoEditor', () => {
async
({
activeFile
,
viewer
,
shouldHaveMarkdownExtension
}
=
{})
=>
{
async
({
activeFile
,
viewer
,
shouldHaveMarkdownExtension
}
=
{})
=>
{
await
createComponent
({
state
:
{
viewer
},
activeFile
});
await
createComponent
({
state
:
{
viewer
},
activeFile
});
expectEditorMarkdownExtension
(
shouldHaveMarkdownExtension
);
if
(
shouldHaveMarkdownExtension
)
{
expect
(
applyExtensionSpy
).
toHaveBeenCalledWith
({
definition
:
EditorMarkdownPreviewExtension
,
setupOptions
:
{
previewMarkdownPath
:
PREVIEW_MARKDOWN_PATH
},
});
// TODO: spying on extensions causes Jest to blow up, so we have to assert on
// the public property the extension adds, as opposed to the args passed to the ctor
expect
(
wrapper
.
vm
.
editor
.
markdownPreview
.
path
).
toBe
(
PREVIEW_MARKDOWN_PATH
);
}
else
{
expect
(
applyExtensionSpy
).
not
.
toHaveBeenCalledWith
(
wrapper
.
vm
.
editor
,
expect
.
any
(
EditorMarkdownExtension
),
);
}
},
},
);
);
});
});
...
@@ -329,18 +329,6 @@ describe('RepoEditor', () => {
...
@@ -329,18 +329,6 @@ describe('RepoEditor', () => {
expect
(
vm
.
model
).
toBe
(
existingModel
);
expect
(
vm
.
model
).
toBe
(
existingModel
);
});
});
it
(
'
adds callback methods
'
,
()
=>
{
jest
.
spyOn
(
vm
.
editor
,
'
onPositionChange
'
);
jest
.
spyOn
(
vm
.
model
,
'
onChange
'
);
jest
.
spyOn
(
vm
.
model
,
'
updateOptions
'
);
vm
.
setupEditor
();
expect
(
vm
.
editor
.
onPositionChange
).
toHaveBeenCalledTimes
(
1
);
expect
(
vm
.
model
.
onChange
).
toHaveBeenCalledTimes
(
1
);
expect
(
vm
.
model
.
updateOptions
).
toHaveBeenCalledWith
(
vm
.
rules
);
});
it
(
'
updates state with the value of the model
'
,
()
=>
{
it
(
'
updates state with the value of the model
'
,
()
=>
{
const
newContent
=
'
As Gregor Samsa
\n
awoke one morning
\n
'
;
const
newContent
=
'
As Gregor Samsa
\n
awoke one morning
\n
'
;
vm
.
model
.
setValue
(
newContent
);
vm
.
model
.
setValue
(
newContent
);
...
@@ -366,53 +354,48 @@ describe('RepoEditor', () => {
...
@@ -366,53 +354,48 @@ describe('RepoEditor', () => {
describe
(
'
editor updateDimensions
'
,
()
=>
{
describe
(
'
editor updateDimensions
'
,
()
=>
{
let
updateDimensionsSpy
;
let
updateDimensionsSpy
;
let
updateDiffViewSpy
;
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
await
createComponent
();
await
createComponent
();
updateDimensionsSpy
=
jest
.
spyOn
(
vm
.
editor
,
'
updateDimensions
'
);
const
ext
=
extensionsStore
.
get
(
'
EditorWebIde
'
);
updateDiffViewSpy
=
jest
.
spyOn
(
vm
.
editor
,
'
updateDiffView
'
).
mockImplementation
();
updateDimensionsSpy
=
jest
.
fn
();
spyOnApi
(
ext
,
{
updateDimensions
:
updateDimensionsSpy
,
});
});
});
it
(
'
calls updateDimensions only when panelResizing is false
'
,
async
()
=>
{
it
(
'
calls updateDimensions only when panelResizing is false
'
,
async
()
=>
{
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDiffViewSpy
).
not
.
toHaveBeenCalled
();
expect
(
vm
.
$store
.
state
.
panelResizing
).
toBe
(
false
);
// default value
expect
(
vm
.
$store
.
state
.
panelResizing
).
toBe
(
false
);
// default value
vm
.
$store
.
state
.
panelResizing
=
true
;
vm
.
$store
.
state
.
panelResizing
=
true
;
await
vm
.
$nextTick
();
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDiffViewSpy
).
not
.
toHaveBeenCalled
();
vm
.
$store
.
state
.
panelResizing
=
false
;
vm
.
$store
.
state
.
panelResizing
=
false
;
await
vm
.
$nextTick
();
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
1
);
vm
.
$store
.
state
.
panelResizing
=
true
;
vm
.
$store
.
state
.
panelResizing
=
true
;
await
vm
.
$nextTick
();
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
1
);
});
});
it
(
'
calls updateDimensions when rightPane is toggled
'
,
async
()
=>
{
it
(
'
calls updateDimensions when rightPane is toggled
'
,
async
()
=>
{
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDiffViewSpy
).
not
.
toHaveBeenCalled
();
expect
(
vm
.
$store
.
state
.
rightPane
.
isOpen
).
toBe
(
false
);
// default value
expect
(
vm
.
$store
.
state
.
rightPane
.
isOpen
).
toBe
(
false
);
// default value
vm
.
$store
.
state
.
rightPane
.
isOpen
=
true
;
vm
.
$store
.
state
.
rightPane
.
isOpen
=
true
;
await
vm
.
$nextTick
();
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
1
);
vm
.
$store
.
state
.
rightPane
.
isOpen
=
false
;
vm
.
$store
.
state
.
rightPane
.
isOpen
=
false
;
await
vm
.
$nextTick
();
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
2
);
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
2
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
2
);
});
});
});
});
...
@@ -447,7 +430,11 @@ describe('RepoEditor', () => {
...
@@ -447,7 +430,11 @@ describe('RepoEditor', () => {
activeFile
:
dummyFile
.
markdown
,
activeFile
:
dummyFile
.
markdown
,
});
});
updateDimensionsSpy
=
jest
.
spyOn
(
vm
.
editor
,
'
updateDimensions
'
);
const
ext
=
extensionsStore
.
get
(
'
EditorWebIde
'
);
updateDimensionsSpy
=
jest
.
fn
();
spyOnApi
(
ext
,
{
updateDimensions
:
updateDimensionsSpy
,
});
changeViewMode
(
FILE_VIEW_MODE_PREVIEW
);
changeViewMode
(
FILE_VIEW_MODE_PREVIEW
);
await
vm
.
$nextTick
();
await
vm
.
$nextTick
();
...
...
spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
View file @
021bb329
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
EDITOR_READY_EVENT
}
from
'
~/editor/constants
'
;
import
{
EDITOR_READY_EVENT
}
from
'
~/editor/constants
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
TextEditor
from
'
~/pipeline_editor/components/editor/text_editor.vue
'
;
import
TextEditor
from
'
~/pipeline_editor/components/editor/text_editor.vue
'
;
import
{
import
{
mockCiConfigPath
,
mockCiConfigPath
,
...
@@ -59,10 +58,6 @@ describe('Pipeline Editor | Text editor component', () => {
...
@@ -59,10 +58,6 @@ describe('Pipeline Editor | Text editor component', () => {
const
findEditor
=
()
=>
wrapper
.
findComponent
(
MockSourceEditor
);
const
findEditor
=
()
=>
wrapper
.
findComponent
(
MockSourceEditor
);
beforeEach
(()
=>
{
SourceEditorExtension
.
deferRerender
=
jest
.
fn
();
});
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment