diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 4dfc27117c000c1acc9bf5edde9f98e606fe38b9..a70611413ae74d79c2a126eea8ded6c9be5a6b3e 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -8,9 +8,10 @@ import ModelManager from './common/model_manager'; import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options'; import { themes } from './themes'; import languages from './languages'; +import schemas from './schemas'; import keymap from './keymap.json'; import { clearDomElement } from '~/editor/utils'; -import { registerLanguages } from '../utils'; +import { registerLanguages, registerSchemas } from '../utils'; function setupThemes() { themes.forEach(theme => { @@ -44,6 +45,7 @@ export default class Editor { setupThemes(); registerLanguages(...languages); + registerSchemas(...schemas); this.debouncedUpdate = debounce(() => { this.updateDimensions(); diff --git a/app/assets/javascripts/ide/lib/schemas/index.js b/app/assets/javascripts/ide/lib/schemas/index.js new file mode 100644 index 0000000000000000000000000000000000000000..38a2f81921b5e1df2ccd24cb3ebab4d06e113d0a --- /dev/null +++ b/app/assets/javascripts/ide/lib/schemas/index.js @@ -0,0 +1,4 @@ +import json from './json'; +import yaml from './yaml'; + +export default [json, yaml]; diff --git a/app/assets/javascripts/ide/lib/schemas/json/index.js b/app/assets/javascripts/ide/lib/schemas/json/index.js new file mode 100644 index 0000000000000000000000000000000000000000..900d5442bec3fc69e97873b9c46086e869826041 --- /dev/null +++ b/app/assets/javascripts/ide/lib/schemas/json/index.js @@ -0,0 +1,8 @@ +export default { + language: 'json', + options: { + validate: true, + enableSchemaRequest: true, + schemas: [], + }, +}; diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js b/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js new file mode 100644 index 0000000000000000000000000000000000000000..af20744abb381d98d25dcd91adc377316a90365f --- /dev/null +++ b/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js @@ -0,0 +1,4 @@ +export default { + uri: 'https://json.schemastore.org/gitlab-ci', + fileMatch: ['*.gitlab-ci.yml'], +}; diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/index.js b/app/assets/javascripts/ide/lib/schemas/yaml/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e3fc406df4bca1b62620640434d741d76f2273d2 --- /dev/null +++ b/app/assets/javascripts/ide/lib/schemas/yaml/index.js @@ -0,0 +1,12 @@ +import gitlabCi from './gitlab_ci'; + +export default { + language: 'yaml', + options: { + validate: true, + enableSchemaRequest: true, + hover: true, + completion: true, + schemas: [gitlabCi], + }, +}; diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index c28a2bd9f1d52c2205e9762a7a59e65a60fd4885..9ec7b2c06ce40a0bd3bdc635f5da3f641bd50115 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -66,7 +66,7 @@ export const trimPathComponents = path => .join('/'); export function registerLanguages(def, ...defs) { - if (defs.length) defs.forEach(lang => registerLanguages(lang)); + defs.forEach(lang => registerLanguages(lang)); const languageId = def.id; @@ -75,6 +75,19 @@ export function registerLanguages(def, ...defs) { languages.setLanguageConfiguration(languageId, def.conf); } +export function registerSchemas({ language, options }, ...schemas) { + schemas.forEach(schema => registerSchemas(schema)); + + const defaults = { + json: languages.json.jsonDefaults, + yaml: languages.yaml.yamlDefaults, + }; + + if (defaults[language]) { + defaults[language].setDiagnosticsOptions(options); + } +} + export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT); export function trimTrailingWhitespace(content) { diff --git a/changelogs/unreleased/218472-gitlab-ci-linting.yml b/changelogs/unreleased/218472-gitlab-ci-linting.yml new file mode 100644 index 0000000000000000000000000000000000000000..0fc937a0a5d9a6d7dc2a319013e41527e6e12185 --- /dev/null +++ b/changelogs/unreleased/218472-gitlab-ci-linting.yml @@ -0,0 +1,5 @@ +--- +title: Add support for linting based on schemas in WebIDE +merge_request: 35838 +author: +type: added diff --git a/config/plugins/monaco_webpack.js b/config/plugins/monaco_webpack.js new file mode 100644 index 0000000000000000000000000000000000000000..7d2837824536591bca5105cbe7762482ab883ace --- /dev/null +++ b/config/plugins/monaco_webpack.js @@ -0,0 +1,17 @@ +const { languagesArr } = require('monaco-editor-webpack-plugin/out/languages'); + +// monaco-yaml library doesn't play so well with monaco-editor-webpack-plugin +// so the only way to include its workers is by patching the list of languages +// in monaco-editor-webpack-plugin and adding support for yaml workers. This is +// a known issue in the library and this workaround was suggested here: +// https://github.com/pengx17/monaco-yaml/issues/20 + +const yamlLang = languagesArr.find(t => t.label === 'yaml'); + +yamlLang.entry = [yamlLang.entry, '../../monaco-yaml/esm/monaco.contribution']; +yamlLang.worker = { + id: 'vs/language/yaml/yamlWorker', + entry: '../../monaco-yaml/esm/yaml.worker.js', +}; + +module.exports = require('monaco-editor-webpack-plugin'); diff --git a/config/webpack.config.js b/config/webpack.config.js index 557db58b1b990d46a5081c363a3657fc92a30b42..8e51ce537c56b3bd508cb519d2a3ce5e011f0155 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -5,7 +5,7 @@ const webpack = require('webpack'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; const CompressionPlugin = require('compression-webpack-plugin'); -const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); +const MonacoWebpackPlugin = require('./plugins/monaco_webpack'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const CopyWebpackPlugin = require('copy-webpack-plugin'); const vendorDllHash = require('./helpers/vendor_dll_hash'); @@ -241,7 +241,7 @@ module.exports = { }, { test: /\.(eot|ttf|woff|woff2)$/, - include: /node_modules\/katex\/dist\/fonts/, + include: /node_modules\/(katex\/dist\/fonts|monaco-editor)/, loader: 'file-loader', options: { name: '[name].[contenthash:8].[ext]', diff --git a/jest.config.base.js b/jest.config.base.js index 1a1fd4e7b620b4289a0541bef279a197872147ec..422b6779af42376488783208c2af5def1f3a552d 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -82,7 +82,9 @@ module.exports = path => { '^.+\\.js$': 'babel-jest', '^.+\\.vue$': 'vue-jest', }, - transformIgnorePatterns: ['node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor)/)'], + transformIgnorePatterns: [ + 'node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor|monaco-yaml)/)', + ], timers: 'fake', testEnvironment: '<rootDir>/spec/frontend/environment.js', testEnvironmentOptions: { diff --git a/package.json b/package.json index 680c850bd9d37d9058dcf83aa4f30711201f5b5b..beeb5cba91da8af6e273540c9395225bd9fa2d8b 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "jquery.caret": "^0.3.1", "jquery.waitforimages": "^2.2.0", "js-cookie": "^2.2.1", + "js-yaml": "^3.13.1", "jszip": "^3.1.3", "jszip-utils": "^0.0.2", "katex": "^0.10.0", @@ -105,8 +106,9 @@ "mermaid": "^8.5.2", "mersenne-twister": "1.1.0", "minimatch": "^3.0.4", - "monaco-editor": "^0.18.1", - "monaco-editor-webpack-plugin": "^1.7.0", + "monaco-editor": "^0.20.0", + "monaco-editor-webpack-plugin": "^1.9.0", + "monaco-yaml": "^2.4.0", "mousetrap": "^1.4.6", "pdfjs-dist": "^2.0.943", "pikaday": "^1.8.0", @@ -220,7 +222,7 @@ }, "resolutions": { "chokidar": "^3.4.0", - "monaco-editor": "0.18.1", + "monaco-editor": "0.20.0", "vue-jest/ts-jest": "24.0.0" }, "engines": { diff --git a/spec/frontend/__mocks__/monaco-editor/index.js b/spec/frontend/__mocks__/monaco-editor/index.js index 7c53cfb51740da0b1abd18a76acedaa01492df43..b9602d69b7450f9c7034b24b581ebbbbc88f7a13 100644 --- a/spec/frontend/__mocks__/monaco-editor/index.js +++ b/spec/frontend/__mocks__/monaco-editor/index.js @@ -8,9 +8,11 @@ import 'monaco-editor/esm/vs/language/css/monaco.contribution'; import 'monaco-editor/esm/vs/language/json/monaco.contribution'; import 'monaco-editor/esm/vs/language/html/monaco.contribution'; import 'monaco-editor/esm/vs/basic-languages/monaco.contribution'; +import 'monaco-yaml/esm/monaco.contribution'; // This language starts trying to spin up web workers which obviously breaks in Jest environment jest.mock('monaco-editor/esm/vs/language/typescript/tsMode'); +jest.mock('monaco-yaml/esm/yamlMode'); export * from 'monaco-editor/esm/vs/editor/editor.api'; export default global.monaco; diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index a77edfc3fa8944d0bfad61f25f4be7dfe251dbf6..a4336b8f2ebfb29d98e53f13556aedd47b918472 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -637,6 +637,8 @@ describe('RepoEditor', () => { // set cursor to line 2, column 1 vm.editor.instance.setSelection(new Range(2, 1, 2, 1)); vm.editor.instance.focus(); + + jest.spyOn(vm.editor.instance, 'hasTextFocus').mockReturnValue(true); }); }); diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js index f5815771cdf516145749b54eed5e2140f4690f84..0952facf3ee9631163e7beff08dcd82ee77cd903 100644 --- a/spec/frontend/ide/lib/editor_spec.js +++ b/spec/frontend/ide/lib/editor_spec.js @@ -199,6 +199,14 @@ describe('Multi-file editor library', () => { }); }); + describe('schemas', () => { + it('registers custom schemas defined with Monaco', () => { + expect(monacoLanguages.yaml.yamlDefaults.diagnosticsOptions).toMatchObject({ + schemas: [{ fileMatch: ['*.gitlab-ci.yml'] }], + }); + }); + }); + describe('replaceSelectedText', () => { let model; let editor; diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js index 15baeca7f36124808b93ddaacf85b84e198030ca..b6de576a0a4d31054e182939e2ba458719ba38d0 100644 --- a/spec/frontend/ide/utils_spec.js +++ b/spec/frontend/ide/utils_spec.js @@ -1,6 +1,7 @@ import { isTextFile, registerLanguages, + registerSchemas, trimPathComponents, insertFinalNewline, trimTrailingWhitespace, @@ -158,6 +159,57 @@ describe('WebIDE utils', () => { }); }); + describe('registerSchemas', () => { + let options; + + beforeEach(() => { + options = { + validate: true, + enableSchemaRequest: true, + hover: true, + completion: true, + schemas: [ + { + uri: 'http://myserver/foo-schema.json', + fileMatch: ['*'], + schema: { + id: 'http://myserver/foo-schema.json', + type: 'object', + properties: { + p1: { enum: ['v1', 'v2'] }, + p2: { $ref: 'http://myserver/bar-schema.json' }, + }, + }, + }, + { + uri: 'http://myserver/bar-schema.json', + schema: { + id: 'http://myserver/bar-schema.json', + type: 'object', + properties: { q1: { enum: ['x1', 'x2'] } }, + }, + }, + ], + }; + + jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions'); + jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions'); + }); + + it.each` + language | defaultsObj + ${'json'} | ${languages.json.jsonDefaults} + ${'yaml'} | ${languages.yaml.yamlDefaults} + `( + 'registers the given schemas with monaco for lang: $language', + ({ language, defaultsObj }) => { + registerSchemas({ language, options }); + + expect(defaultsObj.setDiagnosticsOptions).toHaveBeenCalledWith(options); + }, + ); + }); + describe('trimTrailingWhitespace', () => { it.each` input | output diff --git a/yarn.lock b/yarn.lock index a4de32ca84a2157f3ce6248664d2b4856ae71970..62770e0fa64327b824903db6a611f21f8b88d412 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1171,11 +1171,6 @@ dependencies: "@toast-ui/editor" "^2.2.0" -"@types/anymatch@*": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.0.tgz#d1d55958d1fccc5527d4aba29fc9c4b942f563ff" - integrity sha512-7WcbyctkE8GTzogDb0ulRAEw7v8oIS54ft9mQTU7PfM0hp5e+8kpa+HeQ7IQrFbKtJXBKcZ4bh+Em9dTw5L6AQ== - "@types/babel__core@^7.1.0": version "7.1.2" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" @@ -1285,11 +1280,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== -"@types/tapable@*": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" - integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ== - "@types/tern@*": version "0.23.3" resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460" @@ -1297,13 +1287,6 @@ dependencies: "@types/estree" "*" -"@types/uglify-js@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" - integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ== - dependencies: - source-map "^0.6.1" - "@types/unist@*", "@types/unist@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" @@ -1326,17 +1309,6 @@ "@types/unist" "*" "@types/vfile-message" "*" -"@types/webpack@^4.4.19": - version "4.4.23" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.23.tgz#059d6f4598cfd65ddee0e2db38317ef989696712" - integrity sha512-WswyG+2mRg0ul/ytPpCSWo+kOlVVPW/fKCBEVwqmPVC/2ffWEwhsCEQgnFbWDf8EWId2qGcpL623EjLfNTRk9A== - dependencies: - "@types/anymatch" "*" - "@types/node" "*" - "@types/tapable" "*" - "@types/uglify-js" "*" - source-map "^0.6.0" - "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -8252,17 +8224,24 @@ moment-mini@^2.22.1: resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.22.1.tgz#bc32d73e43a4505070be6b53494b17623183420d" integrity sha512-OUCkHOz7ehtNMYuZjNciXUfwTuz8vmF1MTbAy59ebf+ZBYZO5/tZKuChVWCX+uDo+4idJBpGltNfV8st+HwsGw== -monaco-editor-webpack-plugin@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz#920cbeecca25f15d70d568a7e11b0ba4daf1ae83" - integrity sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA== +monaco-editor-webpack-plugin@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.0.tgz#5b547281b9f404057dc5d8c5722390df9ac90be6" + integrity sha512-tOiiToc94E1sb50BgZ8q8WK/bxus77SRrwCqIpAB5er3cpX78SULbEBY4YPOB8kDolOzKRt30WIHG/D6gz69Ww== dependencies: - "@types/webpack" "^4.4.19" + loader-utils "^1.2.3" -monaco-editor@0.18.1, monaco-editor@^0.18.1: - version "0.18.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.18.1.tgz#ced7c305a23109875feeaf395a504b91f6358cfc" - integrity sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw== +monaco-editor@0.20.0, monaco-editor@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea" + integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ== + +monaco-yaml@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-2.4.0.tgz#027307a231d809c416babf1cf89b4c1bb940e55d" + integrity sha512-ElUS6uBqEjA2/o2gLuNdnqWSAAQXh8ISr1kwlFErm3t5IXO74TNfS3gnjO6Kv9TXS7LImjGfgPAZei7o8zNTHw== + optionalDependencies: + prettier "^1.19.1" mousetrap@^1.4.6: version "1.4.6" @@ -9387,11 +9366,16 @@ prettier@1.16.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d" integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw== -prettier@1.18.2, prettier@^1.18.2: +prettier@1.18.2: version "1.18.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== +prettier@^1.18.2, prettier@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + pretty-format@^24.8.0: version "24.8.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"