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
Expand all
Hide 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
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
FileTemplateExtension
}
from
'
~/editor/extensions/source_editor_file_template_ext
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
{
getBlobLanguage
}
from
'
~/editor/utils
'
;
...
...
@@ -26,23 +27,29 @@ export default class EditBlob {
this
.
editor
.
focus
();
}
fetchMarkdownExtension
()
{
import
(
'
~/editor/extensions/source_editor_markdown_ext
'
)
.
then
(({
EditorMarkdownExtension
:
MarkdownExtension
}
=
{})
=>
{
this
.
editor
.
use
(
new
MarkdownExtension
({
instance
:
this
.
editor
,
previewMarkdownPath
:
this
.
options
.
previewMarkdownPath
,
}),
);
this
.
hasMarkdownExtension
=
true
;
addEditorMarkdownListeners
(
this
.
editor
);
})
.
catch
((
e
)
=>
createFlash
({
message
:
`
${
BLOB_EDITOR_ERROR
}
:
${
e
}
`
,
}),
);
async
fetchMarkdownExtension
()
{
try
{
const
[
{
EditorMarkdownExtension
:
MarkdownExtension
},
{
EditorMarkdownPreviewExtension
:
MarkdownLivePreview
},
]
=
await
Promise
.
all
([
import
(
'
~/editor/extensions/source_editor_markdown_ext
'
),
import
(
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
),
]);
this
.
editor
.
use
([
{
definition
:
MarkdownExtension
},
{
definition
:
MarkdownLivePreview
,
setupOptions
:
{
previewMarkdownPath
:
this
.
options
.
previewMarkdownPath
},
},
]);
}
catch
(
e
)
{
createFlash
({
message
:
`
${
BLOB_EDITOR_ERROR
}
:
${
e
}
`
,
});
}
this
.
hasMarkdownExtension
=
true
;
addEditorMarkdownListeners
(
this
.
editor
);
}
configureMonacoEditor
()
{
...
...
@@ -60,7 +67,7 @@ export default class EditBlob {
blobPath
:
fileNameEl
.
value
,
blobContent
:
editorEl
.
innerText
,
});
this
.
editor
.
use
(
new
FileTemplateExtension
({
instance
:
this
.
editor
})
);
this
.
editor
.
use
(
[{
definition
:
SourceEditorExtension
},
{
definition
:
FileTemplateExtension
}]
);
fileNameEl
.
addEventListener
(
'
change
'
,
()
=>
{
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__(
// 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
// '*.gitlab-ci.yml' regardless of project configuration.
// 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 @@
//
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
*/
...
...
app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
View file @
021bb329
import
ciSchemaPath
from
'
~/editor/schema/ci.json
'
;
import
{
registerSchema
}
from
'
~/ide/utils
'
;
import
{
SourceEditorExtension
}
from
'
./source_editor_extension_base
'
;
export
class
CiSchemaExtension
extends
SourceEditorExtension
{
/**
* Registers a syntax schema to the editor based on project
* identifier and commit.
*
* The schema is added to the file that is currently edited
* in the editor.
*
* @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
// ones loaded by monaco editor, we use absolute URLs
// to fetch schema files, hence the `gon.gitlab_url`
// reference. This prevents error:
// "Failed to execute 'fetch' on 'WorkerGlobalScope'"
const
absoluteSchemaUrl
=
gon
.
gitlab_url
+
ciSchemaPath
;
const
modelFileName
=
this
.
getModel
().
uri
.
path
.
split
(
'
/
'
).
pop
();
export
class
CiSchemaExtension
{
static
get
extensionName
()
{
return
'
CiSchema
'
;
}
// eslint-disable-next-line class-methods-use-this
provides
()
{
return
{
registerCiSchema
:
(
instance
)
=>
{
// In order for workers loaded from `data://` as the
// ones loaded by monaco editor, we use absolute URLs
// to fetch schema files, hence the `gon.gitlab_url`
// reference. This prevents error:
// "Failed to execute 'fetch' on 'WorkerGlobalScope'"
const
absoluteSchemaUrl
=
gon
.
gitlab_url
+
ciSchemaPath
;
const
modelFileName
=
instance
.
getModel
().
uri
.
path
.
split
(
'
/
'
).
pop
();
registerSchema
({
uri
:
absoluteSchemaUrl
,
fileMatch
:
[
modelFileName
],
});
registerSchema
({
uri
:
absoluteSchemaUrl
,
fileMatch
:
[
modelFileName
],
});
},
};
}
}
app/assets/javascripts/editor/extensions/source_editor_extension_base.js
View file @
021bb329
import
{
Range
}
from
'
monaco-editor
'
;
import
{
waitForCSSLoaded
}
from
'
~/helpers/startup_css_helper
'
;
import
{
ERROR_INSTANCE_REQUIRED_FOR_EXTENSION
,
EDITOR_TYPE_CODE
}
from
'
../constants
'
;
import
{
EDITOR_TYPE_CODE
,
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
,
EXTENSION_BASE_LINE_NUMBERS_CLASS
,
}
from
'
../constants
'
;
const
hashRegexp
=
new
RegExp
(
'
#?L
'
,
'
g
'
);
const
createAnchor
=
(
href
)
=>
{
const
fragment
=
new
DocumentFragment
();
const
el
=
document
.
createElement
(
'
a
'
);
el
.
classList
.
add
(
'
link-anchor
'
);
el
.
classList
.
add
(
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS
);
el
.
href
=
href
;
fragment
.
appendChild
(
el
);
el
.
addEventListener
(
'
contextmenu
'
,
(
e
)
=>
{
...
...
@@ -17,38 +20,46 @@ const createAnchor = (href) => {
};
export
class
SourceEditorExtension
{
constructor
({
instance
,
...
options
}
=
{})
{
if
(
instance
)
{
Object
.
assign
(
instance
,
options
);
SourceEditorExtension
.
highlightLines
(
instance
);
if
(
instance
.
getEditorType
&&
instance
.
getEditorType
()
===
EDITOR_TYPE_CODE
)
{
SourceEditorExtension
.
setupLineLinking
(
instance
);
}
SourceEditorExtension
.
deferRerender
(
instance
);
}
else
if
(
Object
.
entries
(
options
).
length
)
{
throw
new
Error
(
ERROR_INSTANCE_REQUIRED_FOR_EXTENSION
);
static
get
extensionName
()
{
return
'
BaseExtension
'
;
}
// eslint-disable-next-line class-methods-use-this
onUse
(
instance
)
{
SourceEditorExtension
.
highlightLines
(
instance
);
if
(
instance
.
getEditorType
&&
instance
.
getEditorType
()
===
EDITOR_TYPE_CODE
)
{
SourceEditorExtension
.
setupLineLinking
(
instance
);
}
}
static
deferRerender
(
instance
)
{
waitForCSSLoaded
(()
=>
{
instance
.
layout
();
});
static
onMouseMoveHandler
(
e
)
{
const
target
=
e
.
target
.
element
;
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
)
{
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
(
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
)
{
const
[
start
,
end
]
=
bounds
&&
Array
.
isArray
(
bounds
)
...
...
@@ -74,29 +85,29 @@ export class SourceEditorExtension {
}
}
static
onMouseMoveHandler
(
e
)
{
const
target
=
e
.
target
.
element
;
if
(
target
.
classList
.
contains
(
'
line-numbers
'
))
{
const
lineNum
=
e
.
target
.
position
.
lineNumber
;
const
hrefAttr
=
`#L
${
lineNum
}
`
;
let
el
=
target
.
querySelector
(
'
a
'
);
if
(
!
el
)
{
el
=
createAnchor
(
hrefAttr
);
target
.
appendChild
(
el
);
}
}
}
// eslint-disable-next-line class-methods-use-this
provides
()
{
return
{
/**
* Removes existing line decorations and updates the reference on the instance
* @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
*/
removeHighlights
:
(
instance
)
=>
{
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
{
SourceEditorExtension
}
from
'
./source_editor_extension_base
'
;
export
class
FileTemplateExtension
extends
SourceEditorExtension
{
navigateFileStart
()
{
this
.
setPosition
(
new
Position
(
1
,
1
));
export
class
FileTemplateExtension
{
static
get
extensionName
()
{
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
extends
EditorMarkdownPreviewExtension
{
getSelectedText
(
selection
=
this
.
getSelection
())
{
const
{
startLineNumber
,
endLineNumber
,
startColumn
,
endColumn
}
=
selection
;
const
valArray
=
this
.
getValue
().
split
(
'
\n
'
);
let
text
=
''
;
if
(
startLineNumber
===
endLineNumber
)
{
text
=
valArray
[
startLineNumber
-
1
].
slice
(
startColumn
-
1
,
endColumn
-
1
);
}
else
{
const
startLineText
=
valArray
[
startLineNumber
-
1
].
slice
(
startColumn
-
1
);
const
endLineText
=
valArray
[
endLineNumber
-
1
].
slice
(
0
,
endColumn
-
1
);
for
(
let
i
=
startLineNumber
,
k
=
endLineNumber
-
1
;
i
<
k
;
i
+=
1
)
{
text
+=
`
${
valArray
[
i
]}
`
;
if
(
i
!==
k
-
1
)
text
+=
`\n`
;
}
text
=
text
?
[
startLineText
,
text
,
endLineText
].
join
(
'
\n
'
)
:
[
startLineText
,
endLineText
].
join
(
'
\n
'
);
}
return
text
;
export
class
EditorMarkdownExtension
{
static
get
extensionName
()
{
return
'
EditorMarkdown
'
;
}
replaceSelectedText
(
text
,
select
=
undefined
)
{
const
forceMoveMarkers
=
!
select
;
this
.
executeEdits
(
''
,
[{
range
:
this
.
getSelection
(),
text
,
forceMoveMarkers
}]);
}
moveCursor
(
dx
=
0
,
dy
=
0
)
{
const
pos
=
this
.
getPosition
();
pos
.
column
+=
dx
;
pos
.
lineNumber
+=
dy
;
this
.
setPosition
(
pos
);
}
// eslint-disable-next-line class-methods-use-this
provides
()
{
return
{
getSelectedText
:
(
instance
,
selection
=
instance
.
getSelection
())
=>
{
const
{
startLineNumber
,
endLineNumber
,
startColumn
,
endColumn
}
=
selection
;
const
valArray
=
instance
.
getValue
().
split
(
'
\n
'
);
let
text
=
''
;
if
(
startLineNumber
===
endLineNumber
)
{
text
=
valArray
[
startLineNumber
-
1
].
slice
(
startColumn
-
1
,
endColumn
-
1
);
}
else
{
const
startLineText
=
valArray
[
startLineNumber
-
1
].
slice
(
startColumn
-
1
);
const
endLineText
=
valArray
[
endLineNumber
-
1
].
slice
(
0
,
endColumn
-
1
);
/**
* Adjust existing selection to select text within the original selection.
* - If `selectedText` is not supplied, we fetch selected text with
*
* ALGORITHM:
*
* MULTI-LINE SELECTION
* 1. Find line that contains `toSelect` text.
* 2. Using the index of this line and the position of `toSelect` text in it,
* construct:
* * newStartLineNumber
* * newStartColumn
*
* SINGLE-LINE SELECTION
* 1. Use `startLineNumber` from the current selection as `newStartLineNumber`
* 2. Find the position of `toSelect` text in it to get `newStartColumn`
*
* 3. `newEndLineNumber` — Since this method is supposed to be used with
* markdown decorators that are pretty short, the `newEndLineNumber` is
* suggested to be assumed the same as the startLine.
* 4. `newEndColumn` — pretty obvious
* 5. Adjust the start and end positions of the current selection
* 6. Re-set selection on the instance
*
* @param {string} toSelect - New text to select within current selection.
* @param {string} selectedText - Currently selected text. It's just a
* shortcut: If it's not supplied, we fetch selected text from the instance
*/
selectWithinSelection
(
toSelect
,
selectedText
)
{
const
currentSelection
=
this
.
getSelection
();
if
(
currentSelection
.
isEmpty
()
||
!
toSelect
)
{
return
;
}
const
text
=
selectedText
||
this
.
getSelectedText
(
currentSelection
);
let
lineShift
;
let
newStartLineNumber
;
let
newStartColumn
;
for
(
let
i
=
startLineNumber
,
k
=
endLineNumber
-
1
;
i
<
k
;
i
+=
1
)
{
text
+=
`
${
valArray
[
i
]}
`
;
if
(
i
!==
k
-
1
)
text
+=
`\n`
;
}
text
=
text
?
[
startLineText
,
text
,
endLineText
].
join
(
'
\n
'
)
:
[
startLineText
,
endLineText
].
join
(
'
\n
'
);
}
return
text
;
},
replaceSelectedText
:
(
instance
,
text
,
select
)
=>
{
const
forceMoveMarkers
=
!
select
;
instance
.
executeEdits
(
''
,
[{
range
:
instance
.
getSelection
(),
text
,
forceMoveMarkers
}]);
},
moveCursor
:
(
instance
,
dx
=
0
,
dy
=
0
)
=>
{
const
pos
=
instance
.
getPosition
();
pos
.
column
+=
dx
;
pos
.
lineNumber
+=
dy
;
instance
.
setPosition
(
pos
);
},
/**
* Adjust existing selection to select text within the original selection.
* - If `selectedText` is not supplied, we fetch selected text with
*
* ALGORITHM:
*
* MULTI-LINE SELECTION
* 1. Find line that contains `toSelect` text.
* 2. Using the index of this line and the position of `toSelect` text in it,
* construct:
* * newStartLineNumber
* * newStartColumn
*
* SINGLE-LINE SELECTION
* 1. Use `startLineNumber` from the current selection as `newStartLineNumber`
* 2. Find the position of `toSelect` text in it to get `newStartColumn`
*
* 3. `newEndLineNumber` — Since this method is supposed to be used with
* markdown decorators that are pretty short, the `newEndLineNumber` is
* suggested to be assumed the same as the startLine.
* 4. `newEndColumn` — pretty obvious
* 5. Adjust the start and end positions of the current selection
* 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} selectedText - Currently selected text. It's just a
* shortcut: If it's not supplied, we fetch selected text from the instance
*/
selectWithinSelection
:
(
instance
,
toSelect
,
selectedText
)
=>
{
const
currentSelection
=
instance
.
getSelection
();
if
(
currentSelection
.
isEmpty
()
||
!
toSelect
)
{
return
;
}
const
text
=
selectedText
||
instance
.
getSelectedText
(
currentSelection
);
let
lineShift
;
let
newStartLineNumber
;
let
newStartColumn
;
const
textLines
=
text
.
split
(
'
\n
'
);
const
textLines
=
text
.
split
(
'
\n
'
);
if
(
textLines
.
length
>
1
)
{
// Multi-line selection
lineShift
=
textLines
.
findIndex
((
line
)
=>
line
.
indexOf
(
toSelect
)
!==
-
1
);
newStartLineNumber
=
currentSelection
.
startLineNumber
+
lineShift
;
newStartColumn
=
textLines
[
lineShift
].
indexOf
(
toSelect
)
+
1
;
}
else
{
// Single-line selection
newStartLineNumber
=
currentSelection
.
startLineNumber
;
newStartColumn
=
currentSelection
.
startColumn
+
text
.
indexOf
(
toSelect
);
}
if
(
textLines
.
length
>
1
)
{
// Multi-line selection
lineShift
=
textLines
.
findIndex
((
line
)
=>
line
.
indexOf
(
toSelect
)
!==
-
1
);
newStartLineNumber
=
currentSelection
.
startLineNumber
+
lineShift
;
newStartColumn
=
textLines
[
lineShift
].
indexOf
(
toSelect
)
+
1
;
}
else
{
// Single-line selection
newStartLineNumber
=
currentSelection
.
startLineNumber
;
newStartColumn
=
currentSelection
.
startColumn
+
text
.
indexOf
(
toSelect
);
}
const
newEndLineNumber
=
newStartLineNumber
;
const
newEndColumn
=
newStartColumn
+
toSelect
.
length
;
const
newEndLineNumber
=
newStartLineNumber
;
const
newEndColumn
=
newStartColumn
+
toSelect
.
length
;
const
newSelection
=
currentSelection
.
setStartPosition
(
newStartLineNumber
,
newStartColumn
)
.
setEndPosition
(
newEndLineNumber
,
newEndColumn
);
const
newSelection
=
currentSelection
.
setStartPosition
(
newStartLineNumber
,
newStartColumn
)
.
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 {
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS
,
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
,
}
from
'
../constants
'
;
import
{
SourceEditorExtension
}
from
'
./source_editor_extension_base
'
;
const
get
Preview
=
(
text
,
previewMarkdownPath
)
=>
{
const
fetch
Preview
=
(
text
,
previewMarkdownPath
)
=>
{
return
axios
.
post
(
previewMarkdownPath
,
{
text
,
...
...
@@ -34,19 +33,20 @@ const setupDomElement = ({ injectToEl = null } = {}) => {
return
previewEl
;
};
export
class
EditorMarkdownPreviewExtension
extends
SourceEditorExtension
{
constructor
({
instance
,
previewMarkdownPath
,
...
args
}
=
{})
{
super
({
instance
,
...
args
});
Object
.
assign
(
instance
,
{
previewMarkdownPath
,
preview
:
{
el
:
undefined
,
action
:
undefined
,
shown
:
false
,
modelChangeListener
:
undefined
,
},
});
this
.
setupPreviewAction
.
call
(
instance
);
export
class
EditorMarkdownPreviewExtension
{
static
get
extensionName
()
{
return
'
EditorMarkdownPreview
'
;
}
onSetup
(
instance
,
setupOptions
)
{
this
.
preview
=
{
el
:
undefined
,
action
:
undefined
,
shown
:
false
,
modelChangeListener
:
undefined
,
path
:
setupOptions
.
previewMarkdownPath
,
};
this
.
setupPreviewAction
(
instance
);
instance
.
getModel
().
onDidChangeLanguage
(({
newLanguage
,
oldLanguage
}
=
{})
=>
{
if
(
newLanguage
===
'
markdown
'
&&
oldLanguage
!==
newLanguage
)
{
...
...
@@ -68,43 +68,31 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
});
}
static
togglePreviewLayout
(
)
{
const
{
width
,
height
}
=
this
.
getLayoutInfo
();
togglePreviewLayout
(
instance
)
{
const
{
width
,
height
}
=
instance
.
getLayoutInfo
();
const
newWidth
=
this
.
preview
.
shown
?
width
/
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
:
width
*
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
;
this
.
layout
({
width
:
newWidth
,
height
});
instance
.
layout
({
width
:
newWidth
,
height
});
}
static
togglePreviewPanel
(
)
{
const
parentEl
=
this
.
getDomNode
().
parentElement
;
togglePreviewPanel
(
instance
)
{
const
parentEl
=
instance
.
getDomNode
().
parentElement
;
const
{
el
:
previewEl
}
=
this
.
preview
;
parentEl
.
classList
.
toggle
(
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS
);
if
(
previewEl
.
style
.
display
===
'
none
'
)
{
// Show the preview panel
this
.
fetchPreview
();
this
.
fetchPreview
(
instance
);
}
else
{
// Hide the preview panel
previewEl
.
style
.
display
=
'
none
'
;
}
}
cleanup
()
{
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
()
{
fetchPreview
(
instance
)
{
const
{
el
:
previewEl
}
=
this
.
preview
;
getPreview
(
this
.
getValue
(),
this
.
previewMarkdownP
ath
)
fetchPreview
(
instance
.
getValue
(),
this
.
preview
.
p
ath
)
.
then
((
data
)
=>
{
previewEl
.
innerHTML
=
sanitize
(
data
);
syntaxHighlight
(
previewEl
.
querySelectorAll
(
'
.js-syntax-highlight
'
));
...
...
@@ -113,10 +101,10 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
.
catch
(()
=>
createFlash
(
BLOB_PREVIEW_ERROR
));
}
setupPreviewAction
()
{
if
(
this
.
getAction
(
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID
))
return
;
setupPreviewAction
(
instance
)
{
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
,
label
:
__
(
'
Preview Markdown
'
),
keybindings
:
[
...
...
@@ -128,27 +116,52 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension {
// Method that will be executed when the action is triggered.
// @param ed The editor instance is passed in as a convenience
run
(
inst
ance
)
{
inst
ance
.
togglePreview
();
run
(
inst
)
{
inst
.
togglePreview
();
},
});
}
togglePreview
()
{
if
(
!
this
.
preview
?.
el
)
{
this
.
preview
.
el
=
setupDomElement
({
injectToEl
:
this
.
getDomNode
().
parentElement
});
}
EditorMarkdownPreviewExtension
.
togglePreviewLayout
.
call
(
this
);
EditorMarkdownPreviewExtension
.
togglePreviewPanel
.
call
(
this
);
provides
()
{
return
{
markdownPreview
:
this
.
preview
,
if
(
!
this
.
preview
?.
shown
)
{
this
.
preview
.
modelChangeListener
=
this
.
onDidChangeModelContent
(
debounce
(
this
.
fetchPreview
.
bind
(
this
),
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
),
);
}
else
{
this
.
preview
.
modelChangeListener
.
dispose
();
}
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
),
this
.
preview
.
shown
=
!
this
.
preview
?.
shown
;
setupPreviewAction
:
(
instance
)
=>
this
.
setupPreviewAction
(
instance
),
togglePreview
:
(
instance
)
=>
{
if
(
!
this
.
preview
?.
el
)
{
this
.
preview
.
el
=
setupDomElement
({
injectToEl
:
instance
.
getDomNode
().
parentElement
});
}
this
.
togglePreviewLayout
(
instance
);
this
.
togglePreviewPanel
(
instance
);
if
(
!
this
.
preview
?.
shown
)
{
this
.
preview
.
modelChangeListener
=
instance
.
onDidChangeModelContent
(
debounce
(
this
.
fetchPreview
.
bind
(
this
,
instance
),
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
,
),
);
}
else
{
this
.
preview
.
modelChangeListener
.
dispose
();
}
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
{
KeyCode
,
KeyMod
,
Range
}
from
'
monaco-editor
'
;
import
{
EDITOR_TYPE_DIFF
}
from
'
~/editor/constants
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
Disposable
from
'
~/ide/lib/common/disposable
'
;
import
{
editorOptions
}
from
'
~/ide/lib/editor_options
'
;
import
keymap
from
'
~/ide/lib/keymap.json
'
;
...
...
@@ -11,154 +19,168 @@ const isDiffEditorType = (instance) => {
};
export
const
UPDATE_DIMENSIONS_DELAY
=
200
;
const
defaultOptions
=
{
modelManager
:
undefined
,
store
:
undefined
,
file
:
undefined
,
options
:
{},
};
export
class
EditorWebIdeExtension
extends
SourceEditorExtension
{
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
);
}
}
});
const
addActions
=
(
instance
,
store
)
=>
{
const
getKeyCode
=
(
key
)
=>
{
const
monacoKeyMod
=
key
.
indexOf
(
'
KEY_
'
)
===
0
;
EditorWebIdeExtension
.
addActions
(
instance
)
;
}
return
monacoKeyMod
?
KeyCode
[
key
]
:
KeyMod
[
key
]
;
}
;
static
addActions
(
instance
)
{
const
{
store
}
=
instance
;
const
getKeyCode
=
(
key
)
=>
{
const
monacoKeyMod
=
key
.
indexOf
(
'
KEY_
'
)
===
0
;
keymap
.
forEach
((
command
)
=>
{
const
{
bindings
,
id
,
label
,
action
}
=
command
;
return
monacoKeyMod
?
KeyCode
[
key
]
:
KeyMod
[
key
];
}
;
const
keybindings
=
bindings
.
map
((
binding
)
=>
{
const
keys
=
binding
.
split
(
'
+
'
)
;
keymap
.
forEach
((
command
)
=>
{
const
{
bindings
,
id
,
label
,
action
}
=
command
;
const
keybindings
=
bindings
.
map
((
binding
)
=>
{
const
keys
=
binding
.
split
(
'
+
'
);
// eslint-disable-next-line no-bitwise
return
keys
.
length
>
1
?
getKeyCode
(
keys
[
0
])
|
getKeyCode
(
keys
[
1
])
:
getKeyCode
(
keys
[
0
]);
});
instance
.
addAction
({
id
,
label
,
keybindings
,
run
()
{
store
.
dispatch
(
action
.
name
,
action
.
params
);
return
null
;
},
});
// eslint-disable-next-line no-bitwise
return
keys
.
length
>
1
?
getKeyCode
(
keys
[
0
])
|
getKeyCode
(
keys
[
1
])
:
getKeyCode
(
keys
[
0
]);
});
}
createModel
(
file
,
head
=
null
)
{
return
this
.
modelManager
.
addModel
(
file
,
head
);
}
attachModel
(
model
)
{
if
(
isDiffEditorType
(
this
))
{
this
.
setModel
({
original
:
model
.
getOriginalModel
(),
modified
:
model
.
getModel
(),
});
return
;
}
this
.
setModel
(
model
.
getModel
());
instance
.
addAction
({
id
,
label
,
keybindings
,
run
()
{
store
.
dispatch
(
action
.
name
,
action
.
params
);
return
null
;
},
});
});
};
this
.
updateOptions
(
editorOptions
.
reduce
((
acc
,
obj
)
=>
{
Object
.
keys
(
obj
).
forEach
((
key
)
=>
{
Object
.
assign
(
acc
,
{
[
key
]:
obj
[
key
](
model
),
});
});
return
acc
;
},
{}),
);
}
const
renderSideBySide
=
(
domElement
)
=>
{
return
domElement
.
offsetWidth
>=
700
;
};
attachMergeRequestModel
(
model
)
{
this
.
setModel
({
original
:
model
.
getBaseModel
(),
modified
:
model
.
getModel
(),
const
updateInstanceDimensions
=
(
instance
)
=>
{
instance
.
layout
();
if
(
isDiffEditorType
(
instance
))
{
instance
.
updateOptions
({
renderSideBySide
:
renderSideBySide
(
instance
.
getDomNode
()),
});
}
};
updateDimensions
()
{
this
.
layout
();
this
.
updateDiffView
()
;
export
class
EditorWebIdeExtension
{
static
get
extensionName
()
{
return
'
EditorWebIde
'
;
}
setPos
({
lineNumber
,
column
})
{
this
.
revealPositionInCenter
({
lineNumber
,
column
,
});
this
.
setPosition
({
lineNumber
,
column
,
});
/**
* 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
);
}
onPositionChange
(
cb
)
{
if
(
!
this
.
onDidChangeCursorPosition
)
{
return
;
}
onUse
(
instance
)
{
window
.
addEventListener
(
'
resize
'
,
this
.
debouncedUpdate
,
false
);
this
.
disposable
.
add
(
this
.
onDidChangeCursorPosition
((
e
)
=>
cb
(
this
,
e
)));
instance
.
onDidDispose
(()
=>
{
this
.
onUnuse
();
});
}
updateDiffView
()
{
if
(
!
isDiffEditorType
(
this
))
{
return
;
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
);
}
}
this
.
updateOptions
({
renderSideBySide
:
EditorWebIdeExtension
.
renderSideBySide
(
this
.
getDomNode
()),
});
}
replaceSelectedText
(
text
)
{
let
selection
=
this
.
getSelection
();
const
range
=
new
Range
(
selection
.
startLineNumber
,
selection
.
startColumn
,
selection
.
endLineNumber
,
selection
.
endColumn
,
);
provides
()
{
return
{
createModel
:
(
instance
,
file
,
head
=
null
)
=>
{
return
this
.
modelManager
.
addModel
(
file
,
head
);
},
attachModel
:
(
instance
,
model
)
=>
{
if
(
isDiffEditorType
(
instance
))
{
instance
.
setModel
({
original
:
model
.
getOriginalModel
(),
modified
:
model
.
getModel
(),
});
this
.
executeEdits
(
''
,
[{
range
,
text
}]);
return
;
}
selection
=
this
.
getSelection
();
this
.
setPosition
({
lineNumber
:
selection
.
endLineNumber
,
column
:
selection
.
endColumn
});
}
instance
.
setModel
(
model
.
getModel
());
instance
.
updateOptions
(
editorOptions
.
reduce
((
acc
,
obj
)
=>
{
Object
.
keys
(
obj
).
forEach
((
key
)
=>
{
Object
.
assign
(
acc
,
{
[
key
]:
obj
[
key
](
model
),
});
});
return
acc
;
},
{}),
);
},
attachMergeRequestModel
:
(
instance
,
model
)
=>
{
instance
.
setModel
({
original
:
model
.
getBaseModel
(),
modified
:
model
.
getModel
(),
});
},
updateDimensions
:
(
instance
)
=>
updateInstanceDimensions
(
instance
),
setPos
:
(
instance
,
{
lineNumber
,
column
})
=>
{
instance
.
revealPositionInCenter
({
lineNumber
,
column
,
});
instance
.
setPosition
({
lineNumber
,
column
,
});
},
onPositionChange
:
(
instance
,
cb
)
=>
{
if
(
typeof
instance
.
onDidChangeCursorPosition
!==
'
function
'
)
{
return
;
}
static
renderSideBySide
(
domElement
)
{
return
domElement
.
offsetWidth
>=
700
;
this
.
disposable
.
add
(
instance
.
onDidChangeCursorPosition
((
e
)
=>
cb
(
instance
,
e
)));
},
replaceSelectedText
:
(
instance
,
text
)
=>
{
let
selection
=
instance
.
getSelection
();
const
range
=
new
Range
(
selection
.
startLineNumber
,
selection
.
startColumn
,
selection
.
endLineNumber
,
selection
.
endColumn
,
);
instance
.
executeEdits
(
''
,
[{
range
,
text
}]);
selection
=
instance
.
getSelection
();
instance
.
setPosition
({
lineNumber
:
selection
.
endLineNumber
,
column
:
selection
.
endColumn
});
},
};
}
}
app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
View file @
021bb329
This diff is collapsed.
Click to expand it.
app/assets/javascripts/editor/source_editor.js
View file @
021bb329
import
{
editor
as
monacoEditor
,
Uri
}
from
'
monaco-editor
'
;
import
{
waitForCSSLoaded
}
from
'
~/helpers/startup_css_helper
'
;
import
{
defaultEditorOptions
}
from
'
~/ide/lib/editor_options
'
;
import
languages
from
'
~/ide/lib/languages
'
;
import
{
registerLanguages
}
from
'
~/ide/utils
'
;
...
...
@@ -11,10 +12,39 @@ import {
EDITOR_TYPE_DIFF
,
}
from
'
./constants
'
;
import
{
clearDomElement
,
setupEditorTheme
,
getBlobLanguage
}
from
'
./utils
'
;
import
EditorInstance
from
'
./source_editor_instance
'
;
const
instanceRemoveFromRegistry
=
(
editor
,
instance
)
=>
{
const
index
=
editor
.
instances
.
findIndex
((
inst
)
=>
inst
===
instance
);
editor
.
instances
.
splice
(
index
,
1
);
};
const
instanceDisposeModels
=
(
editor
,
instance
,
model
)
=>
{
const
instanceModel
=
instance
.
getModel
()
||
model
;
if
(
!
instanceModel
)
{
return
;
}
if
(
instance
.
getEditorType
()
===
EDITOR_TYPE_DIFF
)
{
const
{
original
,
modified
}
=
instanceModel
;
if
(
original
)
{
original
.
dispose
();
}
if
(
modified
)
{
modified
.
dispose
();
}
}
else
{
instanceModel
.
dispose
();
}
};
export
default
class
SourceEditor
{
/**
* Constructs a global editor.
* @param {Object} options - Monaco config options used to create the editor
*/
constructor
(
options
=
{})
{
this
.
instances
=
[];
this
.
extensionsStore
=
new
Map
();
this
.
options
=
{
extraEditorClassName
:
'
gl-source-editor
'
,
...
defaultEditorOptions
,
...
...
@@ -26,19 +56,6 @@ export default class SourceEditor {
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
)
{
if
(
!
el
)
{
throw
new
Error
(
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL
);
...
...
@@ -78,71 +95,17 @@ export default class SourceEditor {
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.
*
* @param {Object} options Options used to initialize monaco.
* @param {Element} options.el The element which will be used to create the monacoEditor.
* Creates a Source Editor Instance with the given options.
* @param {Object} options Options used to initialize the instance.
* @param {Element} options.el The element to attach the instance for.
* @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language.
* @param {string} options.blobContent The content to initialize the monacoEditor.
* @param {string} options.blobOriginalContent The original blob's content. Is used when creating a Diff Instance.
* @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
* @param {Boolean} options.isDiff Flag to enable creation of a Diff Instance?
* @param {...*} options.instanceOptions Configuration options used to instantiate an instance.
* @returns {EditorInstance}
*/
createInstance
({
el
=
undefined
,
...
...
@@ -156,13 +119,18 @@ export default class SourceEditor {
SourceEditor
.
prepareInstance
(
el
);
const
createEditorFn
=
isDiff
?
'
createDiffEditor
'
:
'
create
'
;
const
instance
=
SourceEditor
.
convertMonacoToEL
Instance
(
const
instance
=
new
Editor
Instance
(
monacoEditor
[
createEditorFn
].
call
(
this
,
el
,
{
...
this
.
options
,
...
instanceOptions
,
}),
this
.
extensionsStore
,
);
waitForCSSLoaded
(()
=>
{
instance
.
layout
();
});
let
model
;
if
(
instanceOptions
.
model
!==
null
)
{
model
=
SourceEditor
.
createEditorModel
({
...
...
@@ -176,8 +144,8 @@ export default class SourceEditor {
}
instance
.
onDidDispose
(()
=>
{
SourceEditor
.
instanceRemoveFromRegistry
(
this
,
instance
);
SourceEditor
.
instanceDisposeModels
(
this
,
instance
,
model
);
instanceRemoveFromRegistry
(
this
,
instance
);
instanceDisposeModels
(
this
,
instance
,
model
);
});
this
.
instances
.
push
(
instance
);
...
...
@@ -185,6 +153,11 @@ export default class SourceEditor {
return
instance
;
}
/**
* Create a Diff Instance
* @param {Object} args Options to be passed further down to createInstance() with the same signature
* @returns {EditorInstance}
*/
createDiffInstance
(
args
)
{
return
this
.
createInstance
({
...
args
,
...
...
@@ -192,6 +165,10 @@ export default class SourceEditor {
});
}
/**
* Dispose global editor
* Automatically disposes all the instances registered for this editor
*/
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 {
if
(
typeof
definition
!==
'
function
'
)
{
throw
new
Error
(
EDITOR_EXTENSION_DEFINITION_ERROR
);
}
this
.
name
=
definition
.
name
;
// both class- and fn-based extensions have a name
this
.
setupOptions
=
setupOptions
;
// eslint-disable-next-line new-cap
this
.
obj
=
new
definition
();
this
.
extensionName
=
definition
.
extensionName
||
this
.
obj
.
extensionName
;
// both class- and fn-based extensions have a name
}
get
api
()
{
...
...
app/assets/javascripts/editor/source_editor_instance.js
View file @
021bb329
...
...
@@ -13,7 +13,7 @@
* A Source Editor Extension
* @typedef {Object} SourceEditorExtension
* @property {Object} obj
* @property {string}
n
ame
* @property {string}
extensionN
ame
* @property {Object} api
*/
...
...
@@ -43,12 +43,12 @@ const utils = {
}
},
getStoredExtension
:
(
extensionsStore
,
n
ame
)
=>
{
getStoredExtension
:
(
extensionsStore
,
extensionN
ame
)
=>
{
if
(
!
extensionsStore
)
{
logError
(
EDITOR_EXTENSION_STORE_IS_MISSING_ERROR
);
return
undefined
;
}
return
extensionsStore
.
get
(
n
ame
);
return
extensionsStore
.
get
(
extensionN
ame
);
},
};
...
...
@@ -73,30 +73,18 @@ export default class EditorInstance {
if
(
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
);
},
set
(
target
,
prop
,
value
)
{
Object
.
assign
(
seInstance
,
{
[
prop
]:
value
,
});
return
true
;
},
};
const
instProxy
=
new
Proxy
(
rootInstance
,
getHandler
);
/**
* 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
);
this
.
dispatchExtAction
=
EditorInstance
.
useUnuse
.
bind
(
instProxy
,
extensionsStore
);
return
instProxy
;
}
...
...
@@ -141,7 +129,7 @@ export default class EditorInstance {
}
// Existing Extension Path
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
definition
.
n
ame
);
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
definition
.
extensionN
ame
);
if
(
existingExt
)
{
if
(
isEqual
(
extension
.
setupOptions
,
existingExt
.
setupOptions
))
{
return
existingExt
;
...
...
@@ -168,14 +156,14 @@ export default class EditorInstance {
* @param {Map} extensionsStore - The global registry for the extension instances
*/
registerExtension
(
extension
,
extensionsStore
)
{
const
{
n
ame
}
=
extension
;
const
{
extensionN
ame
}
=
extension
;
const
hasExtensionRegistered
=
extensionsStore
.
has
(
n
ame
)
&&
isEqual
(
extension
.
setupOptions
,
extensionsStore
.
get
(
n
ame
).
setupOptions
);
extensionsStore
.
has
(
extensionN
ame
)
&&
isEqual
(
extension
.
setupOptions
,
extensionsStore
.
get
(
extensionN
ame
).
setupOptions
);
if
(
hasExtensionRegistered
)
{
return
;
}
extensionsStore
.
set
(
n
ame
,
extension
);
extensionsStore
.
set
(
extensionN
ame
,
extension
);
const
{
obj
:
extensionObj
}
=
extension
;
if
(
extensionObj
.
onUse
)
{
extensionObj
.
onUse
(
this
);
...
...
@@ -187,7 +175,7 @@ export default class EditorInstance {
* @param {SourceEditorExtension} extension - Instance of Source Editor extension
*/
registerExtensionMethods
(
extension
)
{
const
{
api
,
n
ame
}
=
extension
;
const
{
api
,
extensionN
ame
}
=
extension
;
if
(
!
api
)
{
return
;
...
...
@@ -197,7 +185,7 @@ export default class EditorInstance {
if
(
this
[
prop
])
{
logError
(
sprintf
(
EDITOR_EXTENSION_NAMING_CONFLICT_ERROR
,
{
prop
}));
}
else
{
this
.
methods
[
prop
]
=
n
ame
;
this
.
methods
[
prop
]
=
extensionN
ame
;
}
},
this
);
}
...
...
@@ -215,10 +203,10 @@ export default class EditorInstance {
if
(
!
extension
)
{
throw
new
Error
(
EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR
);
}
const
{
n
ame
}
=
extension
;
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
n
ame
);
const
{
extensionN
ame
}
=
extension
;
const
existingExt
=
utils
.
getStoredExtension
(
extensionsStore
,
extensionN
ame
);
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
;
if
(
extensionObj
.
onBeforeUnuse
)
{
...
...
@@ -235,12 +223,12 @@ export default class EditorInstance {
* @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
*/
unregisterExtensionMethods
(
extension
)
{
const
{
api
,
n
ame
}
=
extension
;
const
{
api
,
extensionN
ame
}
=
extension
;
if
(
!
api
)
{
return
;
}
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 {
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.
* @returns {Array}
...
...
app/assets/javascripts/ide/components/repo_editor.vue
View file @
021bb329
...
...
@@ -7,6 +7,7 @@ import {
EDITOR_CODE_INSTANCE_FN
,
EDITOR_DIFF_INSTANCE_FN
,
}
from
'
~/editor/constants
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
EditorWebIdeExtension
}
from
'
~/editor/extensions/source_editor_webide_ext
'
;
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
createFlash
from
'
~/flash
'
;
...
...
@@ -302,30 +303,32 @@ export default {
...
instanceOptions
,
...
this
.
editorOptions
,
});
this
.
editor
.
use
(
new
EditorWebIdeExtension
({
instance
:
this
.
editor
,
modelManager
:
this
.
modelManager
,
store
:
this
.
$store
,
file
:
this
.
file
,
options
:
this
.
editorOptions
,
}),
);
this
.
editor
.
use
([
{
definition
:
SourceEditorExtension
,
},
{
definition
:
EditorWebIdeExtension
,
setupOptions
:
{
modelManager
:
this
.
modelManager
,
store
:
this
.
$store
,
file
:
this
.
file
,
options
:
this
.
editorOptions
,
},
},
]);
if
(
this
.
fileType
===
MARKDOWN_FILE_TYPE
&&
this
.
editor
?.
getEditorType
()
===
EDITOR_TYPE_CODE
&&
this
.
previewMarkdownPath
)
{
import
(
'
~/editor/extensions/source_editor_markdown_ext
'
)
.
then
(({
EditorMarkdownExtension
:
MarkdownExtension
}
=
{})
=>
{
this
.
editor
.
use
(
new
MarkdownExtension
({
instance
:
this
.
editor
,
previewMarkdownPath
:
this
.
previewMarkdownPath
,
}),
);
import
(
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
)
.
then
(({
EditorMarkdownPreviewExtension
:
MarkdownLivePreview
})
=>
{
this
.
editor
.
use
({
definition
:
MarkdownLivePreview
,
setupOptions
:
{
previewMarkdownPath
:
this
.
previewMarkdownPath
},
});
})
.
catch
((
e
)
=>
createFlash
({
...
...
app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
View file @
021bb329
...
...
@@ -19,7 +19,7 @@ export default {
if
(
this
.
glFeatures
.
schemaLinting
)
{
const
editorInstance
=
this
.
$refs
.
editor
.
getEditor
();
editorInstance
.
use
(
new
CiSchemaExtension
({
instance
:
editorInstance
})
);
editorInstance
.
use
(
{
definition
:
CiSchemaExtension
}
);
editorInstance
.
registerCiSchema
();
}
},
...
...
spec/frontend/blob_edit/edit_blob_spec.js
View file @
021bb329
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
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
{
EditorMarkdownExtension
}
from
'
~/editor/extensions/source_editor_markdown_ext
'
;
import
{
EditorMarkdownPreviewExtension
}
from
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
;
import
SourceEditor
from
'
~/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_markdown_ext
'
);
jest
.
mock
(
'
~/editor/extensions/source_editor_markdown_livepreview_ext
'
);
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
'
,
()
=>
{
const
useMock
=
jest
.
fn
();
...
...
@@ -29,7 +44,9 @@ describe('Blob Editing', () => {
jest
.
spyOn
(
SourceEditor
.
prototype
,
'
createInstance
'
).
mockReturnValue
(
mockInstance
);
});
afterEach
(()
=>
{
SourceEditorExtension
.
mockClear
();
EditorMarkdownExtension
.
mockClear
();
EditorMarkdownPreviewExtension
.
mockClear
();
FileTemplateExtension
.
mockClear
();
});
...
...
@@ -45,26 +62,22 @@ describe('Blob Editing', () => {
await
waitForPromises
();
};
it
(
'
loads FileTemplateExtension by default
'
,
async
()
=>
{
it
(
'
loads
SourceEditorExtension and
FileTemplateExtension by default
'
,
async
()
=>
{
await
initEditor
();
expect
(
useMock
).
toHaveBeenCalledWith
(
expect
.
any
(
FileTemplateExtension
));
expect
(
FileTemplateExtension
).
toHaveBeenCalledTimes
(
1
);
expect
(
useMock
).
toHaveBeenCalledWith
(
defaultExtensions
);
});
describe
(
'
Markdown
'
,
()
=>
{
it
(
'
does not load MarkdownExtension by default
'
,
async
()
=>
{
it
(
'
does not load MarkdownExtension
s
by default
'
,
async
()
=>
{
await
initEditor
();
expect
(
EditorMarkdownExtension
).
not
.
toHaveBeenCalled
();
expect
(
EditorMarkdownPreviewExtension
).
not
.
toHaveBeenCalled
();
});
it
(
'
loads MarkdownExtension only for the markdown files
'
,
async
()
=>
{
await
initEditor
(
true
);
expect
(
useMock
).
toHaveBeenCalledWith
(
expect
.
any
(
EditorMarkdownExtension
));
expect
(
EditorMarkdownExtension
).
toHaveBeenCalledTimes
(
1
);
expect
(
EditorMarkdownExtension
).
toHaveBeenCalledWith
({
instance
:
mockInstance
,
previewMarkdownPath
:
PREVIEW_MARKDOWN_PATH
,
});
expect
(
useMock
).
toHaveBeenCalledTimes
(
2
);
expect
(
useMock
.
mock
.
calls
[
1
]).
toEqual
([
markdownExtensions
]);
});
});
...
...
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
{
static
get
extensionName
()
{
return
'
SEClassExtension
'
;
}
// eslint-disable-next-line class-methods-use-this
provides
()
{
return
{
...
...
@@ -10,6 +28,7 @@ export class SEClassExtension {
export
function
SEFnExtension
()
{
return
{
extensionName
:
'
SEFnExtension
'
,
fnExtMethod
:
()
=>
'
fn own method
'
,
provides
:
()
=>
{
return
{
...
...
@@ -21,6 +40,7 @@ export function SEFnExtension() {
export
const
SEConstExt
=
()
=>
{
return
{
extensionName
:
'
SEConstExt
'
,
provides
:
()
=>
{
return
{
constExtMethod
:
()
=>
'
const own method
'
,
...
...
@@ -29,36 +49,39 @@ export const SEConstExt = () => {
};
};
export
function
SEWithSetupExt
()
{
return
{
onSetup
:
(
instance
,
setupOptions
=
{})
=>
{
if
(
setupOptions
&&
!
Array
.
isArray
(
setupOptions
))
{
Object
.
entries
(
setupOptions
).
forEach
(([
key
,
value
])
=>
{
Object
.
assign
(
instance
,
{
[
key
]:
value
,
});
export
class
SEWithSetupExt
{
static
get
extensionName
()
{
return
'
SEWithSetupExt
'
;
}
// eslint-disable-next-line class-methods-use-this
onSetup
(
instance
,
setupOptions
=
{})
{
if
(
setupOptions
&&
!
Array
.
isArray
(
setupOptions
))
{
Object
.
entries
(
setupOptions
).
forEach
(([
key
,
value
])
=>
{
Object
.
assign
(
instance
,
{
[
key
]:
value
,
});
}
}
,
provides
:
()
=>
{
return
{
returnInstanceAndProps
:
(
instance
,
stringProp
,
objProp
=
{})
=>
{
return
[
stringProp
,
objProp
,
instance
];
},
returnInstance
:
(
instance
)
=>
{
return
instance
;
},
giveMeContext
:
()
=>
{
return
this
;
},
}
;
}
,
}
;
}
);
}
}
provides
()
{
return
{
returnInstanceAndProps
:
(
instance
,
stringProp
,
objProp
=
{})
=>
{
return
[
stringProp
,
objProp
,
instance
];
},
returnInstance
:
(
instance
)
=>
{
return
instance
;
},
giveMeContext
:
()
=>
{
return
this
;
}
,
}
;
}
}
export
const
conflictingExtensions
=
{
WithInstanceExt
:
()
=>
{
return
{
extensionName
:
'
WithInstanceExt
'
,
provides
:
()
=>
{
return
{
use
:
()
=>
'
A conflict with instance
'
,
...
...
@@ -69,6 +92,7 @@ export const conflictingExtensions = {
},
WithAnotherExt
:
()
=>
{
return
{
extensionName
:
'
WithAnotherExt
'
,
provides
:
()
=>
{
return
{
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', () => {
blobPath
,
blobContent
:
''
,
});
instance
.
use
(
new
CiSchemaExtension
()
);
instance
.
use
(
{
definition
:
CiSchemaExtension
}
);
};
beforeAll
(()
=>
{
...
...
spec/frontend/editor/source_editor_extension_base_spec.js
View file @
021bb329
This diff is collapsed.
Click to expand it.
spec/frontend/editor/source_editor_extension_spec.js
View file @
021bb329
...
...
@@ -40,7 +40,7 @@ describe('Editor Extension', () => {
expect
(
extension
).
toEqual
(
expect
.
objectContaining
({
n
ame
:
expectedName
,
extensionN
ame
:
expectedName
,
setupOptions
,
}),
);
...
...
spec/frontend/editor/source_editor_instance_spec.js
View file @
021bb329
...
...
@@ -32,11 +32,17 @@ describe('Source Editor Instance', () => {
];
const
fooFn
=
jest
.
fn
();
const
fooProp
=
'
foo
'
;
class
DummyExt
{
// eslint-disable-next-line class-methods-use-this
get
extensionName
()
{
return
'
DummyExt
'
;
}
// eslint-disable-next-line class-methods-use-this
provides
()
{
return
{
fooFn
,
fooProp
,
};
}
}
...
...
@@ -64,7 +70,7 @@ describe('Source Editor Instance', () => {
});
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
.
use
({
definition
:
DummyExt
});
...
...
@@ -73,6 +79,13 @@ describe('Source Editor Instance', () => {
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
`
stringPropToPass | objPropToPass | setupOptions
${
undefined
}
|
${
undefined
}
|
${
undefined
}
...
...
@@ -118,20 +131,20 @@ describe('Source Editor Instance', () => {
it
(
"
correctly sets the context of the 'this' keyword for the extension's methods
"
,
()
=>
{
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
'
,
()
=>
{
seInstance
=
new
SourceEditorInstance
({
use
:
fooFn
,
});
jest
.
spyOn
(
seInstanc
e
,
'
use
'
).
mockImplementation
(()
=>
{});
expect
(
s
eInstance
.
use
).
not
.
toHaveBeenCalled
();
const
spy
=
jest
.
spyOn
(
seInstance
.
constructor
.
prototyp
e
,
'
use
'
).
mockImplementation
(()
=>
{});
expect
(
s
py
).
not
.
toHaveBeenCalled
();
expect
(
fooFn
).
not
.
toHaveBeenCalled
();
seInstance
.
use
();
expect
(
s
eInstance
.
use
).
toHaveBeenCalled
();
expect
(
s
py
).
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', () => {
let
instance
;
let
editorEl
;
let
mockAxios
;
const
previewMarkdownPath
=
'
/gitlab/fooGroup/barProj/preview_markdown
'
;
const
firstLine
=
'
This is a
'
;
const
secondLine
=
'
multiline
'
;
const
thirdLine
=
'
string with some **markup**
'
;
...
...
@@ -36,7 +35,7 @@ describe('Markdown Extension for Source Editor', () => {
blobPath
:
markdownPath
,
blobContent
:
text
,
});
instance
.
use
(
new
EditorMarkdownExtension
({
instance
,
previewMarkdownPath
})
);
instance
.
use
(
{
definition
:
EditorMarkdownExtension
}
);
});
afterEach
(()
=>
{
...
...
@@ -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
'
,
()
=>
{
jest
.
spyOn
(
instance
,
'
getSelectedText
'
);
const
toSelect
=
'
string
'
;
selectSecondAndThirdLines
();
instance
.
selectWithinSelection
(
toSelect
);
expect
(
instance
.
getSelectedText
).
toHaveBeenCalled
();
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';
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
{
spyOnApi
}
from
'
./helpers
'
;
jest
.
mock
(
'
~/syntax_highlight
'
);
jest
.
mock
(
'
~/flash
'
);
...
...
@@ -23,6 +24,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let
editorEl
;
let
panelSpy
;
let
mockAxios
;
let
extension
;
const
previewMarkdownPath
=
'
/gitlab/fooGroup/barProj/preview_markdown
'
;
const
firstLine
=
'
This is a
'
;
const
secondLine
=
'
multiline
'
;
...
...
@@ -47,8 +49,11 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
blobPath
:
markdownPath
,
blobContent
:
text
,
});
instance
.
use
(
new
EditorMarkdownPreviewExtension
({
instance
,
previewMarkdownPath
}));
panelSpy
=
jest
.
spyOn
(
EditorMarkdownPreviewExtension
,
'
togglePreviewPanel
'
);
extension
=
instance
.
use
({
definition
:
EditorMarkdownPreviewExtension
,
setupOptions
:
{
previewMarkdownPath
},
});
panelSpy
=
jest
.
spyOn
(
extension
.
obj
.
constructor
.
prototype
,
'
togglePreviewPanel
'
);
});
afterEach
(()
=>
{
...
...
@@ -57,14 +62,14 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios
.
restore
();
});
it
(
'
sets up the instance
'
,
()
=>
{
expect
(
instance
.
p
review
).
toEqual
({
it
(
'
sets up the
preview on the
instance
'
,
()
=>
{
expect
(
instance
.
markdownP
review
).
toEqual
({
el
:
undefined
,
action
:
expect
.
any
(
Object
),
shown
:
false
,
modelChangeListener
:
undefined
,
path
:
previewMarkdownPath
,
});
expect
(
instance
.
previewMarkdownPath
).
toBe
(
previewMarkdownPath
);
});
describe
(
'
model language changes listener
'
,
()
=>
{
...
...
@@ -72,14 +77,22 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let
actionSpy
;
beforeEach
(
async
()
=>
{
cleanupSpy
=
jest
.
spyOn
(
instance
,
'
cleanup
'
);
actionSpy
=
jest
.
spyOn
(
instance
,
'
setupPreviewAction
'
);
cleanupSpy
=
jest
.
fn
();
actionSpy
=
jest
.
fn
();
spyOnApi
(
extension
,
{
cleanup
:
cleanupSpy
,
setupPreviewAction
:
actionSpy
,
});
await
togglePreview
();
});
afterEach
(()
=>
{
jest
.
clearAllMocks
();
});
it
(
'
cleans up when switching away from markdown
'
,
()
=>
{
expect
(
instance
.
cleanup
).
not
.
toHaveBeenCalled
();
expect
(
instance
.
setupPreviewAction
).
not
.
toHaveBeenCalled
();
expect
(
cleanupSpy
).
not
.
toHaveBeenCalled
();
expect
(
actionSpy
).
not
.
toHaveBeenCalled
();
instance
.
updateModelLanguage
(
plaintextPath
);
...
...
@@ -110,8 +123,12 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let
actionSpy
;
beforeEach
(()
=>
{
cleanupSpy
=
jest
.
spyOn
(
instance
,
'
cleanup
'
);
actionSpy
=
jest
.
spyOn
(
instance
,
'
setupPreviewAction
'
);
cleanupSpy
=
jest
.
fn
();
actionSpy
=
jest
.
fn
();
spyOnApi
(
extension
,
{
cleanup
:
cleanupSpy
,
setupPreviewAction
:
actionSpy
,
});
instance
.
togglePreview
();
});
...
...
@@ -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
'
,
()
=>
{
expect
(
instance
.
preview
.
modelChangeListener
).
toBeDefined
();
jest
.
spyOn
(
instance
,
'
fetchPreview
'
);
expect
(
instance
.
markdownPreview
.
modelChangeListener
).
toBeDefined
();
const
fetchPreviewSpy
=
jest
.
fn
();
spyOnApi
(
extension
,
{
fetchPreview
:
fetchPreviewSpy
,
});
instance
.
cleanup
();
instance
.
setValue
(
'
Foo Bar
'
);
jest
.
advanceTimersByTime
(
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY
);
expect
(
instance
.
fetchPreview
).
not
.
toHaveBeenCalled
();
expect
(
fetchPreviewSpy
).
not
.
toHaveBeenCalled
();
});
it
(
'
removes the contextual menu action
'
,
()
=>
{
...
...
@@ -172,13 +192,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
it
(
'
toggles the `shown` flag
'
,
()
=>
{
expect
(
instance
.
p
review
.
shown
).
toBe
(
true
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
true
);
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
'
,
()
=>
{
const
{
el
:
previewEl
}
=
instance
.
p
review
;
const
{
el
:
previewEl
}
=
instance
.
markdownP
review
;
const
parentEl
=
previewEl
.
parentElement
;
expect
(
previewEl
).
toBeVisible
();
...
...
@@ -200,7 +220,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it
(
'
toggles the layout only if the preview is visible
'
,
()
=>
{
const
{
width
}
=
instance
.
getLayoutInfo
();
expect
(
instance
.
p
review
.
shown
).
toBe
(
true
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
true
);
instance
.
cleanup
();
...
...
@@ -234,13 +254,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
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
();
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
()
=>
{
instance
.
p
review
.
el
=
editorEl
.
parentElement
;
instance
.
markdownP
review
.
el
=
editorEl
.
parentElement
;
await
fetchPreview
();
expect
(
syntaxHighlight
).
toHaveBeenCalled
();
});
...
...
@@ -266,14 +286,17 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
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
);
action
.
run
();
expect
(
instance
.
togglePreview
).
toHaveBeenCalled
();
expect
(
togglePreviewSpy
).
toHaveBeenCalled
();
});
});
...
...
@@ -283,39 +306,39 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
it
(
'
toggles preview flag on instance
'
,
()
=>
{
expect
(
instance
.
p
review
.
shown
).
toBe
(
false
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
false
);
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
shown
).
toBe
(
true
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
true
);
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
shown
).
toBe
(
false
);
expect
(
instance
.
markdownP
review
.
shown
).
toBe
(
false
);
});
describe
(
'
panel DOM element set up
'
,
()
=>
{
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
();
expect
(
instance
.
p
review
.
el
).
toBeDefined
();
expect
(
instance
.
preview
.
el
.
classList
.
contains
(
EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS
)).
toBe
(
true
,
);
expect
(
instance
.
markdownP
review
.
el
).
toBeDefined
();
expect
(
instance
.
markdownPreview
.
el
.
classList
.
contains
(
EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS
)
,
)
.
toBe
(
true
)
;
});
it
(
'
re-uses existing preview DOM element on repeated calls
'
,
()
=>
{
instance
.
togglePreview
();
const
origPreviewEl
=
instance
.
p
review
.
el
;
const
origPreviewEl
=
instance
.
markdownP
review
.
el
;
instance
.
togglePreview
();
expect
(
instance
.
p
review
.
el
).
toBe
(
origPreviewEl
);
expect
(
instance
.
markdownP
review
.
el
).
toBe
(
origPreviewEl
);
});
it
(
'
hides the preview DOM element by default
'
,
()
=>
{
panelSpy
.
mockImplementation
();
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', () => {
it
(
'
toggles visibility of the preview DOM element
'
,
async
()
=>
{
await
togglePreview
();
expect
(
instance
.
p
review
.
el
.
style
.
display
).
toBe
(
'
block
'
);
expect
(
instance
.
markdownP
review
.
el
.
style
.
display
).
toBe
(
'
block
'
);
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
'
,
()
=>
{
...
...
@@ -367,9 +390,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
it
(
'
stores disposable listener for model changes
'
,
async
()
=>
{
expect
(
instance
.
p
review
.
modelChangeListener
).
toBeUndefined
();
expect
(
instance
.
markdownP
review
.
modelChangeListener
).
toBeUndefined
();
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', () => {
it
(
'
disposes the model change event listener
'
,
()
=>
{
const
disposeSpy
=
jest
.
fn
();
instance
.
p
review
.
modelChangeListener
=
{
instance
.
markdownP
review
.
modelChangeListener
=
{
dispose
:
disposeSpy
,
};
instance
.
togglePreview
();
...
...
spec/frontend/editor/source_editor_spec.js
View file @
021bb329
This diff is collapsed.
Click to expand it.
spec/frontend/editor/source_editor_yaml_ext_spec.js
View file @
021bb329
...
...
@@ -2,6 +2,10 @@ import { Document } from 'yaml';
import
SourceEditor
from
'
~/editor/source_editor
'
;
import
{
YamlEditorExtension
}
from
'
~/editor/extensions/source_editor_yaml_ext
'
;
import
{
SourceEditorExtension
}
from
'
~/editor/extensions/source_editor_extension_base
'
;
import
{
spyOnApi
}
from
'
jest/editor/helpers
'
;
let
baseExtension
;
let
yamlExtension
;
const
getEditorInstance
=
(
editorInstanceOptions
=
{})
=>
{
setFixtures
(
'
<div id="editor"></div>
'
);
...
...
@@ -16,7 +20,10 @@ const getEditorInstance = (editorInstanceOptions = {}) => {
const
getEditorInstanceWithExtension
=
(
extensionOptions
=
{},
editorInstanceOptions
=
{})
=>
{
setFixtures
(
'
<div id="editor"></div>
'
);
const
instance
=
getEditorInstance
(
editorInstanceOptions
);
instance
.
use
(
new
YamlEditorExtension
({
instance
,
...
extensionOptions
}));
[
baseExtension
,
yamlExtension
]
=
instance
.
use
([
{
definition
:
SourceEditorExtension
},
{
definition
:
YamlEditorExtension
,
setupOptions
:
extensionOptions
},
]);
// Remove the below once
// https://gitlab.com/gitlab-org/gitlab/-/issues/325992 is resolved
...
...
@@ -29,19 +36,16 @@ const getEditorInstanceWithExtension = (extensionOptions = {}, editorInstanceOpt
describe
(
'
YamlCreatorExtension
'
,
()
=>
{
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
({
highlightPath
:
'
foo
'
,
highlightPath
,
enableComments
:
true
,
});
expect
(
instance
).
toEqual
(
expect
.
objectContaining
({
options
:
expect
.
objectContaining
({
highlightPath
:
'
foo
'
,
enableComments
:
true
,
}),
}),
);
expect
(
yamlExtension
.
obj
.
highlightPath
).
toBe
(
highlightPath
);
expect
(
yamlExtension
.
obj
.
enableComments
).
toBe
(
true
);
expect
(
instance
.
highlightPath
).
toBeUndefined
();
expect
(
instance
.
enableComments
).
toBeUndefined
();
});
it
(
'
dumps values loaded with the model constructor options
'
,
()
=>
{
...
...
@@ -55,7 +59,7 @@ describe('YamlCreatorExtension', () => {
it
(
'
registers the onUpdate() function
'
,
()
=>
{
const
instance
=
getEditorInstance
();
const
onDidChangeModelContent
=
jest
.
spyOn
(
instance
,
'
onDidChangeModelContent
'
);
instance
.
use
(
new
YamlEditorExtension
({
instance
})
);
instance
.
use
(
{
definition
:
YamlEditorExtension
}
);
expect
(
onDidChangeModelContent
).
toHaveBeenCalledWith
(
expect
.
any
(
Function
));
});
...
...
@@ -82,21 +86,21 @@ describe('YamlCreatorExtension', () => {
it
(
'
should call transformComments if enableComments is true
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
({
enableComments
:
true
});
const
transformComments
=
jest
.
spyOn
(
YamlEditorExtension
,
'
transformComments
'
);
YamlEditorExtension
.
initFromModel
(
instance
,
model
);
instance
.
initFromModel
(
model
);
expect
(
transformComments
).
toHaveBeenCalled
();
});
it
(
'
should not call transformComments if enableComments is false
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
({
enableComments
:
false
});
const
transformComments
=
jest
.
spyOn
(
YamlEditorExtension
,
'
transformComments
'
);
YamlEditorExtension
.
initFromModel
(
instance
,
model
);
instance
.
initFromModel
(
model
);
expect
(
transformComments
).
not
.
toHaveBeenCalled
();
});
it
(
'
should call setValue with the stringified model
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
();
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
YamlEditorExtension
.
initFromModel
(
instance
,
model
);
instance
.
initFromModel
(
model
);
expect
(
setValue
).
toHaveBeenCalledWith
(
doc
.
toString
());
});
});
...
...
@@ -240,26 +244,35 @@ foo:
it
(
"
should call setValue with the stringified doc if the editor's value is empty
"
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
();
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
const
updateValue
=
jest
.
spyOn
(
instance
,
'
updateValue
'
);
const
updateValueSpy
=
jest
.
fn
();
spyOnApi
(
yamlExtension
,
{
updateValue
:
updateValueSpy
,
});
instance
.
setDoc
(
doc
);
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
"
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
({},
{
value
:
'
asjkdhkasjdh
'
});
const
setValue
=
jest
.
spyOn
(
instance
,
'
setValue
'
);
const
updateValue
=
jest
.
spyOn
(
instance
,
'
updateValue
'
);
const
updateValueSpy
=
jest
.
fn
();
spyOnApi
(
yamlExtension
,
{
updateValue
:
updateValueSpy
,
});
instance
.
setDoc
(
doc
);
expect
(
setValue
).
not
.
toHaveBeenCalled
();
expect
(
updateValue
).
toHaveBeenCalledWith
(
doc
.
toString
());
expect
(
updateValue
Spy
).
toHaveBeenCalledWith
(
instance
,
doc
.
toString
());
});
it
(
'
should trigger the onUpdate method
'
,
()
=>
{
const
instance
=
getEditorInstanceWithExtension
();
const
onUpdate
=
jest
.
spyOn
(
instance
,
'
onUpdate
'
);
const
onUpdateSpy
=
jest
.
fn
();
spyOnApi
(
yamlExtension
,
{
onUpdate
:
onUpdateSpy
,
});
instance
.
setDoc
(
doc
);
expect
(
onUpdate
).
toHaveBeenCalled
();
expect
(
onUpdate
Spy
).
toHaveBeenCalled
();
});
});
...
...
@@ -320,9 +333,12 @@ foo:
it
(
'
calls highlight
'
,
()
=>
{
const
highlightPath
=
'
foo
'
;
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
();
expect
(
instance
.
highlight
).
toHaveBeenCalledWith
(
highlightPath
);
expect
(
highlightSpy
).
toHaveBeenCalledWith
(
instance
,
highlightPath
);
});
});
...
...
@@ -350,8 +366,12 @@ foo:
beforeEach
(()
=>
{
instance
=
getEditorInstanceWithExtension
({
highlightPath
:
highlightPathOnSetup
},
{
value
});
highlightLinesSpy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
highlightLines
'
);
removeHighlightsSpy
=
jest
.
spyOn
(
SourceEditorExtension
,
'
removeHighlights
'
);
highlightLinesSpy
=
jest
.
fn
();
removeHighlightsSpy
=
jest
.
fn
();
spyOnApi
(
baseExtension
,
{
highlightLines
:
highlightLinesSpy
,
removeHighlights
:
removeHighlightsSpy
,
});
});
afterEach
(()
=>
{
...
...
@@ -361,7 +381,7 @@ foo:
it
(
'
saves the highlighted path in highlightPath
'
,
()
=>
{
const
path
=
'
foo.bar
'
;
instance
.
highlight
(
path
);
expect
(
instance
.
options
.
highlightPath
).
toEqual
(
path
);
expect
(
yamlExtension
.
obj
.
highlightPath
).
toEqual
(
path
);
});
it
(
'
calls highlightLines with a number of lines
'
,
()
=>
{
...
...
@@ -374,14 +394,14 @@ foo:
instance
.
highlight
(
null
);
expect
(
removeHighlightsSpy
).
toHaveBeenCalledWith
(
instance
);
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
'
,
()
=>
{
expect
(()
=>
instance
.
highlight
(
'
invalidPath[0]
'
)).
toThrow
(
'
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
(
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';
import
{
exampleConfigs
,
exampleFiles
}
from
'
jest/ide/lib/editorconfig/mock_data
'
;
import
{
EDITOR_CODE_INSTANCE_FN
,
EDITOR_DIFF_INSTANCE_FN
}
from
'
~/editor/constants
'
;
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
RepoEditor
from
'
~/ide/components/repo_editor.vue
'
;
import
{
...
...
@@ -23,6 +23,8 @@ import service from '~/ide/services';
import
{
createStoreOptions
}
from
'
~/ide/stores
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
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
'
;
const
PREVIEW_MARKDOWN_PATH
=
'
/foo/bar/preview_markdown
'
;
...
...
@@ -101,6 +103,7 @@ describe('RepoEditor', () => {
let
createDiffInstanceSpy
;
let
createModelSpy
;
let
applyExtensionSpy
;
let
extensionsStore
;
const
waitForEditorSetup
=
()
=>
new
Promise
((
resolve
)
=>
{
...
...
@@ -120,6 +123,7 @@ describe('RepoEditor', () => {
});
await
waitForPromises
();
vm
=
wrapper
.
vm
;
extensionsStore
=
wrapper
.
vm
.
globalEditor
.
extensionsStore
;
jest
.
spyOn
(
vm
,
'
getFileData
'
).
mockResolvedValue
();
jest
.
spyOn
(
vm
,
'
getRawFileData
'
).
mockResolvedValue
();
};
...
...
@@ -127,28 +131,12 @@ describe('RepoEditor', () => {
const
findEditor
=
()
=>
wrapper
.
find
(
'
[data-testid="editor-container"]
'
);
const
findTabs
=
()
=>
wrapper
.
findAll
(
'
.ide-mode-tabs .nav-links li
'
);
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
(()
=>
{
createInstanceSpy
=
jest
.
spyOn
(
SourceEditor
.
prototype
,
EDITOR_CODE_INSTANCE_FN
);
createDiffInstanceSpy
=
jest
.
spyOn
(
SourceEditor
.
prototype
,
EDITOR_DIFF_INSTANCE_FN
);
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
,
'
getRawFileData
'
).
mockResolvedValue
();
});
...
...
@@ -275,14 +263,13 @@ describe('RepoEditor', () => {
);
it
(
'
installs the WebIDE extension
'
,
async
()
=>
{
const
extensionSpy
=
jest
.
spyOn
(
SourceEditor
,
'
instanceApplyExtension
'
);
await
createComponent
();
expect
(
e
xtensionSpy
).
toHaveBeenCalled
();
Reflect
.
ownKeys
(
EditorWebIdeExtension
.
prototype
)
.
filter
((
fn
)
=>
fn
!==
'
constructor
'
)
.
forEach
((
fn
)
=>
{
expect
(
vm
.
editor
[
fn
]).
toBe
(
EditorWebIdeExtension
.
prototype
[
fn
]
);
});
expect
(
applyE
xtensionSpy
).
toHaveBeenCalled
();
const
ideExtensionApi
=
extensionsStore
.
get
(
'
EditorWebIde
'
).
api
;
Reflect
.
ownKeys
(
ideExtensionApi
).
forEach
((
fn
)
=>
{
expect
(
vm
.
editor
[
fn
]).
toBeDefined
();
expect
(
vm
.
editor
.
methods
[
fn
]).
toBe
(
'
EditorWebIde
'
);
});
});
it
.
each
`
...
...
@@ -301,7 +288,20 @@ describe('RepoEditor', () => {
async
({
activeFile
,
viewer
,
shouldHaveMarkdownExtension
}
=
{})
=>
{
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', () => {
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
'
,
()
=>
{
const
newContent
=
'
As Gregor Samsa
\n
awoke one morning
\n
'
;
vm
.
model
.
setValue
(
newContent
);
...
...
@@ -366,53 +354,48 @@ describe('RepoEditor', () => {
describe
(
'
editor updateDimensions
'
,
()
=>
{
let
updateDimensionsSpy
;
let
updateDiffViewSpy
;
beforeEach
(
async
()
=>
{
await
createComponent
();
updateDimensionsSpy
=
jest
.
spyOn
(
vm
.
editor
,
'
updateDimensions
'
);
updateDiffViewSpy
=
jest
.
spyOn
(
vm
.
editor
,
'
updateDiffView
'
).
mockImplementation
();
const
ext
=
extensionsStore
.
get
(
'
EditorWebIde
'
);
updateDimensionsSpy
=
jest
.
fn
();
spyOnApi
(
ext
,
{
updateDimensions
:
updateDimensionsSpy
,
});
});
it
(
'
calls updateDimensions only when panelResizing is false
'
,
async
()
=>
{
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDiffViewSpy
).
not
.
toHaveBeenCalled
();
expect
(
vm
.
$store
.
state
.
panelResizing
).
toBe
(
false
);
// default value
vm
.
$store
.
state
.
panelResizing
=
true
;
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDiffViewSpy
).
not
.
toHaveBeenCalled
();
vm
.
$store
.
state
.
panelResizing
=
false
;
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
1
);
vm
.
$store
.
state
.
panelResizing
=
true
;
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
1
);
});
it
(
'
calls updateDimensions when rightPane is toggled
'
,
async
()
=>
{
expect
(
updateDimensionsSpy
).
not
.
toHaveBeenCalled
();
expect
(
updateDiffViewSpy
).
not
.
toHaveBeenCalled
();
expect
(
vm
.
$store
.
state
.
rightPane
.
isOpen
).
toBe
(
false
);
// default value
vm
.
$store
.
state
.
rightPane
.
isOpen
=
true
;
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
1
);
vm
.
$store
.
state
.
rightPane
.
isOpen
=
false
;
await
vm
.
$nextTick
();
expect
(
updateDimensionsSpy
).
toHaveBeenCalledTimes
(
2
);
expect
(
updateDiffViewSpy
).
toHaveBeenCalledTimes
(
2
);
});
});
...
...
@@ -447,7 +430,11 @@ describe('RepoEditor', () => {
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
);
await
vm
.
$nextTick
();
...
...
spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
View file @
021bb329
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
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
{
mockCiConfigPath
,
...
...
@@ -59,10 +58,6 @@ describe('Pipeline Editor | Text editor component', () => {
const
findEditor
=
()
=>
wrapper
.
findComponent
(
MockSourceEditor
);
beforeEach
(()
=>
{
SourceEditorExtension
.
deferRerender
=
jest
.
fn
();
});
afterEach
(()
=>
{
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