├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── License.txt ├── README.md ├── example ├── package-lock.json ├── package.json ├── src │ ├── client.ts │ ├── index.html │ └── main.ts ├── tsconfig.json └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── docker.ts ├── dockerAssist.ts ├── dockerCommands.ts ├── dockerCompletion.ts ├── dockerDefinition.ts ├── dockerFolding.ts ├── dockerFormatter.ts ├── dockerHighlight.ts ├── dockerHover.ts ├── dockerLinks.ts ├── dockerMarkdown.ts ├── dockerPlainText.ts ├── dockerRegistryClient.ts ├── dockerRename.ts ├── dockerSemanticTokens.ts ├── dockerSignatures.ts ├── dockerSymbols.ts ├── languageService.ts ├── main.ts └── tsconfig.json ├── test ├── dockerAssist.registry.test.ts ├── dockerAssist.test.ts ├── dockerCommands.test.ts ├── dockerDefinition.test.ts ├── dockerFolding.test.ts ├── dockerFormatter.test.ts ├── dockerHighlight.test.ts ├── dockerHover.test.ts ├── dockerLinks.test.ts ├── dockerRename.test.ts ├── dockerSemanticTokens.test.ts ├── dockerSignatures.tests.ts ├── dockerSymbols.test.ts └── dockerValidate.test.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rcjsuen 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Builds 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | include: 10 | - node-version: 18.x 11 | - node-version: 20.x 12 | - node-version: 22.x 13 | status: "LTS" 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Build 21 | run: | 22 | npm install 23 | npm run build 24 | - name: Package 25 | run: npm pack 26 | - name: Test 27 | run: npm run nyc-ci 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | if: ${{ matrix.status == 'LTS' }} 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .nyc_output 4 | coverage 5 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | tsconfig.json 4 | example 5 | *.tgz 6 | .github -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Remy Suen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockerfile Language Service 2 | 3 | ![Node.js Builds](https://github.com/rcjsuen/dockerfile-language-service/workflows/Node.js%20Builds/badge.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/rcjsuen/dockerfile-language-service/badge.svg?branch=master)](https://coveralls.io/github/rcjsuen/dockerfile-language-service?branch=master) [![Build Dependencies](https://david-dm.org/rcjsuen/dockerfile-language-service.svg)](https://david-dm.org/rcjsuen/dockerfile-language-service) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | This is a language service for Dockerfiles written in TypeScript. 6 | If you are looking for an actual Dockerfile language server that can be used with editors that implement the [language server protocol](http://microsoft.github.com/language-server-protocol), please visit the [rcjsuen/dockerfile-language-server-nodejs repository](https://github.com/rcjsuen/dockerfile-language-server-nodejs). 7 | 8 | The purpose of this project is to provide an API for creating a feature-rich Dockerfile editor. 9 | While this language service implements requests from the language server protocol, they are exposed as regular JavaScript functions so you can use them in the browser if you wish. 10 | For a demonstration of this language service's capabilities with Microsoft's [Monaco Editor](https://microsoft.github.io/monaco-editor/), please click [here](https://rcjsuen.github.io/dockerfile-language-service/). 11 | 12 | To [install](#installation-instructions) this language service as a dependency into your project, you will need to have [Node.js](https://nodejs.org/en/download/) installed. 13 | 14 | **Supported features:** 15 | - code actions 16 | - code completion 17 | - definition 18 | - diagnostics 19 | - document highlight 20 | - document links 21 | - document symbols 22 | - folding 23 | - formatting 24 | - hovers 25 | - rename 26 | - semantic tokens 27 | - signature help 28 | 29 | ## Development Instructions 30 | 31 | If you wish to build and compile this language server, you must first install [Node.js](https://nodejs.org/en/download/) if you have not already done so. 32 | After you have installed Node.js and cloned the repository with Git, you may now proceed to build and compile the language server with the following commands: 33 | 34 | ``` 35 | npm install 36 | npm run build 37 | npm test 38 | ``` 39 | 40 | If you are planning to change the code, use `npm run watch` to get the TypeScript files transpiled on-the-fly as they are modified. 41 | 42 | ## Installation Instructions 43 | 44 | To add this language service into your project, you must add `dockerfile-language-service` as a dependency in your package.json file. 45 | 46 | ## Browser Example 47 | 48 | The `/example/` folder includes an example for using this language service in a browser as a static HTML page with JavaScript. 49 | To build the example, please run the following commands from the root of the project: 50 | 51 | ``` 52 | npm install 53 | cd example 54 | npm install 55 | npm run build 56 | ``` 57 | 58 | As the example naturally relies on the language service, it is necessary to invoke `npm install` on the root folder first before invoking `npm install` in the `/example/` folder. 59 | Once `npm run build` has completed, you can open the `/example/lib/index.html` in a browser to test things out! 60 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rcjsuen/dockerfile-language-service-example", 4 | "version": "0.0.1", 5 | "dependencies": { 6 | "dockerfile-language-service": "0.8.1", 7 | "monaco-editor": "0.31.1" 8 | }, 9 | "devDependencies": { 10 | "copy-webpack-plugin": "^9.0.1", 11 | "typescript": "^3.7.5", 12 | "umd-compat-loader": "^2.1.1", 13 | "webpack": "^5.51.1", 14 | "webpack-cli": "^4.8.0" 15 | }, 16 | "scripts": { 17 | "compile": "tsc", 18 | "watch": "tsc -w", 19 | "copy": "cp src/index.html lib/index.html", 20 | "build": "npm run compile && webpack && npm run copy" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/src/client.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import { DockerfileLanguageServiceFactory } from 'dockerfile-language-service'; 6 | import { IMarkdownString } from 'monaco-editor'; 7 | import { Range, FormattingOptions, TextEdit, DocumentLink, Hover, CompletionItem, SignatureInformation, ParameterInformation, InsertTextFormat, SemanticTokenTypes, SemanticTokenModifiers, MarkupContent } from 'vscode-languageserver-types'; 8 | 9 | declare var monaco: any 10 | const LANGUAGE_ID = 'dockerfile'; 11 | 12 | // content to initialize the editor with 13 | const content = 14 | `FROM node:alpine 15 | COPY lib /docker-langserver/lib 16 | COPY bin /docker-langserver/bin 17 | COPY package.json /docker-langserver/package.json 18 | WORKDIR /docker-langserver/ 19 | RUN npm install --production && \\ 20 | chmod +x /docker-langserver/bin/docker-langserver 21 | ENTRYPOINT [ "/docker-langserver/bin/docker-langserver" ]`; 22 | 23 | monaco.editor.defineTheme("custom", { 24 | base: "vs", 25 | inherit: true, 26 | colors: {}, 27 | rules: [ 28 | { token: "keyword", foreground: "450640" }, 29 | { token: "comment", foreground: "EA19D5" }, 30 | { token: "parameter", foreground: "1C7E5C" }, 31 | { token: "property", foreground: "4930CE" }, 32 | { token: "namespace", foreground: "CE4930" }, 33 | { token: "class", foreground: "CE3049", fontStyle: "underline" }, 34 | { token: "macro", foreground: "6ED5D5" }, 35 | { token: "string", foreground: "4E11F8" }, 36 | { token: "variable", foreground: "7F7F30" }, 37 | { token: "operator", foreground: "6ED5D5" }, 38 | { token: "modifier", foreground: "10107F" }, 39 | ] 40 | }); 41 | 42 | monaco.editor.defineTheme("custom-dark", { 43 | base: "vs-dark", 44 | inherit: true, 45 | colors: {}, 46 | rules: [ 47 | { token: "keyword", foreground: "C586C0" }, 48 | { token: "comment", foreground: "6A9955" }, 49 | { token: "parameter", foreground: "9CFEDC" }, 50 | { token: "property", foreground: "C9B04E" }, 51 | { token: "namespace", foreground: "4EC9B0" }, 52 | { token: "class", foreground: "4EB0C9", fontStyle: "underline" }, 53 | { token: "macro", foreground: "EE5555" }, 54 | { token: "string", foreground: "CE9178" }, 55 | { token: "variable", foreground: "FFFFB0" }, 56 | { token: "operator", foreground: "EE5555" }, 57 | { token: "modifier", foreground: "9090FF" }, 58 | ] 59 | }); 60 | 61 | // create the Monaco editor 62 | const editor = monaco.editor.create(document.getElementById("container")!, { 63 | language: LANGUAGE_ID, 64 | value: content, 65 | lightbulb: { 66 | enabled: true 67 | }, 68 | 'semanticHighlighting.enabled': true, 69 | formatOnType: true, 70 | theme: "custom-dark" 71 | }); 72 | const monacoModel = editor.getModel(); 73 | const MONACO_URI = monacoModel.uri; 74 | const MODEL_URI = MONACO_URI.toString(); 75 | const LSP_URI = { uri: MODEL_URI }; 76 | 77 | const service = DockerfileLanguageServiceFactory.createLanguageService(); 78 | service.setCapabilities({ completion: { completionItem: { snippetSupport: true }}}); 79 | 80 | function convertFormattingOptions(options: any): FormattingOptions { 81 | return { 82 | tabSize: options.tabSize, 83 | insertSpaces: options.insertSpaces 84 | } 85 | } 86 | 87 | function convertHover(hover: Hover) { 88 | return { 89 | contents: [ 90 | { 91 | value: hover.contents 92 | } 93 | ], 94 | range: hover.range === undefined ? undefined : convertProtocolRange(hover.range as Range) 95 | } 96 | } 97 | 98 | function convertMonacoRange(range: any): Range { 99 | return { 100 | start: { 101 | line: range.startLineNumber - 1, 102 | character: range.startColumn - 1 103 | }, 104 | end: { 105 | line: range.endLineNumber - 1, 106 | character: range.endColumn - 1 107 | } 108 | } 109 | } 110 | 111 | function convertPosition(line: number, character: number) { 112 | return { 113 | line: line - 1, 114 | character: character - 1 115 | } 116 | } 117 | 118 | function convertProtocolRange(range: Range) { 119 | return { 120 | startLineNumber: range.start.line + 1, 121 | startColumn: range.start.character + 1, 122 | endLineNumber: range.end.line + 1, 123 | endColumn: range.end.character + 1, 124 | } 125 | } 126 | 127 | function convertLink(link: DocumentLink) { 128 | return { 129 | range: convertProtocolRange(link.range), 130 | url: link.target, 131 | } 132 | } 133 | 134 | function convertTextEdit(edit: TextEdit) { 135 | return { 136 | range: convertProtocolRange(edit.range), 137 | text: edit.newText 138 | } 139 | } 140 | 141 | function convertTextEdits(edits: TextEdit[]) { 142 | return edits.map(convertTextEdit); 143 | } 144 | 145 | function convertParameter(parameter: ParameterInformation) { 146 | return { 147 | label: parameter.label, 148 | documentation: convertDocumentation(parameter.documentation) 149 | } 150 | } 151 | 152 | function convertDocumentation(documentation: string | MarkupContent | undefined): string | IMarkdownString | undefined { 153 | if (documentation === undefined) { 154 | return undefined; 155 | } else if (typeof documentation === "string") { 156 | return documentation; 157 | } 158 | const content = (documentation as MarkupContent); 159 | return { 160 | value: content.value 161 | }; 162 | } 163 | 164 | function convertSignature(signature: SignatureInformation) { 165 | return { 166 | documentation: convertDocumentation(signature.documentation), 167 | label: signature.label, 168 | parameters: signature.parameters ? signature.parameters.map(convertParameter) : [] 169 | } 170 | } 171 | 172 | function convertToWorkspaceEdit(monacoEdits: any) { 173 | const workspaceEdits = monacoEdits.map((edit: any) => { 174 | return { 175 | edit: edit, 176 | resource: MONACO_URI 177 | } 178 | }); 179 | return { 180 | edits: workspaceEdits 181 | }; 182 | } 183 | 184 | function convertCompletionItem(item: CompletionItem) { 185 | item = service.resolveCompletionItem(item); 186 | return { 187 | label: item.label, 188 | documentation: { 189 | value: item.documentation 190 | }, 191 | range: item.textEdit ? convertProtocolRange((item.textEdit as TextEdit).range) : undefined, 192 | kind: item.kind as number + 1, 193 | insertText: item.textEdit ? item.textEdit.newText : item.insertText, 194 | insertTextRules: item.insertTextFormat === InsertTextFormat.Snippet ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, 195 | } 196 | } 197 | 198 | function convertMonacoCodeActionContext(context: any) { 199 | return { 200 | diagnostics: context.markers.map((marker: any) => { 201 | const range = convertMonacoRange(marker); 202 | return { 203 | code: Number(marker.code), 204 | range: range 205 | } 206 | }) 207 | } 208 | } 209 | 210 | monacoModel.onDidChangeContent(() => { 211 | const diagnostics = service.validate(monacoModel.getValue()); 212 | const markers = diagnostics.map((diagnostic) => { 213 | const range = convertProtocolRange(diagnostic.range); 214 | return { 215 | code: diagnostic.code !== undefined ? diagnostic.code.toString() : undefined, 216 | severity: diagnostic.severity === 1 ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning, 217 | startLineNumber: range.startLineNumber, 218 | startColumn: range.startColumn, 219 | endLineNumber: range.endLineNumber, 220 | endColumn: range.endColumn, 221 | message: diagnostic.message, 222 | source: diagnostic.source, 223 | tags: diagnostic.tags, 224 | } 225 | }); 226 | monaco.editor.setModelMarkers(monacoModel, LANGUAGE_ID, markers); 227 | }); 228 | 229 | monaco.languages.registerDocumentSemanticTokensProvider(LANGUAGE_ID, { 230 | getLegend() { 231 | let tokenTypes = []; 232 | let tokenModifiers = []; 233 | tokenTypes.push(SemanticTokenTypes.keyword); 234 | tokenTypes.push(SemanticTokenTypes.comment); 235 | tokenTypes.push(SemanticTokenTypes.parameter); 236 | tokenTypes.push(SemanticTokenTypes.property); 237 | tokenTypes.push(SemanticTokenTypes.namespace); 238 | tokenTypes.push(SemanticTokenTypes.class); 239 | tokenTypes.push(SemanticTokenTypes.macro); 240 | tokenTypes.push(SemanticTokenTypes.string); 241 | tokenTypes.push(SemanticTokenTypes.variable); 242 | tokenTypes.push(SemanticTokenTypes.operator); 243 | tokenTypes.push(SemanticTokenTypes.modifier); 244 | 245 | tokenModifiers.push(SemanticTokenModifiers.declaration); 246 | tokenModifiers.push(SemanticTokenModifiers.definition); 247 | tokenModifiers.push(SemanticTokenModifiers.deprecated); 248 | return { 249 | tokenModifiers, 250 | tokenTypes 251 | }; 252 | }, 253 | 254 | provideDocumentSemanticTokens(model: any) { 255 | return service.computeSemanticTokens(model.getValue()); 256 | }, 257 | 258 | releaseDocumentSemanticTokens() { 259 | // nothing to do 260 | } 261 | }); 262 | 263 | monaco.languages.registerCodeActionProvider(LANGUAGE_ID, { 264 | provideCodeActions(_model: any, range: any, context: any) { 265 | const commands = service.computeCodeActions(LSP_URI, convertMonacoRange(range), convertMonacoCodeActionContext(context)); 266 | const codeActions = []; 267 | for (let command of commands) { 268 | let args = command.arguments ? command.arguments : [] 269 | let edits = service.computeCommandEdits(monacoModel.getValue(), command.command, args); 270 | codeActions.push( 271 | { 272 | title: command.title, 273 | edit: convertToWorkspaceEdit(convertTextEdits(edits)) 274 | } 275 | ); 276 | } 277 | return { 278 | actions: codeActions, 279 | dispose: () => {} 280 | } as any; 281 | } 282 | }); 283 | 284 | monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, { 285 | triggerCharacters: ['=', ' ', '$', '-'], 286 | 287 | provideCompletionItems(model: any, position: any) { 288 | const lspPosition = convertPosition(position.lineNumber, position.column); 289 | const items = service.computeCompletionItems(model.getValue(), lspPosition); 290 | if ((items as any).then) { 291 | return (items as any).then((result: any) => { 292 | return { 293 | incomplete: false, 294 | suggestions: result.map(convertCompletionItem) 295 | } 296 | }); 297 | } 298 | return { 299 | incomplete: false, 300 | suggestions: (items as any).map(convertCompletionItem) 301 | } 302 | }, 303 | }); 304 | 305 | monaco.languages.registerDefinitionProvider(LANGUAGE_ID, { 306 | provideDefinition(model: any, position: any) { 307 | const definition = service.computeDefinition(LSP_URI, model.getValue(), convertPosition(position.lineNumber, position.column)); 308 | if (definition) { 309 | return { 310 | range: convertProtocolRange(definition.range), 311 | uri: MONACO_URI 312 | } 313 | } 314 | return null; 315 | } 316 | }); 317 | 318 | monaco.languages.registerDocumentHighlightProvider(LANGUAGE_ID, { 319 | provideDocumentHighlights(model: any, position: any) { 320 | const highlightRanges = service.computeHighlightRanges(model.getValue(), convertPosition(position.lineNumber, position.column)); 321 | return highlightRanges.map((highlightRange) => { 322 | return { 323 | kind: highlightRange.kind ? highlightRange.kind - 1 : undefined, 324 | range: convertProtocolRange(highlightRange.range) 325 | } 326 | }); 327 | } 328 | }); 329 | 330 | monaco.languages.registerHoverProvider(LANGUAGE_ID, { 331 | provideHover(model: any, position: any) { 332 | const hover = service.computeHover(model.getValue(), convertPosition(position.lineNumber, position.column)); 333 | return hover === null ? null : convertHover(hover); 334 | } 335 | }); 336 | 337 | monaco.languages.registerDocumentSymbolProvider(LANGUAGE_ID, { 338 | provideDocumentSymbols(model: any) { 339 | const symbols = service.computeSymbols(LSP_URI, model.getValue()); 340 | return symbols.map((symbol) => { 341 | return { 342 | name: symbol.name, 343 | range: convertProtocolRange(symbol.location.range), 344 | kind: symbol.kind - 1 345 | } 346 | }); 347 | } 348 | }); 349 | 350 | monaco.languages.registerSignatureHelpProvider(LANGUAGE_ID, { 351 | signatureHelpTriggerCharacters: [' ', '-', '=', '[', ','], 352 | 353 | provideSignatureHelp(model: any, position: any) { 354 | const signatureHelp = service.computeSignatureHelp(model.getValue(), convertPosition(position.lineNumber, position.column)); 355 | return { 356 | // SignatureHelpResult API needs this 357 | // https://github.com/microsoft/monaco-editor/issues/2164 358 | dispose: () => { }, 359 | value: { 360 | activeParameter: signatureHelp.activeParameter !== undefined ? signatureHelp.activeParameter : undefined, 361 | activeSignature: signatureHelp.activeSignature !== undefined ? signatureHelp.activeSignature : undefined, 362 | signatures: signatureHelp.signatures.map(convertSignature) 363 | } 364 | } 365 | } 366 | }); 367 | 368 | monaco.languages.registerRenameProvider(LANGUAGE_ID, { 369 | provideRenameEdits(model: any, position: any, newName: string) { 370 | const edits = service.computeRename(LSP_URI, model.getValue(), convertPosition(position.lineNumber, position.column), newName); 371 | const monacoEdits = convertTextEdits(edits); 372 | return convertToWorkspaceEdit(monacoEdits); 373 | } 374 | }); 375 | 376 | monaco.languages.registerLinkProvider(LANGUAGE_ID, { 377 | provideLinks(model: any) { 378 | const links = service.computeLinks(model.getValue()); 379 | return { 380 | links: links.map((link) => { 381 | return convertLink(service.resolveLink(link)); 382 | }) 383 | }; 384 | } 385 | }); 386 | 387 | monaco.languages.registerDocumentFormattingEditProvider(LANGUAGE_ID, { 388 | provideDocumentFormattingEdits(model: any, options: any) { 389 | const edits = service.format(model.getValue(), convertFormattingOptions(options)); 390 | return convertTextEdits(edits); 391 | } 392 | }); 393 | 394 | monaco.languages.registerDocumentRangeFormattingEditProvider(LANGUAGE_ID, { 395 | provideDocumentRangeFormattingEdits(model: any, range: any, options: any) { 396 | const edits = service.formatRange(model.getValue(), convertMonacoRange(range), convertFormattingOptions(options)); 397 | return convertTextEdits(edits); 398 | } 399 | }); 400 | 401 | monaco.languages.registerOnTypeFormattingEditProvider(LANGUAGE_ID, { 402 | autoFormatTriggerCharacters: ['`', '\\'], 403 | 404 | provideOnTypeFormattingEdits(model: any, position: any, ch: any, options: any) { 405 | const edits = service.formatOnType(model.getValue(), convertPosition(position.lineNumber, position.column), ch, convertFormattingOptions(options)); 406 | return convertTextEdits(edits); 407 | } 408 | }); 409 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 34 | 35 | 36 |

37 | Theme change: 38 | 42 |

43 |
44 | 45 | 50 | 51 | -------------------------------------------------------------------------------- /example/src/main.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | window.onload = () => { 6 | const w = window; 7 | // load Monaco code 8 | w.require(['vs/editor/editor.main'], () => { 9 | // load client code 10 | require('./client'); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "inlineSources": false, 8 | "declaration": true, 9 | "stripInternal": true, 10 | "lib": ["es2016", "dom"], 11 | "outDir": "lib", 12 | "strictNullChecks": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "preserveSymlinks": true, 18 | "skipLibCheck": true 19 | }, 20 | "include": [ 21 | "src" 22 | ] 23 | } -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | const path = require('path'); 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | 8 | const buildRoot = path.resolve(__dirname, "lib"); 9 | const monacoEditorPath = './node_modules/monaco-editor/min/vs'; 10 | 11 | module.exports = { 12 | entry: path.resolve(buildRoot, "main.js"), 13 | output: { 14 | filename: 'bundle.js', 15 | path: buildRoot 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /node_modules[\\\\|\/](dockerfile-language-service|vscode-languageserver-types)/, 21 | use: { loader: 'umd-compat-loader' } 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | fallback: { 27 | fs: 'empty', 28 | https: false, 29 | child_process: 'empty', 30 | net: 'empty', 31 | crypto: 'empty' 32 | }, 33 | extensions: ['.js'], 34 | alias: { 35 | 'vs': path.resolve(buildRoot, monacoEditorPath) 36 | } 37 | }, 38 | devtool: 'source-map', 39 | target: 'web', 40 | plugins: [ 41 | new CopyWebpackPlugin({ 42 | patterns: [ 43 | { 44 | from: monacoEditorPath, 45 | to: 'vs' 46 | } 47 | ] 48 | }) 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dockerfile-language-service", 3 | "description": "A language service for Dockerfiles to enable the creation of feature-rich Dockerfile editors.", 4 | "keywords": [ 5 | "language", 6 | "editor", 7 | "docker", 8 | "dockerfile", 9 | "moby" 10 | ], 11 | "version": "0.15.0", 12 | "author": "Remy Suen", 13 | "license": "MIT", 14 | "bugs": "https://github.com/rcjsuen/dockerfile-language-service/", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/rcjsuen/dockerfile-language-service.git" 18 | }, 19 | "engines": { 20 | "node": "*" 21 | }, 22 | "dependencies": { 23 | "dockerfile-ast": "0.7.0", 24 | "dockerfile-utils": "0.16.2", 25 | "vscode-languageserver-textdocument": "1.0.8", 26 | "vscode-languageserver-types": "3.17.3" 27 | }, 28 | "main": "./lib/main.js", 29 | "types": "./lib/main.d.ts", 30 | "devDependencies": { 31 | "@types/mocha": "^9.0.0", 32 | "@types/node": "^6.0.52", 33 | "mocha": "^11.1.0", 34 | "nyc": "^17.0.0", 35 | "typescript": "^5.2.2" 36 | }, 37 | "scripts": { 38 | "build": "tsc -p .", 39 | "prepublish": "tsc -p ./src", 40 | "watch": "tsc --watch -p .", 41 | "test": "mocha out/test", 42 | "nyc": "nyc mocha out/test", 43 | "nyc-ci": "nyc --reporter=lcov mocha out/test" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/docker.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { Range, Position } from 'vscode-languageserver-types'; 8 | 9 | export const KEYWORDS = [ 10 | "ADD", 11 | "ARG", 12 | "CMD", 13 | "COPY", 14 | "ENTRYPOINT", 15 | "ENV", 16 | "EXPOSE", 17 | "FROM", 18 | "HEALTHCHECK", 19 | "LABEL", 20 | "MAINTAINER", 21 | "ONBUILD", 22 | "RUN", 23 | "SHELL", 24 | "STOPSIGNAL", 25 | "USER", 26 | "VOLUME", 27 | "WORKDIR" 28 | ]; 29 | 30 | export class Util { 31 | public static isWhitespace(char: string): boolean { 32 | return char === ' ' || char === '\t' || Util.isNewline(char); 33 | } 34 | 35 | public static isNewline(char: string): boolean { 36 | return char === '\r' || char === '\n'; 37 | } 38 | 39 | /** 40 | * Determines if the given position is contained within the given range. 41 | * 42 | * @param position the position to check 43 | * @param range the range to see if the position is inside of 44 | */ 45 | public static isInsideRange(position: Position, range: Range): boolean { 46 | if (range === null) { 47 | return false; 48 | } else if (range.start.line === range.end.line) { 49 | return range.start.line === position.line 50 | && range.start.character <= position.character 51 | && position.character <= range.end.character; 52 | } else if (range.start.line === position.line) { 53 | return range.start.character <= position.character; 54 | } else if (range.end.line === position.line) { 55 | return position.character <= range.end.character; 56 | } 57 | return range.start.line < position.line && position.line < range.end.line; 58 | } 59 | 60 | public static isEmpty(range: Range): boolean { 61 | return range.start.line === range.end.line && range.start.character === range.end.character; 62 | } 63 | 64 | public static rangeEquals(range: Range, range2: Range) { 65 | return Util.positionEquals(range.start, range2.start) && Util.positionEquals(range.end, range2.end); 66 | } 67 | 68 | public static positionEquals(position: Position, position2: Position) { 69 | return position.line == position2.line && position.character === position2.character; 70 | } 71 | 72 | public static positionBefore(origin: Position, other: Position) { 73 | if (origin.line < other.line) { 74 | return true; 75 | } else if (origin.line > other.line) { 76 | return false; 77 | } 78 | return origin.character < other.character; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/dockerCommands.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { TextDocument } from 'vscode-languageserver-textdocument'; 8 | import { Command, Diagnostic, Range, TextEdit } from 'vscode-languageserver-types'; 9 | import { ValidationCode } from 'dockerfile-utils'; 10 | import { CommandIds } from './main'; 11 | 12 | export class DockerCommands { 13 | 14 | public analyzeDiagnostics(diagnostics: Diagnostic[], textDocumentURI: string): Command[] { 15 | let commands: Command[] = []; 16 | for (let i = 0; i < diagnostics.length; i++) { 17 | // Diagnostic's code is (number | string), convert it if necessary 18 | if (typeof diagnostics[i].code === "string") { 19 | diagnostics[i].code = parseInt(diagnostics[i].code as string); 20 | } 21 | switch (diagnostics[i].code) { 22 | case ValidationCode.CASING_DIRECTIVE: 23 | commands.push({ 24 | title: "Convert directive to lowercase", 25 | command: CommandIds.LOWERCASE, 26 | arguments: [textDocumentURI, diagnostics[i].range] 27 | }); 28 | break; 29 | case ValidationCode.CASING_INSTRUCTION: 30 | commands.push({ 31 | title: "Convert instruction to uppercase", 32 | command: CommandIds.UPPERCASE, 33 | arguments: [textDocumentURI, diagnostics[i].range] 34 | }); 35 | break; 36 | case ValidationCode.ARGUMENT_EXTRA: 37 | commands.push({ 38 | title: "Remove extra argument", 39 | command: CommandIds.EXTRA_ARGUMENT, 40 | arguments: [textDocumentURI, diagnostics[i].range] 41 | }); 42 | break; 43 | case ValidationCode.INVALID_ESCAPE_DIRECTIVE: 44 | commands.push({ 45 | title: "Convert to backslash", 46 | command: CommandIds.DIRECTIVE_TO_BACKSLASH, 47 | arguments: [textDocumentURI, diagnostics[i].range] 48 | }); 49 | commands.push({ 50 | title: "Convert to backtick", 51 | command: CommandIds.DIRECTIVE_TO_BACKTICK, 52 | arguments: [textDocumentURI, diagnostics[i].range] 53 | }); 54 | break; 55 | case ValidationCode.INVALID_AS: 56 | commands.push({ 57 | title: "Convert to AS", 58 | command: CommandIds.CONVERT_TO_AS, 59 | arguments: [textDocumentURI, diagnostics[i].range] 60 | }); 61 | break; 62 | case ValidationCode.UNKNOWN_HEALTHCHECK_FLAG: 63 | commands.push({ 64 | title: "Convert to --interval", 65 | command: CommandIds.FLAG_TO_HEALTHCHECK_INTERVAL, 66 | arguments: [textDocumentURI, diagnostics[i].range] 67 | }); 68 | commands.push({ 69 | title: "Convert to --retries", 70 | command: CommandIds.FLAG_TO_HEALTHCHECK_RETRIES, 71 | arguments: [textDocumentURI, diagnostics[i].range] 72 | }); 73 | commands.push({ 74 | title: "Convert to --start-period", 75 | command: CommandIds.FLAG_TO_HEALTHCHECK_START_PERIOD, 76 | arguments: [textDocumentURI, diagnostics[i].range] 77 | }); 78 | commands.push({ 79 | title: "Convert to --timeout", 80 | command: CommandIds.FLAG_TO_HEALTHCHECK_TIMEOUT, 81 | arguments: [textDocumentURI, diagnostics[i].range] 82 | }); 83 | break; 84 | case ValidationCode.UNKNOWN_ADD_FLAG: 85 | commands.push({ 86 | title: "Convert to --chown", 87 | command: CommandIds.FLAG_TO_CHOWN, 88 | arguments: [textDocumentURI, diagnostics[i].range] 89 | }); 90 | break; 91 | case ValidationCode.UNKNOWN_COPY_FLAG: 92 | commands.push({ 93 | title: "Convert to --chown", 94 | command: CommandIds.FLAG_TO_CHOWN, 95 | arguments: [textDocumentURI, diagnostics[i].range] 96 | }); 97 | commands.push({ 98 | title: "Convert to --from", 99 | command: CommandIds.FLAG_TO_COPY_FROM, 100 | arguments: [textDocumentURI, diagnostics[i].range] 101 | }); 102 | break; 103 | case ValidationCode.EMPTY_CONTINUATION_LINE: 104 | if (diagnostics[i].range.start.line + 1 === diagnostics[i].range.end.line) { 105 | commands.push({ 106 | title: "Remove empty continuation line", 107 | command: CommandIds.REMOVE_EMPTY_CONTINUATION_LINE, 108 | arguments: [textDocumentURI, diagnostics[i].range] 109 | }); 110 | } else { 111 | commands.push({ 112 | title: "Remove empty continuation lines", 113 | command: CommandIds.REMOVE_EMPTY_CONTINUATION_LINE, 114 | arguments: [textDocumentURI, diagnostics[i].range] 115 | }); 116 | } 117 | break; 118 | } 119 | } 120 | return commands; 121 | } 122 | 123 | public computeCommandEdits(content: string, command: string, args: any[]): TextEdit[] { 124 | let document = TextDocument.create("", "", 0, content); 125 | let range: Range = args[1]; 126 | switch (command) { 127 | case CommandIds.LOWERCASE: 128 | const directive = document.getText().substring(document.offsetAt(range.start), document.offsetAt(range.end)); 129 | return [ 130 | TextEdit.replace(range, directive.toLowerCase()) 131 | ] 132 | ; 133 | case CommandIds.UPPERCASE: 134 | let instruction = document.getText().substring(document.offsetAt(range.start), document.offsetAt(range.end)); 135 | return [ 136 | TextEdit.replace(range, instruction.toUpperCase()) 137 | ] 138 | ; 139 | case CommandIds.EXTRA_ARGUMENT: 140 | return [ 141 | TextEdit.del(range) 142 | ]; 143 | case CommandIds.DIRECTIVE_TO_BACKSLASH: 144 | return [ 145 | TextEdit.replace(range, '\\') 146 | ]; 147 | case CommandIds.DIRECTIVE_TO_BACKTICK: 148 | return [ 149 | TextEdit.replace(range, '`') 150 | ]; 151 | case CommandIds.CONVERT_TO_AS: 152 | return [ 153 | TextEdit.replace(range, "AS") 154 | ]; 155 | case CommandIds.FLAG_TO_CHOWN: 156 | return [ 157 | TextEdit.replace(range, "--chown") 158 | ]; 159 | case CommandIds.FLAG_TO_HEALTHCHECK_INTERVAL: 160 | return [ 161 | TextEdit.replace(range, "--interval") 162 | ]; 163 | case CommandIds.FLAG_TO_HEALTHCHECK_RETRIES: 164 | return [ 165 | TextEdit.replace(range, "--retries") 166 | ]; 167 | case CommandIds.FLAG_TO_HEALTHCHECK_START_PERIOD: 168 | return [ 169 | TextEdit.replace(range, "--start-period") 170 | ]; 171 | case CommandIds.FLAG_TO_HEALTHCHECK_TIMEOUT: 172 | return [ 173 | TextEdit.replace(range, "--timeout") 174 | ]; 175 | case CommandIds.FLAG_TO_COPY_FROM: 176 | return [ 177 | TextEdit.replace(range, "--from") 178 | ]; 179 | case CommandIds.REMOVE_EMPTY_CONTINUATION_LINE: 180 | return [ 181 | TextEdit.del(range) 182 | ]; 183 | } 184 | return null; 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/dockerCompletion.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { CompletionItem, MarkupKind } from 'vscode-languageserver-types'; 8 | import { MarkdownDocumentation } from './dockerMarkdown'; 9 | import { PlainTextDocumentation } from './dockerPlainText'; 10 | 11 | export class DockerCompletion { 12 | 13 | private dockerMarkdown = new MarkdownDocumentation(); 14 | private dockerPlainText = new PlainTextDocumentation(); 15 | 16 | public resolveCompletionItem(item: CompletionItem, documentationFormat?: MarkupKind[]): CompletionItem { 17 | if (!item.documentation && item.data) { 18 | if (documentationFormat === undefined || documentationFormat === null) { 19 | item.documentation = this.dockerPlainText.getDocumentation(item.data); 20 | } else { 21 | for (let format of documentationFormat) { 22 | if (format === MarkupKind.PlainText) { 23 | item.documentation = { 24 | kind: MarkupKind.PlainText, 25 | value: this.dockerPlainText.getDocumentation(item.data) 26 | }; 27 | return item; 28 | } else if (format === MarkupKind.Markdown) { 29 | item.documentation = { 30 | kind: MarkupKind.Markdown, 31 | value: this.dockerMarkdown.getMarkdown(item.data).contents as string 32 | }; 33 | return item; 34 | } 35 | } 36 | // no known format detected, just use plain text then 37 | item.documentation = this.dockerPlainText.getDocumentation(item.data); 38 | } 39 | } 40 | return item; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/dockerDefinition.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { Position, Range, Location, TextDocumentIdentifier } from 'vscode-languageserver-types'; 8 | import { Util } from './docker'; 9 | import { 10 | DockerfileParser, Dockerfile, ImageTemplate, 11 | Arg, Env, Heredoc, Property, Copy, Run 12 | } from 'dockerfile-ast'; 13 | 14 | export class DockerDefinition { 15 | 16 | private computeBuildStageDefinition(dockerfile: Dockerfile, position: Position): Range | null { 17 | let source = undefined; 18 | for (let instruction of dockerfile.getCOPYs()) { 19 | let flag = instruction.getFromFlag(); 20 | if (flag) { 21 | let range = flag.getValueRange(); 22 | if (range && range.start.line === position.line && range.start.character <= position.character && position.character <= range.end.character) { 23 | source = flag.getValue(); 24 | break; 25 | } 26 | } 27 | } 28 | 29 | for (let instruction of dockerfile.getFROMs()) { 30 | let range = instruction.getBuildStageRange(); 31 | if (range) { 32 | if (range.start.line === position.line && range.start.character <= position.character && position.character <= range.end.character) { 33 | // cursor in FROM's build stage itself 34 | return range; 35 | } else if (source !== undefined && instruction.getBuildStage().toLowerCase() === source.toLowerCase()) { 36 | // FROM's build stage matches what's in COPY 37 | return range; 38 | } 39 | } 40 | 41 | range = instruction.getImageNameRange(); 42 | if (Util.isInsideRange(position, range)) { 43 | const stageName = instruction.getImageName(); 44 | for (const from of dockerfile.getFROMs()) { 45 | if (stageName === from.getBuildStage() && from.getRange().start.line < range.start.line) { 46 | return from.getBuildStageRange(); 47 | } 48 | } 49 | return null; 50 | } 51 | } 52 | return null; 53 | } 54 | 55 | private static computeVariableDefinition(image: ImageTemplate, position: Position): Property { 56 | let variableName = null; 57 | for (let arg of image.getARGs()) { 58 | let property = arg.getProperty(); 59 | // might be an ARG with no arguments 60 | if (property) { 61 | // is the caret inside the definition itself 62 | if (Util.isInsideRange(position, property.getNameRange())) { 63 | variableName = property.getName(); 64 | break; 65 | } 66 | } 67 | } 68 | 69 | if (variableName === null) { 70 | variableCheck: for (let env of image.getENVs()) { 71 | let properties = env.getProperties(); 72 | for (let property of properties) { 73 | // is the caret inside the definition itself 74 | if (Util.isInsideRange(position, property.getNameRange())) { 75 | variableName = property.getName(); 76 | break variableCheck; 77 | } 78 | } 79 | } 80 | } 81 | 82 | if (variableName === null) { 83 | variableCheck: for (let instruction of image.getInstructions()) { 84 | for (let variable of instruction.getVariables()) { 85 | if (Util.isInsideRange(position, variable.getNameRange())) { 86 | variableName = variable.getName(); 87 | break variableCheck; 88 | } 89 | } 90 | } 91 | } 92 | 93 | for (let instruction of image.getInstructions()) { 94 | if (instruction instanceof Arg) { 95 | let property = instruction.getProperty(); 96 | // might be an ARG with no arguments 97 | if (property && property.getName() === variableName) { 98 | return property; 99 | } 100 | } else if (instruction instanceof Env) { 101 | let properties = instruction.getProperties(); 102 | for (let property of properties) { 103 | if (property.getName() === variableName) { 104 | return property; 105 | } 106 | } 107 | } 108 | } 109 | return null; 110 | } 111 | 112 | public static findDefinition(dockerfile: Dockerfile, position: Position): Property { 113 | for (const from of dockerfile.getFROMs()) { 114 | for (const variable of from.getVariables()) { 115 | if (Util.isInsideRange(position, variable.getNameRange())) { 116 | for (const arg of dockerfile.getInitialARGs()) { 117 | const property = arg.getProperty(); 118 | if (property && property.getName() === variable.getName()) { 119 | return property; 120 | } 121 | } 122 | return null; 123 | } 124 | } 125 | } 126 | let image = dockerfile.getContainingImage(position); 127 | if (image === null) { 128 | return null; 129 | } 130 | return DockerDefinition.computeVariableDefinition(image, position); 131 | } 132 | 133 | private static checkHeredocs(heredocs: Heredoc[], position: Position): Range | null { 134 | for (const heredoc of heredocs) { 135 | const nameRange = heredoc.getNameRange(); 136 | if (Util.isInsideRange(position, nameRange)) { 137 | return Util.isEmpty(nameRange) ? null : nameRange; 138 | } 139 | const delimiterRange = heredoc.getDelimiterRange(); 140 | if (delimiterRange !== null && Util.isInsideRange(position, delimiterRange)) { 141 | return nameRange; 142 | } 143 | } 144 | return null; 145 | } 146 | 147 | private static computeHeredocDefinition(dockerfile: Dockerfile, position: Position): Range | null { 148 | for (const instruction of dockerfile.getInstructions()) { 149 | if (instruction instanceof Copy) { 150 | const range = DockerDefinition.checkHeredocs(instruction.getHeredocs(), position); 151 | if (range !== null) { 152 | return range; 153 | } 154 | } else if (instruction instanceof Run) { 155 | const range = DockerDefinition.checkHeredocs(instruction.getHeredocs(), position); 156 | if (range !== null) { 157 | return range; 158 | } 159 | } 160 | } 161 | return null; 162 | } 163 | 164 | private computeVariableDefinition(dockerfile: Dockerfile, position: Position): Range | null { 165 | const property = DockerDefinition.findDefinition(dockerfile, position); 166 | return property ? property.getNameRange() : null; 167 | } 168 | 169 | public computeDefinitionRange(content: string, position: Position): Range | null { 170 | let dockerfile = DockerfileParser.parse(content); 171 | let range = this.computeBuildStageDefinition(dockerfile, position); 172 | if (range === null) { 173 | range = this.computeVariableDefinition(dockerfile, position); 174 | if (range === null) { 175 | return DockerDefinition.computeHeredocDefinition(dockerfile, position); 176 | } 177 | } 178 | return range; 179 | } 180 | 181 | public computeDefinition(textDocument: TextDocumentIdentifier, content: string, position: Position): Location | null { 182 | const range = this.computeDefinitionRange(content, position); 183 | if (range !== null) { 184 | return Location.create(textDocument.uri, range); 185 | } 186 | return null; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/dockerFolding.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { TextDocument } from 'vscode-languageserver-textdocument'; 8 | import { Position, Range, FoldingRange, FoldingRangeKind, uinteger } from 'vscode-languageserver-types'; 9 | import { DockerfileParser } from 'dockerfile-ast'; 10 | 11 | export class DockerFolding { 12 | 13 | private createFoldingRange(foldingRangeLineFoldingOnly: boolean, startLine: number, endLine: number, startCharacter: number, endCharacter: number, kind?: FoldingRangeKind): FoldingRange { 14 | if (foldingRangeLineFoldingOnly) { 15 | return { 16 | startLine, 17 | endLine, 18 | kind 19 | } 20 | } 21 | return FoldingRange.create(startLine, endLine, startCharacter, endCharacter, kind); 22 | } 23 | 24 | private getLineLength(document: TextDocument, line: number): number { 25 | let text = document.getText(Range.create(line, 0, line, uinteger.MAX_VALUE)); 26 | let length = text.length; 27 | let char = text.charAt(length - 1); 28 | while (char === '\r' || char === '\n') { 29 | length--; 30 | char = text.charAt(length - 1); 31 | } 32 | return length; 33 | } 34 | 35 | public computeFoldingRanges(content: string, lineFoldingOnly: boolean, limit: number): FoldingRange[] { 36 | if (limit < 1) { 37 | return []; 38 | } 39 | 40 | const ranges = []; 41 | const dockerfile = DockerfileParser.parse(content); 42 | const document = TextDocument.create("", "", 0, content); 43 | for (const instruction of dockerfile.getInstructions()) { 44 | const range = instruction.getRange(); 45 | if (range.start.line !== range.end.line) { 46 | const startLineLength = this.getLineLength(document, range.start.line); 47 | const endLineLength = this.getLineLength(document, range.end.line); 48 | ranges.push(this.createFoldingRange(lineFoldingOnly, range.start.line, range.end.line, startLineLength, endLineLength)); 49 | if (ranges.length === limit) { 50 | // return if we've reached the client's desired limit 51 | return ranges; 52 | } 53 | } 54 | } 55 | const comments = dockerfile.getComments(); 56 | if (comments.length < 2) { 57 | // no folding if zero or one comment 58 | return ranges; 59 | } 60 | 61 | let found = false; 62 | let startRange = comments[0].getRange(); 63 | let end = Position.create(startRange.start.line + 1, startRange.start.character); 64 | for (let i = 1; i < comments.length; i++) { 65 | const range = comments[i].getRange(); 66 | if (range.start.line === end.line) { 67 | // lines match, increment the folding range 68 | end = Position.create(range.end.line + 1, range.end.character); 69 | found = true 70 | } else { 71 | if (found) { 72 | // fold the previously found lines 73 | ranges.push(this.createFoldingRange(lineFoldingOnly, startRange.start.line, end.line - 1, startRange.end.character, end.character, FoldingRangeKind.Comment)); 74 | if (ranges.length === limit) { 75 | // return if we've reached the client's desired limit 76 | return ranges; 77 | } 78 | } 79 | // reset 80 | startRange = range; 81 | end = Position.create(startRange.start.line + 1, startRange.start.character); 82 | found = false; 83 | } 84 | } 85 | 86 | // loop ended, consider fold any found lines if necessary 87 | if (found) { 88 | ranges.push(this.createFoldingRange(lineFoldingOnly, startRange.start.line, end.line - 1, startRange.end.character, end.character, FoldingRangeKind.Comment)); 89 | } 90 | 91 | return ranges; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/dockerFormatter.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { TextDocument } from 'vscode-languageserver-textdocument'; 8 | import { 9 | TextEdit, Position, Range, FormattingOptions, 10 | } from 'vscode-languageserver-types'; 11 | import { DockerfileParser } from 'dockerfile-ast'; 12 | 13 | export class DockerFormatter { 14 | 15 | private getIndentation(formattingOptions?: FormattingOptions): string { 16 | let indentation = "\t"; 17 | if (formattingOptions && formattingOptions.insertSpaces) { 18 | indentation = ""; 19 | for (let i = 0; i < formattingOptions.tabSize; i++) { 20 | indentation = indentation + " "; 21 | } 22 | } 23 | return indentation; 24 | } 25 | 26 | /** 27 | * Creates a TextEdit for formatting the given document. 28 | * 29 | * @param document the document being formatted 30 | * @param start the start offset of the document's content to be replaced 31 | * @param end the end offset of the document's content to be replaced 32 | * @param indent true if this block should be replaced with an indentation, false otherwise 33 | * @param indentation the string to use for an indentation 34 | */ 35 | private createFormattingEdit(document: TextDocument, start: number, end: number, indent: boolean, indentation: string): TextEdit { 36 | if (indent) { 37 | return TextEdit.replace({ 38 | start: document.positionAt(start), 39 | end: document.positionAt(end) 40 | }, indentation); 41 | } else { 42 | return TextEdit.del({ 43 | start: document.positionAt(start), 44 | end: document.positionAt(end) 45 | }); 46 | } 47 | } 48 | 49 | public formatOnType(content: string, position: Position, ch: string, options: FormattingOptions): TextEdit[] { 50 | const dockerfile = DockerfileParser.parse(content); 51 | // check that the inserted character is the escape character 52 | if (dockerfile.getEscapeCharacter() === ch) { 53 | for (let comment of dockerfile.getComments()) { 54 | // ignore if we're in a comment 55 | if (comment.getRange().start.line === position.line) { 56 | return []; 57 | } 58 | } 59 | 60 | const directive = dockerfile.getDirective(); 61 | // ignore if we're in the parser directive 62 | if (directive && position.line === 0) { 63 | return []; 64 | } 65 | 66 | const document = TextDocument.create("", "", 0, content); 67 | validityCheck: for (let i = document.offsetAt(position); i < content.length; i++) { 68 | switch (content.charAt(i)) { 69 | case ' ': 70 | case '\t': 71 | break; 72 | case '\r': 73 | case '\n': 74 | break validityCheck; 75 | default: 76 | // not escaping a newline, no need to format the next line 77 | return []; 78 | } 79 | } 80 | 81 | const lines = [position.line + 1]; 82 | const indentedLines: boolean[] = []; 83 | indentedLines[lines[0]] = true; 84 | return this.formatLines(document, document.getText(), lines, indentedLines, options); 85 | } 86 | return []; 87 | } 88 | 89 | public formatRange(content: string, range: Range, options?: FormattingOptions): TextEdit[] { 90 | const lines: number[] = []; 91 | for (let i = range.start.line; i <= range.end.line; i++) { 92 | lines.push(i); 93 | } 94 | return this.format(content, lines, options); 95 | } 96 | 97 | /** 98 | * Formats the specified lines of the given document based on the 99 | * provided formatting options. 100 | * 101 | * @param document the text document to format 102 | * @param lines the lines to format 103 | * @param options the formatting options to use to perform the format 104 | * @return the text edits to apply to format the lines of the document 105 | */ 106 | private format(content: string, lines: number[], options?: FormattingOptions): TextEdit[] { 107 | let document = TextDocument.create("", "", 0, content); 108 | let dockerfile = DockerfileParser.parse(content); 109 | const indentedLines: boolean[] = []; 110 | for (let i = 0; i < document.lineCount; i++) { 111 | indentedLines[i] = false; 112 | } 113 | for (let instruction of dockerfile.getInstructions()) { 114 | let range = instruction.getRange(); 115 | indentedLines[range.start.line] = false; 116 | for (let i = range.start.line + 1; i <= range.end.line; i++) { 117 | indentedLines[i] = true; 118 | } 119 | } 120 | return this.formatLines(document, content, lines, indentedLines, options); 121 | } 122 | 123 | private formatLines(document: TextDocument, content: string, lines: number[], indentedLines: boolean[], options?: FormattingOptions): TextEdit[] { 124 | const indentation = this.getIndentation(options); 125 | const edits: TextEdit[] = []; 126 | lineCheck: for (let line of lines) { 127 | let startOffset = document.offsetAt(Position.create(line, 0)); 128 | for (let i = startOffset; i < content.length; i++) { 129 | switch (content.charAt(i)) { 130 | case ' ': 131 | case '\t': 132 | break; 133 | case '\r': 134 | case '\n': 135 | if (i !== startOffset) { 136 | // only whitespace on this line, trim it 137 | let edit = TextEdit.del({ 138 | start: document.positionAt(startOffset), 139 | end: document.positionAt(i) 140 | }); 141 | edits.push(edit); 142 | } 143 | // process the next line 144 | continue lineCheck; 145 | default: 146 | // non-whitespace encountered 147 | if (i !== startOffset || indentedLines[line]) { 148 | let edit = this.createFormattingEdit(document, startOffset, i, indentedLines[line], indentation); 149 | edits.push(edit); 150 | } 151 | // process the next line 152 | continue lineCheck; 153 | } 154 | } 155 | if (startOffset < content.length) { 156 | // only whitespace on the last line, trim it 157 | let edit = TextEdit.del({ 158 | start: document.positionAt(startOffset), 159 | end: document.positionAt(content.length) 160 | }); 161 | edits.push(edit); 162 | } 163 | } 164 | return edits; 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/dockerHighlight.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { TextDocument } from 'vscode-languageserver-textdocument'; 8 | import { 9 | Position, DocumentHighlight, DocumentHighlightKind 10 | } from 'vscode-languageserver-types'; 11 | import { Copy, DockerfileParser, From, Run } from 'dockerfile-ast'; 12 | import { DockerDefinition } from './dockerDefinition'; 13 | import { Util } from './docker'; 14 | 15 | export class DockerHighlight { 16 | 17 | public computeHighlightRanges(content: string, position: Position): DocumentHighlight[] { 18 | let dockerfile = DockerfileParser.parse(content); 19 | let provider = new DockerDefinition(); 20 | const definitionRange = provider.computeDefinitionRange(content, position); 21 | let image = definitionRange === null ? dockerfile.getContainingImage(position) : dockerfile.getContainingImage(definitionRange.start); 22 | const highlights: DocumentHighlight[] = []; 23 | if (definitionRange === null) { 24 | for (let instruction of dockerfile.getCOPYs()) { 25 | let flag = instruction.getFromFlag(); 26 | if (flag) { 27 | let range = flag.getValueRange(); 28 | if (range && range.start.line === position.line && range.start.character <= position.character && position.character <= range.end.character) { 29 | let stage = flag.getValue(); 30 | 31 | for (let other of dockerfile.getCOPYs()) { 32 | let otherFlag = other.getFromFlag(); 33 | if (otherFlag && otherFlag.getValue().toLowerCase() === stage.toLowerCase()) { 34 | highlights.push(DocumentHighlight.create(otherFlag.getValueRange(), DocumentHighlightKind.Read)); 35 | } 36 | } 37 | return highlights; 38 | } 39 | } 40 | } 41 | 42 | for (const from of dockerfile.getFROMs()) { 43 | for (const variable of from.getVariables()) { 44 | if (Util.isInsideRange(position, variable.getNameRange())) { 45 | const name = variable.getName(); 46 | for (const loopFrom of dockerfile.getFROMs()) { 47 | for (const fromVariable of loopFrom.getVariables()) { 48 | if (fromVariable.getName() === name) { 49 | highlights.push(DocumentHighlight.create(fromVariable.getNameRange(), DocumentHighlightKind.Read)); 50 | } 51 | } 52 | } 53 | return highlights; 54 | } 55 | } 56 | } 57 | 58 | if (image !== null) { 59 | for (let instruction of image.getInstructions()) { 60 | for (let variable of instruction.getVariables()) { 61 | if (Util.isInsideRange(position, variable.getNameRange())) { 62 | let name = variable.getName(); 63 | 64 | for (let instruction of image.getInstructions()) { 65 | if (!(instruction instanceof From)) { 66 | for (let variable of instruction.getVariables()) { 67 | if (variable.getName() === name) { 68 | highlights.push(DocumentHighlight.create(variable.getNameRange(), DocumentHighlightKind.Read)); 69 | } 70 | } 71 | } 72 | } 73 | return highlights; 74 | } 75 | } 76 | } 77 | } 78 | } else { 79 | for (const instruction of dockerfile.getInstructions()) { 80 | if (instruction instanceof Copy || instruction instanceof Run) { 81 | for (const heredoc of instruction.getHeredocs()) { 82 | const nameRange = heredoc.getNameRange(); 83 | if (Util.positionEquals(definitionRange.start, nameRange.start) && Util.positionEquals(definitionRange.start, nameRange.start)) { 84 | highlights.push(DocumentHighlight.create(definitionRange, DocumentHighlightKind.Write)); 85 | const delimiterRange = heredoc.getDelimiterRange(); 86 | if (delimiterRange !== null) { 87 | highlights.push(DocumentHighlight.create(delimiterRange, DocumentHighlightKind.Read)); 88 | } 89 | return highlights; 90 | } 91 | } 92 | } 93 | } 94 | let document = TextDocument.create("", "", 0, content); 95 | let definition = document.getText().substring(document.offsetAt(definitionRange.start), document.offsetAt(definitionRange.end)); 96 | let isBuildStage = false; 97 | const fromInstructions = dockerfile.getFROMs() 98 | for (const from of fromInstructions) { 99 | let stage = from.getBuildStage(); 100 | if (stage && definition.toLowerCase() === stage.toLowerCase()) { 101 | highlights.push(DocumentHighlight.create(from.getBuildStageRange(), DocumentHighlightKind.Write)); 102 | isBuildStage = true; 103 | } 104 | } 105 | 106 | if (isBuildStage) { 107 | for (const from of fromInstructions) { 108 | if (from.getRange().start.line > definitionRange.start.line) { 109 | if (from.getImageName() === definition) { 110 | highlights.push(DocumentHighlight.create(from.getImageNameRange(), DocumentHighlightKind.Read)); 111 | } 112 | } 113 | } 114 | 115 | for (let instruction of dockerfile.getCOPYs()) { 116 | let flag = instruction.getFromFlag(); 117 | if (flag) { 118 | if (flag.getValue().toLowerCase() === definition.toLowerCase()) { 119 | highlights.push(DocumentHighlight.create(flag.getValueRange(), DocumentHighlightKind.Read)); 120 | } 121 | } 122 | } 123 | return highlights; 124 | } 125 | 126 | for (let arg of image.getARGs()) { 127 | let property = arg.getProperty(); 128 | // property may be null if it's an ARG with no arguments 129 | if (property && property.getName() === definition) { 130 | highlights.push(DocumentHighlight.create(property.getNameRange(), DocumentHighlightKind.Write)); 131 | } 132 | } 133 | 134 | for (let env of image.getENVs()) { 135 | for (let property of env.getProperties()) { 136 | if (property.getName() === definition) { 137 | highlights.push(DocumentHighlight.create(property.getNameRange(), DocumentHighlightKind.Write)); 138 | } 139 | } 140 | } 141 | 142 | for (let instruction of image.getInstructions()) { 143 | // only highlight variables in non-FROM instructions 144 | if (!(instruction instanceof From)) { 145 | for (const variable of instruction.getVariables()) { 146 | if (variable.getName() === definition) { 147 | highlights.push(DocumentHighlight.create(variable.getNameRange(), DocumentHighlightKind.Read)); 148 | } 149 | } 150 | } 151 | } 152 | 153 | for (const arg of dockerfile.getInitialARGs()) { 154 | const property = arg.getProperty(); 155 | if (property && Util.rangeEquals(property.getNameRange(), definitionRange)) { 156 | for (const from of dockerfile.getFROMs()) { 157 | for (const variable of from.getVariables()) { 158 | if (variable.getName() === definition) { 159 | highlights.push(DocumentHighlight.create(variable.getNameRange(), DocumentHighlightKind.Read)); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | return highlights; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/dockerHover.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { Hover, Position, MarkupKind } from 'vscode-languageserver-types'; 8 | import { DockerfileParser, Dockerfile, Arg, Env, Instruction, ModifiableInstruction, Onbuild, Directive } from 'dockerfile-ast'; 9 | import { Util } from './docker'; 10 | import { MarkdownDocumentation } from './dockerMarkdown'; 11 | import { PlainTextDocumentation } from './dockerPlainText'; 12 | 13 | export class DockerHover { 14 | 15 | private markdown: MarkdownDocumentation; 16 | private plainText: PlainTextDocumentation; 17 | 18 | constructor(markdown: MarkdownDocumentation, plainText: PlainTextDocumentation) { 19 | this.markdown = markdown; 20 | this.plainText = plainText; 21 | } 22 | 23 | public onHover(content: string, position: Position, markupKind: MarkupKind[]): Hover | null { 24 | let dockerfile = DockerfileParser.parse(content); 25 | let image = dockerfile.getContainingImage(position); 26 | if (!image) { 27 | // position is invalid, not inside the Dockerfile 28 | return null; 29 | } 30 | 31 | let key = this.computeHoverKey(dockerfile, position); 32 | if (key) { 33 | // if it's not a raw value, apply markup if necessary 34 | if (markupKind && markupKind.length > 0) { 35 | switch (markupKind[0]) { 36 | case MarkupKind.Markdown: 37 | let markdownDocumentation = this.markdown.getMarkdown(key); 38 | if (markdownDocumentation) { 39 | return { 40 | contents: { 41 | kind: MarkupKind.Markdown, 42 | value: markdownDocumentation.contents as string 43 | } 44 | }; 45 | } 46 | return null; 47 | case MarkupKind.PlainText: 48 | let plainTextDocumentation = this.plainText.getDocumentation(key); 49 | if (plainTextDocumentation) { 50 | return { 51 | contents: { 52 | kind: MarkupKind.PlainText, 53 | value: plainTextDocumentation 54 | } 55 | }; 56 | } 57 | } 58 | return null; 59 | } 60 | const hover = this.markdown.getMarkdown(key); 61 | return hover === undefined ? null : hover; 62 | } 63 | 64 | for (let instruction of image.getInstructions()) { 65 | if (instruction instanceof Arg) { 66 | // hovering over an argument defined by ARG 67 | let property = instruction.getProperty(); 68 | if (property && Util.isInsideRange(position, property.getNameRange()) && property.getValue() !== null) { 69 | return { contents: property.getValue() }; 70 | } 71 | } 72 | 73 | if (instruction instanceof Env) { 74 | // hovering over an argument defined by ENV 75 | for (let property of instruction.getProperties()) { 76 | if (Util.isInsideRange(position, property.getNameRange()) && property.getValue() !== null) { 77 | return { 78 | contents: property.getValue() 79 | }; 80 | } 81 | } 82 | } 83 | } 84 | 85 | for (let instruction of image.getInstructions()) { 86 | for (let variable of instruction.getVariables()) { 87 | // are we hovering over a variable 88 | if (Util.isInsideRange(position, variable.getNameRange())) { 89 | let resolved = dockerfile.resolveVariable(variable.getName(), variable.getNameRange().start.line); 90 | if (resolved || resolved === "") { 91 | return { contents: resolved }; 92 | } else if (resolved === null) { 93 | return null; 94 | } 95 | } 96 | } 97 | } 98 | 99 | return null; 100 | } 101 | 102 | /** 103 | * Analyzes the Dockerfile at the given position to determine if the user 104 | * is hovering over a keyword, a flag, or a directive. 105 | * 106 | * @param dockerfile the Dockerfile to check 107 | * @param position the place that the user is hovering over 108 | * @return the string key value for the keyword, flag, or directive that's 109 | * being hovered over, or null if the user isn't hovering over 110 | * such a word 111 | */ 112 | private computeHoverKey(dockerfile: Dockerfile, position: Position): string | null { 113 | for (const directive of dockerfile.getDirectives()) { 114 | const range = directive.getNameRange(); 115 | switch (directive.getDirective()) { 116 | case Directive.escape: 117 | if (Util.isInsideRange(position, range)) { 118 | return Directive.escape; 119 | } 120 | break; 121 | case Directive.syntax: 122 | if (Util.isInsideRange(position, range)) { 123 | return Directive.syntax; 124 | } 125 | break; 126 | } 127 | } 128 | 129 | const image = dockerfile.getContainingImage(position); 130 | for (let instruction of image.getInstructions()) { 131 | let instructionRange = instruction.getInstructionRange(); 132 | if (Util.isInsideRange(position, instructionRange)) { 133 | return instruction.getKeyword(); 134 | } 135 | 136 | if (instruction instanceof Onbuild) { 137 | // hovering over a trigger instruction of an ONBUILD 138 | let range = instruction.getTriggerRange(); 139 | if (Util.isInsideRange(position, range)) { 140 | return instruction.getTrigger(); 141 | } 142 | } 143 | 144 | let hover = this.getFlagsHover(position, instruction); 145 | if (hover !== null) { 146 | return hover; 147 | } 148 | } 149 | 150 | return null; 151 | } 152 | 153 | private getFlagsHover(position: Position, instruction: Instruction): string | null { 154 | switch (instruction.getKeyword()) { 155 | case "ADD": 156 | let addFlags = (instruction as ModifiableInstruction).getFlags(); 157 | for (let flag of addFlags) { 158 | if (Util.isInsideRange(position, flag.getNameRange())) { 159 | switch (flag.getName()) { 160 | case "chown": 161 | return "ADD_FlagChown"; 162 | } 163 | } 164 | } 165 | break; 166 | case "COPY": 167 | let copyFlags = (instruction as ModifiableInstruction).getFlags(); 168 | for (let flag of copyFlags) { 169 | if (Util.isInsideRange(position, flag.getNameRange())) { 170 | switch (flag.getName()) { 171 | case "chown": 172 | return "COPY_FlagChown"; 173 | case "from": 174 | return "COPY_FlagFrom"; 175 | } 176 | } 177 | } 178 | break; 179 | case "FROM": 180 | const fromFlags = (instruction as ModifiableInstruction).getFlags(); 181 | for (const flag of fromFlags) { 182 | if (Util.isInsideRange(position, flag.getNameRange())) { 183 | if (flag.getName() === "platform") { 184 | return "FROM_FlagPlatform"; 185 | } 186 | return null; 187 | } 188 | } 189 | break; 190 | case "HEALTHCHECK": 191 | let flags = (instruction as ModifiableInstruction).getFlags(); 192 | for (let flag of flags) { 193 | if (Util.isInsideRange(position, flag.getNameRange())) { 194 | switch (flag.getName()) { 195 | case "interval": 196 | return "HEALTHCHECK_FlagInterval"; 197 | case "retries": 198 | return "HEALTHCHECK_FlagRetries"; 199 | case "start-interval": 200 | return "HEALTHCHECK_FlagStartInterval"; 201 | case "start-period": 202 | return "HEALTHCHECK_FlagStartPeriod"; 203 | case "timeout": 204 | return "HEALTHCHECK_FlagTimeout"; 205 | } 206 | return null; 207 | } 208 | } 209 | break; 210 | case "ONBUILD": 211 | let trigger = (instruction as Onbuild).getTriggerInstruction(); 212 | if (trigger !== null) { 213 | return this.getFlagsHover(position, trigger); 214 | } 215 | break; 216 | } 217 | return null; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/dockerLinks.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { DocumentLink, Range } from 'vscode-languageserver-types'; 8 | import { DockerfileParser } from 'dockerfile-ast'; 9 | 10 | export class DockerLinks { 11 | 12 | public getLinks(content: string): DocumentLink[] { 13 | let dockerfile = DockerfileParser.parse(content); 14 | let links = []; 15 | const stages = dockerfile.getFROMs().reduce((accumulator, from) => { 16 | const stage = from.getBuildStage(); 17 | if (stage !== null) { 18 | accumulator.push(stage); 19 | } 20 | return accumulator; 21 | }, []); 22 | for (let from of dockerfile.getFROMs()) { 23 | let name = from.getImageName(); 24 | const registry = from.getRegistry(); 25 | if (registry === "ghcr.io") { 26 | const idx = name.lastIndexOf("/"); 27 | if (idx === -1) { 28 | continue; 29 | } 30 | links.push({ 31 | range: Range.create(from.getRegistryRange().start, from.getImageNameRange().end), 32 | target: `https://github.com/${name}/pkgs/container/${name.substring(idx+1)}` 33 | }) 34 | continue; 35 | } 36 | if (registry === "mcr.microsoft.com") { 37 | links.push({ 38 | range: Range.create(from.getRegistryRange().start, from.getImageNameRange().end), 39 | target: "https://mcr.microsoft.com/artifact/mar/" + name 40 | }) 41 | continue; 42 | } 43 | if (registry === "quay.io") { 44 | links.push({ 45 | range: Range.create(from.getRegistryRange().start, from.getImageNameRange().end), 46 | target: "https://quay.io/repository/" + name 47 | }) 48 | continue; 49 | } 50 | if (name !== null && stages.indexOf(name) === -1) { 51 | if (name.indexOf('/') === -1) { 52 | links.push({ 53 | range: from.getImageNameRange(), 54 | data: "_/" + name + '/' 55 | }); 56 | } else { 57 | links.push({ 58 | range: from.getImageNameRange(), 59 | data: "r/" + name + '/' 60 | }); 61 | } 62 | } 63 | } 64 | return links; 65 | } 66 | 67 | public resolveLink(link: DocumentLink): DocumentLink { 68 | if (link.data) { 69 | link.target = "https://hub.docker.com/" + link.data; 70 | } 71 | return link; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/dockerMarkdown.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { Hover } from 'vscode-languageserver-types'; 8 | 9 | export class MarkdownDocumentation { 10 | 11 | private dockerMessages = { 12 | "hoverAdd": "Copy files, folders, or remote URLs from `source` to the `dest` path in the image's filesystem.\n\n", 13 | "hoverArg": "Define a variable with an optional default value that users can override at build-time when using `docker build`.\n\nSince Docker 1.9\n\n", 14 | "hoverCmd": "Provide defaults for an executing container. If an executable is not specified, then `ENTRYPOINT` must be specified as well. There can only be one `CMD` instruction in a `Dockerfile`.\n\n", 15 | "hoverCopy": "Copy files or folders from `source` to the `dest` path in the image's filesystem.\n\n", 16 | "hoverEntrypoint": "Configures the container to be run as an executable.\n\n", 17 | "hoverEnv": "Set the environment variable `key` to the value `value`.\n\n", 18 | "hoverExpose": "Define the network `port`s that this container will listen on at runtime.\n\n", 19 | "hoverFrom": "Set the `baseImage` to use for subsequent instructions. `FROM` must be the first instruction in a `Dockerfile`.\n\n", 20 | "hoverHealthcheck": "Define how Docker should test the container to check that it is still working. Alternatively, disable the base image's `HEALTHCHECK` instruction. There can only be one `HEALTHCHECK` instruction in a `Dockerfile`.\n\nSince Docker 1.12\n\n", 21 | "hoverLabel": "Adds metadata to an image.\n\nSince Docker 1.6\n\n", 22 | "hoverMaintainer": "Set the _Author_ field of the generated images. This instruction has been deprecated in favor of `LABEL`.\n\n", 23 | "hoverOnbuild": "Add a _trigger_ instruction to the image that will be executed when the image is used as a base image for another build.\n\n", 24 | "hoverRun": "Execute any commands on top of the current image as a new layer and commit the results.\n\n", 25 | "hoverShell": "Override the default shell used for the _shell_ form of commands.\n\nSince Docker 1.12\n\n", 26 | "hoverStopsignal": "Set the system call signal to use to send to the container to exit. Signals can be valid unsigned numbers or a signal name in the `SIGNAME` format such as `SIGKILL`.\n\nSince Docker 1.9\n\n", 27 | "hoverUser": "Set the user name or UID to use when running the image in addition to any subsequent `CMD`, `ENTRYPOINT`, or `RUN` instructions that follow it in the `Dockerfile`.\n\n", 28 | "hoverVolume": "Create a mount point with the specified name and mark it as holding externally mounted volumes from the native host or from other containers.\n\n", 29 | "hoverWorkdir": "Set the working directory for any subsequent `ADD`, `COPY`, `CMD`, `ENTRYPOINT`, or `RUN` instructions that follow it in the `Dockerfile`.\n\n", 30 | 31 | "hoverEscape": "Sets the character to use to escape characters and newlines in this Dockerfile. If unspecified, the default escape character is `\\`.\n\n", 32 | "hoverSyntax": "Set the location of the Dockerfile builder to use for building the current Dockerfile.\n\n", 33 | 34 | "hoverOnlineDocumentationFooter": "\n\n[Online documentation](${0})", 35 | 36 | "hoverAddFlagChown": "The username, groupname, or UID/GID combination to own the added content.", 37 | "hoverCopyFlagChown": "The username, groupname, or UID/GID combination to own the copied content.", 38 | "hoverCopyFlagFrom": "The previous build stage to use as the source location instead of the build's context.\n\nSince Docker 17.05.0-ce.", 39 | "hoverFromFlagPlatform": "The platform of the image if referencing a multi-platform image.\n\nSince Docker CE 18.04.", 40 | "hoverHealthcheckFlagInterval": "The seconds to wait for the health check to run after the container has started, and then again the number of seconds to wait before running again after the previous check has completed.", 41 | "hoverHealthcheckFlagRetries": "The number of consecutive failures of this health check before the container is considered to be `unhealthy`.", 42 | "hoverHealthcheckFlagStartInterval": "The number of seconds to wait between health checks during the start period.", 43 | "hoverHealthcheckFlagStartPeriod": "The number of seconds to wait for the container to startup. Failures during this grace period will not count towards the maximum number of retries. However, should a health check succeed during this period then any subsequent failures will count towards the maximum number of retries.\n\nSince Docker 17.05.0-ce.", 44 | "hoverHealthcheckFlagTimeout": "The number of seconds to wait for the check to complete before considering it to have failed.", 45 | 46 | "proposalArgNameOnly": "Define a variable that users can set at build-time when using `docker build`.\n\n", 47 | "proposalArgDefaultValue": "Define a variable with the given default value that users can override at build-time when using `docker build`.\n\n", 48 | "proposalHealthcheckExec": "Define how Docker should test the container to check that it is still working. There can only be one `HEALTHCHECK` instruction in a `Dockerfile`.\n\nSince Docker 1.12\n\n", 49 | "proposalHealthcheckNone": "Disable the `HEALTHCHECK` instruction inherited from the base image if one exists. There can only be one `HEALTHCHECK` instruction in a `Dockerfile`.\n\nSince Docker 1.12" 50 | }; 51 | 52 | private markdowns: any; 53 | 54 | constructor() { 55 | this.markdowns = { 56 | ADD: { 57 | contents: this.dockerMessages["hoverAdd"] + 58 | "```\n" + 59 | "ADD hello.txt /absolute/path\n" + 60 | "ADD hello.txt relative/to/workdir\n" + 61 | "```" + 62 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#add") 63 | }, 64 | 65 | ADD_FlagChown: { 66 | contents: this.dockerMessages["hoverAddFlagChown"] 67 | }, 68 | 69 | ARG: { 70 | contents: this.dockerMessages["hoverArg"] + 71 | "```\n" + 72 | "ARG userName\n" + 73 | "ARG testOutputDir=test\n" + 74 | "```" + 75 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#arg") 76 | }, 77 | 78 | ARG_NameOnly: { 79 | contents: this.dockerMessages["proposalArgNameOnly"] + 80 | "```\n" + 81 | "ARG userName\n" + 82 | "```" + 83 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#arg") 84 | }, 85 | 86 | ARG_DefaultValue: { 87 | contents: this.dockerMessages["proposalArgDefaultValue"] + 88 | "```\n" + 89 | "ARG testOutputDir=test\n" + 90 | "```" + 91 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#arg") 92 | }, 93 | 94 | CMD: { 95 | contents: this.dockerMessages["hoverCmd"] + 96 | "```\n" + 97 | "CMD [ \"/bin/ls\", \"-l\" ]\n" + 98 | "```\n" + 99 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#cmd") 100 | }, 101 | 102 | COPY: { 103 | contents: this.dockerMessages["hoverCopy"] + 104 | "```\n" + 105 | "COPY hello.txt /absolute/path\n" + 106 | "COPY hello.txt relative/to/workdir\n" + 107 | "```" + 108 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#copy") 109 | }, 110 | 111 | COPY_FlagChown: { 112 | contents: this.dockerMessages["hoverCopyFlagChown"] 113 | }, 114 | 115 | COPY_FlagFrom: { 116 | contents: this.dockerMessages["hoverCopyFlagFrom"] 117 | }, 118 | 119 | ENTRYPOINT: { 120 | contents: this.dockerMessages["hoverEntrypoint"] + 121 | "```\n" + 122 | "ENTRYPOINT [ \"/opt/app/run.sh\", \"--port\", \"8080\" ]\n" + 123 | "```" + 124 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#entrypoint") 125 | }, 126 | 127 | ENV: { 128 | contents: this.dockerMessages["hoverEnv"] + 129 | "```\n" + 130 | "ENV buildTag=1.0\n" + 131 | "```\n" + 132 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#env") 133 | }, 134 | 135 | EXPOSE: { 136 | contents: this.dockerMessages["hoverExpose"] + 137 | "```\n" + 138 | "EXPOSE 8080\n" + 139 | "EXPOSE 80 443 22\n" + 140 | "EXPOSE 7000-8000\n" + 141 | "```" + 142 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#expose") 143 | }, 144 | 145 | FROM: { 146 | contents: this.dockerMessages["hoverFrom"] + 147 | "```\n" + 148 | "FROM baseImage\n" + 149 | "FROM baseImage:tag\n" + 150 | "FROM baseImage@digest\n" + 151 | "```" + 152 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#from") 153 | }, 154 | 155 | FROM_FlagPlatform: { 156 | contents: this.dockerMessages["hoverFromFlagPlatform"] 157 | }, 158 | 159 | HEALTHCHECK: { 160 | contents: this.dockerMessages["hoverHealthcheck"] + 161 | "```\n" + 162 | "HEALTHCHECK --interval=10m --timeout=5s \\\n" + 163 | " CMD curl -f http://localhost/ || exit 1\n" + 164 | "HEALTHCHECK NONE\n" + 165 | "```" + 166 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#healthcheck") 167 | }, 168 | 169 | HEALTHCHECK_CMD: { 170 | contents: this.dockerMessages["proposalHealthcheckExec"] + 171 | "```\n" + 172 | "HEALTHCHECK --interval=10m --timeout=5s \\\n" + 173 | " CMD curl -f http://localhost/ || exit 1\n" + 174 | "```" + 175 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#healthcheck") 176 | }, 177 | 178 | HEALTHCHECK_FlagInterval: { 179 | contents: this.dockerMessages["hoverHealthcheckFlagInterval"] 180 | }, 181 | 182 | HEALTHCHECK_FlagRetries: { 183 | contents: this.dockerMessages["hoverHealthcheckFlagRetries"] 184 | }, 185 | 186 | HEALTHCHECK_FlagStartInterval: { 187 | contents: this.dockerMessages["hoverHealthcheckFlagStartInterval"] 188 | }, 189 | 190 | HEALTHCHECK_FlagStartPeriod: { 191 | contents: this.dockerMessages["hoverHealthcheckFlagStartPeriod"] 192 | }, 193 | 194 | HEALTHCHECK_FlagTimeout: { 195 | contents: this.dockerMessages["hoverHealthcheckFlagTimeout"] 196 | }, 197 | 198 | HEALTHCHECK_NONE: { 199 | contents: this.dockerMessages["proposalHealthcheckNone"] + 200 | "```\n" + 201 | "HEALTHCHECK NONE\n" + 202 | "```" + 203 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#healthcheck") 204 | }, 205 | 206 | LABEL: { 207 | contents: this.dockerMessages["hoverLabel"] + 208 | "```\n" + 209 | "LABEL version=\"1.0\"\n" + 210 | "```\n" + 211 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#label") 212 | }, 213 | 214 | MAINTAINER: { 215 | contents: this.dockerMessages["hoverMaintainer"] + 216 | "```\n" + 217 | "MAINTAINER name\n" + 218 | "```\n" + 219 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#maintainer") 220 | }, 221 | 222 | ONBUILD: { 223 | contents: this.dockerMessages["hoverOnbuild"] + 224 | "```\n" + 225 | "ONBUILD ADD . /opt/app/src/extensions\n" + 226 | "ONBUILD RUN /usr/local/bin/build.sh /opt/app" + 227 | "```" + 228 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#cmd") 229 | }, 230 | 231 | RUN: { 232 | contents: this.dockerMessages["hoverRun"] + 233 | "```\n" + 234 | "RUN apt-get update && apt-get install -y curl\n" + 235 | "```\n" + 236 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#run") 237 | }, 238 | 239 | SHELL: { 240 | contents: this.dockerMessages["hoverShell"] + 241 | "```\n" + 242 | "SHELL [ \"powershell\", \"-command\" ]\n" + 243 | "```\n" + 244 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#shell") 245 | }, 246 | 247 | STOPSIGNAL: { 248 | contents: this.dockerMessages["hoverStopsignal"] + 249 | "```\n" + 250 | "STOPSIGNAL 9\n" + 251 | "```\n" + 252 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#stopsignal") 253 | }, 254 | 255 | USER: { 256 | contents: this.dockerMessages["hoverUser"] + 257 | "```\n" + 258 | "USER daemon\n" + 259 | "```\n" + 260 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#user") 261 | }, 262 | 263 | VOLUME: { 264 | contents: this.dockerMessages["hoverVolume"] + 265 | "```\n" + 266 | "VOLUME [ \"/var/db\" ]\n" + 267 | "```\n" + 268 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#volume") 269 | }, 270 | 271 | WORKDIR: { 272 | contents: this.dockerMessages["hoverWorkdir"] + 273 | "```\n" + 274 | "WORKDIR /path/to/workdir\n" + 275 | "WORKDIR relative/path\n" + 276 | "```" + 277 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#workdir") 278 | }, 279 | 280 | escape: { 281 | contents: this.dockerMessages["hoverEscape"] + 282 | "```\n" + 283 | "# escape=`\n" + 284 | "```" + 285 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#escape") 286 | }, 287 | 288 | syntax: { 289 | contents: this.dockerMessages["hoverSyntax"] + 290 | "```\n" + 291 | "# syntax=docker/dockerfile:1.0\n" + 292 | "# syntax=docker/dockerfile:1.0.0-experimental\n" + 293 | "```" + 294 | this.formatMessage(this.dockerMessages["hoverOnlineDocumentationFooter"], "https://docs.docker.com/engine/reference/builder/#syntax") 295 | } 296 | }; 297 | } 298 | 299 | private formatMessage(text: string, variable: string): string { 300 | return text.replace("${0}", variable); 301 | } 302 | 303 | /** 304 | * Retrieves the Markdown documentation for the given word. 305 | * 306 | * @param word the Dockerfile keyword or directive, must not be null 307 | */ 308 | public getMarkdown(word: string): Hover { 309 | return this.markdowns[word]; 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/dockerPlainText.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | export class PlainTextDocumentation { 8 | 9 | private dockerMessages = { 10 | "hoverAdd": "Copy files, folders, or remote URLs from `source` to the `dest` path in the image's filesystem.\n\n", 11 | "hoverArg": "Define a variable with an optional default value that users can override at build-time when using `docker build`.\n\nSince Docker 1.9\n\n", 12 | "hoverCmd": "Provide defaults for an executing container. If an executable is not specified, then ENTRYPOINT must be specified as well. There can only be one CMD instruction in a Dockerfile.\n\n", 13 | "hoverCopy": "Copy files or folders from `source` to the `dest` path in the image's filesystem.\n\n", 14 | "hoverEntrypoint": "Configures the container to be run as an executable.\n\n", 15 | "hoverEnv": "Set the environment variable `key` to the value `value`.\n\n", 16 | "hoverExpose": "Define the network `port`s that this container will listen on at runtime.\n\n", 17 | "hoverFrom": "Set the `baseImage` to use for subsequent instructions. FROM must be the first instruction in a Dockerfile.\n\n", 18 | "hoverHealthcheck": "Define how Docker should test the container to check that it is still working. Alternatively, disable the base image's HEALTHCHECK instruction. There can only be one HEALTHCHECK instruction in a Dockerfile.\n\nSince Docker 1.12\n\n", 19 | "hoverLabel": "Adds metadata to an image.\n\nSince Docker 1.6\n\n", 20 | "hoverMaintainer": "Set the Author field of the generated images. This instruction has been deprecated in favor of LABEL.\n\n", 21 | "hoverOnbuild": "Add a trigger instruction to the image that will be executed when the image is used as a base image for another build.\n\n", 22 | "hoverRun": "Execute any commands on top of the current image as a new layer and commit the results.\n\n", 23 | "hoverShell": "Override the default shell used for the shell form of commands.\n\nSince Docker 1.12\n\n", 24 | "hoverStopsignal": "Set the system call signal to use to send to the container to exit. Signals can be valid unsigned numbers or a signal name in the SIGNAME format such as SIGKILL.\n\nSince Docker 1.9\n\n", 25 | "hoverUser": "Set the user name or UID to use when running the image in addition to any subsequent CMD, ENTRYPOINT, or RUN instructions that follow it in the Dockerfile.\n\n", 26 | "hoverVolume": "Create a mount point with the specified name and mark it as holding externally mounted volumes from the native host or from other containers.\n\n", 27 | "hoverWorkdir": "Set the working directory for any subsequent ADD, COPY, CMD, ENTRYPOINT, or RUN` instructions that follow it in the `Dockerfile`.\n\n", 28 | 29 | "hoverAddFlagChown": "The username, groupname, or UID/GID combination to own the added content.", 30 | "hoverCopyFlagChown": "The username, groupname, or UID/GID combination to own the copied content.", 31 | "hoverCopyFlagFrom": "The previous build stage to use as the source location instead of the build's context.\n\nSince Docker 17.05.0-ce.", 32 | "hoverFromFlagPlatform": "The platform of the image if referencing a multi-platform image.\n\nSince Docker CE 18.04.", 33 | "hoverHealthcheckFlagInterval": "The seconds to wait for the health check to run after the container has started, and then again the number of seconds to wait before running again after the previous check has completed.", 34 | "hoverHealthcheckFlagRetries": "The number of consecutive failures of this health check before the container is considered to be `unhealthy`.", 35 | "hoverHealthcheckFlagStartInterval": "The number of seconds to wait between health checks during the start period.", 36 | "hoverHealthcheckFlagStartPeriod": "The number of seconds to wait for the container to startup. Failures during this grace period will not count towards the maximum number of retries. However, should a health check succeed during this period then any subsequent failures will count towards the maximum number of retries.\n\nSince Docker 17.05.0-ce.", 37 | "hoverHealthcheckFlagTimeout": "The number of seconds to wait for the check to complete before considering it to have failed.", 38 | 39 | "hoverEscape": "Sets the character to use to escape characters and newlines in this Dockerfile. If unspecified, the default escape character is `\\`.\n\n", 40 | "hoverSyntax": "Set the location of the Dockerfile builder to use for building the current Dockerfile.\n\n", 41 | 42 | "signatureEscape": "Sets this Dockerfile's escape character. If unspecified, the default escape character is `\\`.", 43 | "signatureEscape_Param": "The character to use to escape characters and newlines in this Dockerfile.", 44 | 45 | "signatureAdd_Signature0": "Copy new files, directories or remote URLs to the image's filesystem.", 46 | "signatureAdd_Signature0_Param1": "The resource to copy or unpack if it is a local tar archive in a recognized compression format.", 47 | "signatureAdd_Signature0_Param3": "The name of the destination file or folder.", 48 | "signatureArg_Signature0": "Define a variable that users can pass a value to at build-time with `docker build`.", 49 | "signatureArg_Signature0_Param": "The name of the variable.", 50 | "signatureArg_Signature1": "Define a variable with an optional default value that users can override at build-time with `docker build`.", 51 | "signatureArg_Signature1_Param1": "The default value of the variable.", 52 | "signatureCmd_Signature0": "Set the default executable and parameters for this executing container.", 53 | "signatureCmd_Signature0_Param0": "The default executable for this executing container.", 54 | "signatureCmd_Signature0_Param1": "A parameter to the default executable.", 55 | "signatureCmd_Signature1": "Set the default parameters for this executing container. An ENTRYPOINT instruction must also be specified.", 56 | "signatureCmd_Signature1_Param0": "A parameter to the entrypoint executable.", 57 | "signatureCopy_Signature0": "Copy new files and directories to the image's filesystem.", 58 | "signatureCopy_Signature0_Param0": "Optional flags to configure this instruction.", 59 | "signatureCopy_Signature0_Param1": "The resource to copy.", 60 | "signatureCopyFlagFrom": "Set the build stage to use as the source location of this copy instruction instead of the build's context.", 61 | "signatureCopyFlagFrom_Param": "The build stage or image name to use as the source. Also may be a numeric index.", 62 | "signatureEntrypoint_Signature0": "Configure this container for running as an executable.", 63 | "signatureEntrypoint_Signature0_Param1": "The container's main executable.", 64 | "signatureEntrypoint_Signature0_Param2": "A parameter to the entrypoint executable.", 65 | "signatureEnv_Signature0": "Set an environment variable to the specified value. The value will be in the environment of any descendent Dockerfiles", 66 | "signatureEnv_Signature0_Param0": "The name of the environment variable.", 67 | "signatureEnv_Signature0_Param1": "The value to set the environment variable to.", 68 | "signatureExpose": "Define network ports for this container to listen on at runtime.", 69 | "signatureExpose_Param0": "The port that this container should listen on.", 70 | "signatureFrom_Signature0": "Set the base image to use for any subsequent instructions that follow.", 71 | "signatureFrom_Signature0_Param": "The name of the base image to use.", 72 | "signatureFrom_Signature1_Param1": "The tag of the base image to use.", 73 | "signatureFrom_Signature2_Param1": "The digest of the base image to use.", 74 | "signatureFrom_Signature3": "Set the base image to use for any subsequent instructions that follow and also give this build stage a name.", 75 | "signatureFrom_Signature3_Param2": "The name of this build stage.", 76 | "signatureFrom_Param2": "The name of this build stage.", 77 | "signatureHealthcheck_Signature0": "Define how Docker should test the container to check that it is still working.", 78 | "signatureHealthcheck_Signature1_Param2": "The parameters to the CMD instruction for the healthcheck.", 79 | "signatureHealthcheck_Signature2": "Disable the inherited HEALTHCHECK instruction from the base image.", 80 | "signatureLabel_Signature0": "Set metadata to an image.", 81 | "signatureLabel_Signature0_Param0": "The name of the metadata.", 82 | "signatureLabel_Signature0_Param1": "The value of the metadata.", 83 | "signatureMaintainer": "Set the \"Author\" field of this image.", 84 | "signatureMaintainer_Param": "The name of this image's maintainer.", 85 | "signatureOnbuild": "Register a build instruction as a trigger to be executed when this image is used as a base image for another build.", 86 | "signatureOnbuild_Param": "The build instruction to register as a trigger instruction.", 87 | "signatureRun_Signature0": "Execute commands inside a shell.", 88 | "signatureRun_Signature0_Param0": "The command to run.", 89 | "signatureRun_Signature0_Param1": "A parameter to the command.", 90 | "signatureRun_Signature1": "Execute commands without invoking a command shell.", 91 | "signatureShell": "Override default shell used for the shell form of commands.", 92 | "signatureShell_Param1": "The shell executable to use.", 93 | "signatureShell_Param2": "The parameters to the shell executable.", 94 | "signatureStopsignal": "Set the system call signal to use to send to the container to exit.", 95 | "signatureStopsignal_Param": "The signal to send to the container to exit. This may be an valid unsigned number or a signal name in the SIGNAME format such as SIGKILL.", 96 | "signatureUser_Signature0": "Set the user name to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", 97 | "signatureUser_Signature0_Param": "The user name to use.", 98 | "signatureUser_Signature1": "Set the user name and user group to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", 99 | "signatureUser_Signature1_Param1": "The group name to use.", 100 | "signatureUser_Signature2": "Set the UID to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", 101 | "signatureUser_Signature2_Param": "The UID to use.", 102 | "signatureUser_Signature3": "Set the UID and GID to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", 103 | "signatureUser_Signature3_Param1": "The GID to use.", 104 | "signatureVolume_Signature0": "Create mount points for holding externally mounted volumes from the native host or other containers.", 105 | "signatureVolume_Signature0_Param0": "The name of the mount point.", 106 | "signatureWorkdir": "Set the working directory for any ADD, COPY, CMD, ENTRYPOINT, or RUN instructions that follow.", 107 | "signatureWorkdir_Param": "The absolute or relative path to use as the working directory. Will be created if it does not exist.", 108 | 109 | "proposalArgNameOnly": "Define a variable that users can set at build-time when using `docker build`.\n\n", 110 | "proposalArgDefaultValue": "Define a variable with the given default value that users can override at build-time when using `docker build`.\n\n", 111 | "proposalHealthcheckExec": "Define how Docker should test the container to check that it is still working. There can only be one HEALTHCHECK instruction in a Dockerfile.\n\nSince Docker 1.12\n\n", 112 | "proposalHealthcheckNone": "Disable the HEALTHCHECK instruction inherited from the base image if one exists. There can only be one HEALTHCHECK instruction in a Dockerfile.\n\nSince Docker 1.12" 113 | }; 114 | 115 | private markdowns: any; 116 | 117 | constructor() { 118 | this.markdowns = { 119 | ADD: this.dockerMessages["hoverAdd"] + 120 | "ADD hello.txt /absolute/path\n" + 121 | "ADD hello.txt relative/to/workdir" 122 | , 123 | 124 | ADD_FlagChown: this.dockerMessages["hoverAddFlagChown"], 125 | 126 | ARG: this.dockerMessages["hoverArg"] + 127 | "ARG userName\n" + 128 | "ARG testOutputDir=test" 129 | , 130 | 131 | ARG_NameOnly: this.dockerMessages["proposalArgNameOnly"] + 132 | "ARG userName" 133 | , 134 | 135 | ARG_DefaultValue: this.dockerMessages["proposalArgDefaultValue"] + 136 | "ARG testOutputDir=test" 137 | , 138 | 139 | CMD: this.dockerMessages["hoverCmd"] + 140 | "CMD [ \"/bin/ls\", \"-l\" ]" 141 | , 142 | 143 | COPY: this.dockerMessages["hoverCopy"] + 144 | "COPY hello.txt /absolute/path\n" + 145 | "COPY hello.txt relative/to/workdir" 146 | , 147 | 148 | COPY_FlagChown: this.dockerMessages["hoverCopyFlagChown"], 149 | 150 | COPY_FlagFrom: this.dockerMessages["hoverCopyFlagFrom"], 151 | 152 | ENTRYPOINT: this.dockerMessages["hoverEntrypoint"] + 153 | "ENTRYPOINT [ \"/opt/app/run.sh\", \"--port\", \"8080\" ]" 154 | , 155 | 156 | ENV: this.dockerMessages["hoverEnv"] + 157 | "ENV buildTag=1.0" 158 | , 159 | 160 | EXPOSE: this.dockerMessages["hoverExpose"] + 161 | "EXPOSE 8080\n" + 162 | "EXPOSE 80 443 22\n" + 163 | "EXPOSE 7000-8000" 164 | , 165 | 166 | FROM: this.dockerMessages["hoverFrom"] + 167 | "FROM baseImage\n" + 168 | "FROM baseImage:tag\n" + 169 | "FROM baseImage@digest" 170 | , 171 | 172 | FROM_FlagPlatform: this.dockerMessages["hoverFromFlagPlatform"], 173 | 174 | HEALTHCHECK: this.dockerMessages["hoverHealthcheck"] + 175 | "HEALTHCHECK --interval=10m --timeout=5s \\\n" + 176 | " CMD curl -f http://localhost/ || exit 1\n" + 177 | "HEALTHCHECK NONE" 178 | , 179 | 180 | HEALTHCHECK_CMD: this.dockerMessages["proposalHealthcheckExec"] + 181 | "HEALTHCHECK --interval=10m --timeout=5s \\\n" + 182 | " CMD curl -f http://localhost/ || exit 1" 183 | , 184 | 185 | HEALTHCHECK_FlagInterval: this.dockerMessages["hoverHealthcheckFlagInterval"], 186 | 187 | HEALTHCHECK_FlagRetries: this.dockerMessages["hoverHealthcheckFlagRetries"], 188 | 189 | HEALTHCHECK_FlagStartInterval: this.dockerMessages["hoverHealthcheckFlagStartInterval"], 190 | 191 | HEALTHCHECK_FlagStartPeriod: this.dockerMessages["hoverHealthcheckFlagStartPeriod"], 192 | 193 | HEALTHCHECK_FlagTimeout: this.dockerMessages["hoverHealthcheckFlagTimeout"], 194 | 195 | HEALTHCHECK_NONE: this.dockerMessages["proposalHealthcheckNone"], 196 | 197 | LABEL: this.dockerMessages["hoverLabel"] + 198 | "LABEL version=\"1.0\"" 199 | , 200 | 201 | MAINTAINER: this.dockerMessages["hoverMaintainer"] + 202 | "MAINTAINER name" 203 | , 204 | 205 | ONBUILD: this.dockerMessages["hoverOnbuild"] + 206 | "ONBUILD ADD . /opt/app/src/extensions\n" + 207 | "ONBUILD RUN /usr/local/bin/build.sh /opt/app" 208 | , 209 | 210 | RUN: this.dockerMessages["hoverRun"] + 211 | "RUN apt-get update && apt-get install -y curl" 212 | , 213 | 214 | SHELL: this.dockerMessages["hoverShell"] + 215 | "SHELL [ \"powershell\", \"-command\" ]" 216 | , 217 | 218 | STOPSIGNAL: this.dockerMessages["hoverStopsignal"] + 219 | "STOPSIGNAL 9" 220 | , 221 | 222 | USER: this.dockerMessages["hoverUser"] + 223 | "USER daemon" 224 | , 225 | 226 | VOLUME: this.dockerMessages["hoverVolume"] + 227 | "VOLUME [ \"/var/db\" ]" 228 | , 229 | 230 | WORKDIR: this.dockerMessages["hoverWorkdir"] + 231 | "WORKDIR /path/to/workdir\n" + 232 | "WORKDIR relative/path" 233 | , 234 | 235 | escape: this.dockerMessages["hoverEscape"] + 236 | "# escape=`", 237 | 238 | syntax: this.dockerMessages["hoverSyntax"] + 239 | "# syntax=docker/dockerfile:1.0\n" + 240 | "# syntax=docker/dockerfile:1.0.0-experimental" 241 | , 242 | 243 | signatureEscape: this.dockerMessages["signatureEscape"], 244 | signatureEscape_Param: this.dockerMessages["signatureEscape_Param"], 245 | 246 | signatureAdd_Signature0: this.dockerMessages["signatureAdd_Signature0"], 247 | signatureAdd_Signature0_Param0: this.dockerMessages["signatureCopy_Signature0_Param0"], 248 | signatureAdd_Signature0_Param1: this.dockerMessages["signatureAdd_Signature0_Param1"], 249 | signatureAdd_Signature0_Param2: this.dockerMessages["signatureAdd_Signature0_Param1"], 250 | signatureAdd_Signature0_Param3: this.dockerMessages["signatureAdd_Signature0_Param3"], 251 | signatureAdd_Signature1: this.dockerMessages["signatureAdd_Signature0"], 252 | signatureAdd_Signature1_Param0: this.dockerMessages["signatureCopy_Signature0_Param0"], 253 | signatureAdd_Signature1_Param2: this.dockerMessages["signatureAdd_Signature0_Param1"], 254 | signatureAdd_Signature1_Param3: this.dockerMessages["signatureAdd_Signature0_Param1"], 255 | signatureAdd_Signature1_Param4: this.dockerMessages["signatureAdd_Signature0_Param3"], 256 | signatureArg_Signature0: this.dockerMessages["signatureArg_Signature0"], 257 | signatureArg_Signature0_Param: this.dockerMessages["signatureArg_Signature0_Param"], 258 | signatureArg_Signature1: this.dockerMessages["signatureArg_Signature1"], 259 | signatureArg_Signature1_Param0: this.dockerMessages["signatureArg_Signature0_Param"], 260 | signatureArg_Signature1_Param1: this.dockerMessages["signatureArg_Signature1_Param1"], 261 | signatureCmd_Signature0: this.dockerMessages["signatureCmd_Signature0"], 262 | signatureCmd_Signature0_Param1: this.dockerMessages["signatureCmd_Signature0_Param0"], 263 | signatureCmd_Signature0_Param2: this.dockerMessages["signatureCmd_Signature0_Param1"], 264 | signatureCmd_Signature0_Param3: this.dockerMessages["signatureCmd_Signature0_Param1"], 265 | signatureCmd_Signature1: this.dockerMessages["signatureCmd_Signature1"], 266 | signatureCmd_Signature1_Param1: this.dockerMessages["signatureCmd_Signature1_Param0"], 267 | signatureCmd_Signature1_Param2: this.dockerMessages["signatureCmd_Signature1_Param0"], 268 | signatureCmd_Signature1_Param3: this.dockerMessages["signatureCmd_Signature1_Param0"], 269 | signatureCmd_Signature2: this.dockerMessages["signatureCmd_Signature0"], 270 | signatureCmd_Signature2_Param0: this.dockerMessages["signatureCmd_Signature0_Param0"], 271 | signatureCmd_Signature2_Param1: this.dockerMessages["signatureCmd_Signature0_Param1"], 272 | signatureCmd_Signature2_Param2: this.dockerMessages["signatureCmd_Signature0_Param1"], 273 | signatureCopy_Signature0: this.dockerMessages["signatureCopy_Signature0"], 274 | signatureCopy_Signature0_Param0: this.dockerMessages["signatureCopy_Signature0_Param0"], 275 | signatureCopy_Signature0_Param1: this.dockerMessages["signatureCopy_Signature0_Param1"], 276 | signatureCopy_Signature0_Param2: this.dockerMessages["signatureCopy_Signature0_Param1"], 277 | signatureCopy_Signature0_Param3: this.dockerMessages["signatureAdd_Signature0_Param3"], 278 | signatureCopy_Signature1: this.dockerMessages["signatureCopy_Signature0"], 279 | signatureCopy_Signature1_Param0: this.dockerMessages["signatureCopy_Signature0_Param0"], 280 | signatureCopy_Signature1_Param2: this.dockerMessages["signatureCopy_Signature0_Param1"], 281 | signatureCopy_Signature1_Param3: this.dockerMessages["signatureCopy_Signature0_Param1"], 282 | signatureCopy_Signature1_Param4: this.dockerMessages["signatureAdd_Signature0_Param3"], 283 | signatureCopyFlagFrom: this.dockerMessages["signatureCopyFlagFrom"], 284 | signatureCopyFlagFrom_Param: this.dockerMessages["signatureCopyFlagFrom_Param"], 285 | signatureEntrypoint_Signature0: this.dockerMessages["signatureEntrypoint_Signature0"], 286 | signatureEntrypoint_Signature0_Param1: this.dockerMessages["signatureEntrypoint_Signature0_Param1"], 287 | signatureEntrypoint_Signature0_Param2: this.dockerMessages["signatureEntrypoint_Signature0_Param2"], 288 | signatureEntrypoint_Signature0_Param3: this.dockerMessages["signatureEntrypoint_Signature0_Param2"], 289 | signatureEntrypoint_Signature1: this.dockerMessages["signatureEntrypoint_Signature0"], 290 | signatureEntrypoint_Signature1_Param0: this.dockerMessages["signatureEntrypoint_Signature0_Param1"], 291 | signatureEntrypoint_Signature1_Param1: this.dockerMessages["signatureEntrypoint_Signature0_Param2"], 292 | signatureEntrypoint_Signature1_Param2: this.dockerMessages["signatureEntrypoint_Signature0_Param2"], 293 | signatureEnv_Signature0: this.dockerMessages["signatureEnv_Signature0"], 294 | signatureEnv_Signature0_Param0: this.dockerMessages["signatureEnv_Signature0_Param0"], 295 | signatureEnv_Signature0_Param1: this.dockerMessages["signatureEnv_Signature0_Param1"], 296 | signatureEnv_Signature1: this.dockerMessages["signatureEnv_Signature0"], 297 | signatureEnv_Signature1_Param0: this.dockerMessages["signatureEnv_Signature0_Param0"], 298 | signatureEnv_Signature1_Param1: this.dockerMessages["signatureEnv_Signature0_Param1"], 299 | signatureEnv_Signature2: this.dockerMessages["signatureEnv_Signature0"], 300 | signatureEnv_Signature2_Param0: this.dockerMessages["signatureEnv_Signature0_Param0"], 301 | signatureEnv_Signature2_Param1: this.dockerMessages["signatureEnv_Signature0_Param1"], 302 | signatureEnv_Signature2_Param2: this.dockerMessages["signatureEnv_Signature0_Param0"], 303 | signatureEnv_Signature2_Param3: this.dockerMessages["signatureEnv_Signature0_Param1"], 304 | signatureExpose: this.dockerMessages["signatureExpose"], 305 | signatureExpose_Param0: this.dockerMessages["signatureExpose_Param0"], 306 | signatureExpose_Param1: this.dockerMessages["signatureExpose_Param0"], 307 | signatureFrom_Signature0: this.dockerMessages["signatureFrom_Signature0"], 308 | signatureFrom_Signature0_Param: this.dockerMessages["signatureFrom_Signature0_Param"], 309 | signatureFrom_Signature1: this.dockerMessages["signatureFrom_Signature0"], 310 | signatureFrom_Signature1_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], 311 | signatureFrom_Signature1_Param1: this.dockerMessages["signatureFrom_Signature1_Param1"], 312 | signatureFrom_Signature2: this.dockerMessages["signatureFrom_Signature0"], 313 | signatureFrom_Signature2_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], 314 | signatureFrom_Signature2_Param1: this.dockerMessages["signatureFrom_Signature2_Param1"], 315 | signatureFrom_Signature3: this.dockerMessages["signatureFrom_Signature3"], 316 | signatureFrom_Signature3_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], 317 | signatureFrom_Signature3_Param2: this.dockerMessages["signatureFrom_Signature3_Param2"], 318 | signatureFrom_Signature4: this.dockerMessages["signatureFrom_Signature3"], 319 | signatureFrom_Signature4_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], 320 | signatureFrom_Signature4_Param1: this.dockerMessages["signatureFrom_Signature1_Param1"], 321 | signatureFrom_Signature4_Param3: this.dockerMessages["signatureFrom_Signature3_Param2"], 322 | signatureFrom_Signature5: this.dockerMessages["signatureFrom_Signature3"], 323 | signatureFrom_Signature5_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], 324 | signatureFrom_Signature5_Param1: this.dockerMessages["signatureFrom_Signature2_Param1"], 325 | signatureFrom_Signature5_Param3: this.dockerMessages["signatureFrom_Signature3_Param2"], 326 | signatureHealthcheck: this.dockerMessages["signatureHealthcheck_Signature0"], 327 | signatureHealthcheck_Signature0: this.dockerMessages["signatureHealthcheck_Signature0"], 328 | signatureHealthcheck_Signature1: this.dockerMessages["signatureHealthcheck_Signature0"], 329 | signatureHealthcheck_Signature1_Param0: this.dockerMessages["signatureCopy_Signature0_Param0"], 330 | signatureHealthcheck_Signature1_Param2: this.dockerMessages["signatureHealthcheck_Signature1_Param2"], 331 | signatureHealthcheck_Signature2: this.dockerMessages["signatureHealthcheck_Signature0"], 332 | signatureHealthcheckFlagInterval_Param: this.dockerMessages["hoverHealthcheckFlagInterval"], 333 | signatureHealthcheckFlagRetries_Param: this.dockerMessages["hoverHealthcheckFlagRetries"], 334 | signatureHealthcheckFlagStartPeriod_Param: this.dockerMessages["hoverHealthcheckFlagStartPeriod"], 335 | signatureHealthcheckFlagTimeout_Param: this.dockerMessages["hoverHealthcheckFlagTimeout"], 336 | signatureLabel_Signature0: this.dockerMessages["signatureLabel_Signature0"], 337 | signatureLabel_Signature0_Param0: this.dockerMessages["signatureLabel_Signature0_Param0"], 338 | signatureLabel_Signature0_Param1: this.dockerMessages["signatureLabel_Signature0_Param1"], 339 | signatureLabel_Signature1: this.dockerMessages["signatureLabel_Signature0"], 340 | signatureLabel_Signature1_Param0: this.dockerMessages["signatureLabel_Signature0_Param0"], 341 | signatureLabel_Signature1_Param1: this.dockerMessages["signatureLabel_Signature0_Param1"], 342 | signatureLabel_Signature2: this.dockerMessages["signatureLabel_Signature0"], 343 | signatureLabel_Signature2_Param0: this.dockerMessages["signatureLabel_Signature0_Param0"], 344 | signatureLabel_Signature2_Param1: this.dockerMessages["signatureLabel_Signature0_Param1"], 345 | signatureLabel_Signature2_Param2: this.dockerMessages["signatureLabel_Signature0_Param0"], 346 | signatureLabel_Signature2_Param3: this.dockerMessages["signatureLabel_Signature0_Param1"], 347 | signatureMaintainer: this.dockerMessages["signatureMaintainer"], 348 | signatureMaintainer_Param: this.dockerMessages["signatureMaintainer_Param"], 349 | signatureOnbuild: this.dockerMessages["signatureOnbuild"], 350 | signatureOnbuild_Param: this.dockerMessages["signatureOnbuild_Param"], 351 | signatureRun_Signature0: this.dockerMessages["signatureRun_Signature0"], 352 | signatureRun_Signature0_Param0: this.dockerMessages["signatureRun_Signature0_Param0"], 353 | signatureRun_Signature0_Param1: this.dockerMessages["signatureRun_Signature0_Param1"], 354 | signatureRun_Signature0_Param2: this.dockerMessages["signatureRun_Signature0_Param1"], 355 | signatureRun_Signature1: this.dockerMessages["signatureRun_Signature1"], 356 | signatureRun_Signature1_Param1: this.dockerMessages["signatureRun_Signature0_Param0"], 357 | signatureRun_Signature1_Param2: this.dockerMessages["signatureRun_Signature0_Param1"], 358 | signatureRun_Signature1_Param3: this.dockerMessages["signatureRun_Signature0_Param1"], 359 | signatureShell: this.dockerMessages["signatureShell"], 360 | signatureShell_Param1: this.dockerMessages["signatureShell_Param1"], 361 | signatureShell_Param2: this.dockerMessages["signatureShell_Param2"], 362 | signatureShell_Param3: this.dockerMessages["signatureShell_Param2"], 363 | signatureStopsignal: this.dockerMessages["signatureStopsignal"], 364 | signatureStopsignal_Param: this.dockerMessages["signatureStopsignal_Param"], 365 | signatureUser_Signature0: this.dockerMessages["signatureUser_Signature0"], 366 | signatureUser_Signature0_Param: this.dockerMessages["signatureUser_Signature0_Param"], 367 | signatureUser_Signature1: this.dockerMessages["signatureUser_Signature1"], 368 | signatureUser_Signature1_Param0: this.dockerMessages["signatureUser_Signature0"], 369 | signatureUser_Signature1_Param1: this.dockerMessages["signatureUser_Signature1_Param1"], 370 | signatureUser_Signature2: this.dockerMessages["signatureUser_Signature2"], 371 | signatureUser_Signature2_Param: this.dockerMessages["signatureUser_Signature2_Param"], 372 | signatureUser_Signature3: this.dockerMessages["signatureUser_Signature3"], 373 | signatureUser_Signature3_Param0: this.dockerMessages["signatureUser_Signature2_Param"], 374 | signatureUser_Signature3_Param1: this.dockerMessages["signatureUser_Signature3_Param1"], 375 | signatureVolume_Signature0: this.dockerMessages["signatureVolume_Signature0"], 376 | signatureVolume_Signature0_Param0: this.dockerMessages["signatureVolume_Signature0_Param0"], 377 | signatureVolume_Signature0_Param1: this.dockerMessages["signatureVolume_Signature0_Param0"], 378 | signatureVolume_Signature1: this.dockerMessages["signatureVolume_Signature0"], 379 | signatureVolume_Signature1_Param1: this.dockerMessages["signatureVolume_Signature0_Param0"], 380 | signatureVolume_Signature1_Param2: this.dockerMessages["signatureVolume_Signature0_Param0"], 381 | signatureWorkdir: this.dockerMessages["signatureWorkdir"], 382 | signatureWorkdir_Param: this.dockerMessages["signatureWorkdir_Param"] 383 | }; 384 | } 385 | 386 | getDocumentation(data: string): string { 387 | return this.markdowns[data]; 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/dockerRegistryClient.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import https = require('https'); 8 | import { ILogger } from './main'; 9 | 10 | /** 11 | * The DockerRegistryClient provides a way to communicate with the 12 | * official Docker registry hosted on Docker Hub. 13 | */ 14 | export class DockerRegistryClient { 15 | 16 | private readonly logger: ILogger; 17 | 18 | constructor(logger?: ILogger) { 19 | this.logger = logger; 20 | } 21 | 22 | /** 23 | * Gets the list of tags of the specified image from the Docker registry on Docker Hub. 24 | * 25 | * @param image the name of the interested image 26 | * @param prefix an optional prefix for filtering the list of tags 27 | * @return a promise that resolves to the specified image's list 28 | * of tags, may be empty 29 | */ 30 | public getTags(image: string, prefix?: string): Promise { 31 | if (image.indexOf('/') === -1) { 32 | image = "library/" + image; 33 | } 34 | 35 | return this.requestToken(image).then((data: any) => { 36 | if (data === null) { 37 | return []; 38 | } 39 | 40 | return this.listTags(data.token, image).then((data: any) => { 41 | if (!prefix) { 42 | return data.tags; 43 | } 44 | 45 | const tags = []; 46 | for (const tag of data.tags) { 47 | if (tag.indexOf(prefix) === 0) { 48 | tags.push(tag); 49 | } 50 | } 51 | return tags; 52 | }); 53 | }); 54 | } 55 | 56 | /** 57 | * Requests for an authentication token from the Docker registry 58 | * for accessing the given image. 59 | * 60 | * @param image the name of the interested image 61 | * @return a promise that resolves to the authentication token if 62 | * successful, or null if an error has occurred 63 | */ 64 | private requestToken(image: string): Promise { 65 | return this.performHttpsGet({ 66 | hostname: "auth.docker.io", 67 | port: 443, 68 | path: "/token?service=registry.docker.io&scope=repository:" + image + ":pull", 69 | headers: { 70 | Accept: "application/json" 71 | } 72 | }).catch((error) => { 73 | this.log(error); 74 | return null; 75 | }); 76 | } 77 | 78 | /** 79 | * Queries the registry for the given image's list of tags. 80 | * 81 | * @param authToken the authentication token to use for the GET 82 | * @param image the name of the interested image 83 | * @return a promise that will resolve to the image's list of 84 | * tags, an empty array will be returned if an error 85 | * occurs during the GET request 86 | */ 87 | private listTags(authToken: string, image: string): Promise { 88 | return this.performHttpsGet({ 89 | hostname: "registry-1.docker.io", 90 | port: 443, 91 | path: "/v2/" + image + "/tags/list", 92 | headers: { 93 | Accept: "application/json", 94 | Authorization: "Bearer " + authToken 95 | } 96 | }).catch((error) => { 97 | this.log(error); 98 | return { tags: [] }; 99 | }); 100 | } 101 | 102 | private performHttpsGet(opts: https.RequestOptions): Promise { 103 | return new Promise((resolve, reject) => { 104 | const request = https.get(opts, (response) => { 105 | if (response.statusCode !== 200) { 106 | // not a 200 OK, reject the promise with the error 107 | const error: any = new Error(response.statusMessage); 108 | error.statusCode = response.statusCode; 109 | reject(error); 110 | } else { 111 | let buffer = ''; 112 | response.on('data', (data: string) => { 113 | buffer += data; 114 | }) 115 | response.on('end', () => { 116 | resolve(JSON.parse(buffer)); 117 | }); 118 | } 119 | }); 120 | request.end(); 121 | request.on('error', (error) => { 122 | reject(error); 123 | }); 124 | }); 125 | } 126 | 127 | private log(error: any) { 128 | if (this.logger) { 129 | this.logger.log(error.toString()); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/dockerRename.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { Range, Position, TextEdit, TextDocumentIdentifier } from 'vscode-languageserver-types'; 8 | import { DockerfileParser, Copy, Run } from 'dockerfile-ast'; 9 | import { DockerHighlight } from './dockerHighlight'; 10 | import { Util } from './docker'; 11 | 12 | export class DockerRename { 13 | 14 | public prepareRename(content: string, position: Position): Range | null { 15 | let dockerfile = DockerfileParser.parse(content); 16 | let image = dockerfile.getContainingImage(position); 17 | for (let instruction of dockerfile.getCOPYs()) { 18 | let flag = instruction.getFromFlag(); 19 | if (flag) { 20 | let range = flag.getValueRange(); 21 | if (Util.isInsideRange(position, range)) { 22 | return range; 23 | } 24 | } 25 | } 26 | 27 | const fromInstructions = dockerfile.getFROMs(); 28 | for (const from of fromInstructions) { 29 | if (Util.isInsideRange(position, from.getBuildStageRange())) { 30 | return from.getBuildStageRange(); 31 | } 32 | 33 | const range = from.getImageNameRange(); 34 | if (Util.isInsideRange(position, range)) { 35 | const imageName = from.getImageName(); 36 | for (const stageCheck of fromInstructions) { 37 | if (stageCheck.getBuildStage() === imageName && stageCheck.getBuildStageRange().start.line < range.start.line) { 38 | return range; 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (image === null) { 45 | return null; 46 | } 47 | 48 | for (let env of image.getENVs()) { 49 | for (let property of env.getProperties()) { 50 | if (Util.isInsideRange(position, property.getNameRange())) { 51 | return property.getNameRange(); 52 | } 53 | } 54 | } 55 | 56 | for (let arg of image.getARGs()) { 57 | let property = arg.getProperty(); 58 | if (property !== null && Util.isInsideRange(position, property.getNameRange())) { 59 | return property.getNameRange(); 60 | } 61 | } 62 | 63 | for (let instruction of image.getInstructions()) { 64 | for (let variable of instruction.getVariables()) { 65 | if (Util.isInsideRange(position, variable.getNameRange())) { 66 | return variable.getNameRange(); 67 | } 68 | } 69 | 70 | if (instruction instanceof Copy || instruction instanceof Run) { 71 | for (const heredoc of instruction.getHeredocs()) { 72 | let range = heredoc.getNameRange(); 73 | if (Util.isInsideRange(position, range)) { 74 | return range; 75 | } 76 | 77 | range = heredoc.getDelimiterRange(); 78 | if (Util.isInsideRange(position, range)) { 79 | return range; 80 | } 81 | } 82 | } 83 | } 84 | return null; 85 | } 86 | 87 | public rename(textDocument: TextDocumentIdentifier, content: string, position: Position, newName: string): TextEdit[] { 88 | const edits: TextEdit[] = []; 89 | const highlighter = new DockerHighlight(); 90 | const highlightRanges = highlighter.computeHighlightRanges(content, position); 91 | for (const highlightRange of highlightRanges) { 92 | edits.push(TextEdit.replace(highlightRange.range, newName)); 93 | } 94 | return edits; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/dockerSymbols.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { SymbolInformation, SymbolKind, Range, TextDocumentIdentifier } from 'vscode-languageserver-types'; 8 | import { DockerfileParser, Keyword } from 'dockerfile-ast'; 9 | 10 | export class DockerSymbols { 11 | 12 | private createSymbolInformation(name: string, textDocumentURI: string, range: Range, kind: SymbolKind, deprecated: boolean): SymbolInformation { 13 | if (deprecated) { 14 | return { 15 | name: name, 16 | location: { 17 | uri: textDocumentURI, 18 | range: range 19 | }, 20 | kind: kind, 21 | deprecated: true 22 | }; 23 | } 24 | return { 25 | name: name, 26 | location: { 27 | uri: textDocumentURI, 28 | range: range 29 | }, 30 | kind: kind 31 | }; 32 | } 33 | 34 | public parseSymbolInformation(textDocument: TextDocumentIdentifier, content: string): SymbolInformation[] { 35 | let dockerfile = DockerfileParser.parse(content); 36 | let symbols: SymbolInformation[] = []; 37 | for (const directive of dockerfile.getDirectives()) { 38 | symbols.push(this.createSymbolInformation(directive.getName(), textDocument.uri, directive.getNameRange(), SymbolKind.Property, false)); 39 | } 40 | for (let instruction of dockerfile.getInstructions()) { 41 | let keyword = instruction.getKeyword(); 42 | symbols.push(this.createSymbolInformation(instruction.getInstruction(), textDocument.uri, instruction.getInstructionRange(), SymbolKind.Function, keyword === Keyword.MAINTAINER)); 43 | } 44 | return symbols; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/languageService.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import { DockerfileLanguageService, ILogger, Capabilities, CompletionItemCapabilities, FormatterSettings } from "./main"; 6 | import { TextDocument } from 'vscode-languageserver-textdocument'; 7 | import { 8 | Position, CompletionItem, Range, CodeActionContext, Command, TextDocumentIdentifier, SemanticTokens, Location, DocumentHighlight, SymbolInformation, SignatureHelp, DocumentLink, TextEdit, Hover, FormattingOptions, Diagnostic, MarkupKind, FoldingRange, CompletionItemTag 9 | } from "vscode-languageserver-types"; 10 | import * as DockerfileUtils from 'dockerfile-utils'; 11 | import { DockerAssist } from "./dockerAssist"; 12 | import { DockerRegistryClient } from "./dockerRegistryClient"; 13 | import { DockerCommands } from "./dockerCommands"; 14 | import { DockerFolding } from "./dockerFolding"; 15 | import { DockerDefinition } from "./dockerDefinition"; 16 | import { DockerHighlight } from "./dockerHighlight"; 17 | import { DockerSymbols } from "./dockerSymbols"; 18 | import { DockerSignatures } from "./dockerSignatures"; 19 | import { DockerLinks } from "./dockerLinks"; 20 | import { PlainTextDocumentation } from "./dockerPlainText"; 21 | import { DockerRename } from "./dockerRename"; 22 | import { DockerHover } from "./dockerHover"; 23 | import { MarkdownDocumentation } from "./dockerMarkdown"; 24 | import { DockerFormatter } from "./dockerFormatter"; 25 | import { DockerCompletion } from "./dockerCompletion"; 26 | import { DockerSemanticTokens } from "./dockerSemanticTokens"; 27 | 28 | export class LanguageService implements DockerfileLanguageService { 29 | 30 | private markdownDocumentation = new MarkdownDocumentation(); 31 | private plainTextDocumentation = new PlainTextDocumentation(); 32 | private logger: ILogger; 33 | 34 | private hoverContentFormat: MarkupKind[]; 35 | private completionItemCapabilities: CompletionItemCapabilities; 36 | 37 | private foldingRangeLineFoldingOnly: boolean = false; 38 | private foldingRangeLimit: number = Number.MAX_VALUE; 39 | 40 | public setLogger(logger: ILogger): void { 41 | this.logger = logger; 42 | } 43 | 44 | public setCapabilities(capabilities: Capabilities): void { 45 | this.completionItemCapabilities = capabilities && capabilities.completion && capabilities.completion.completionItem; 46 | this.hoverContentFormat = capabilities && capabilities.hover && capabilities.hover.contentFormat; 47 | this.foldingRangeLineFoldingOnly = capabilities && capabilities.foldingRange && capabilities.foldingRange.lineFoldingOnly; 48 | this.foldingRangeLimit = capabilities && capabilities.foldingRange && capabilities.foldingRange.rangeLimit; 49 | } 50 | 51 | public computeCodeActions(textDocument: TextDocumentIdentifier, range: Range, context: CodeActionContext): Command[] { 52 | let dockerCommands = new DockerCommands(); 53 | return dockerCommands.analyzeDiagnostics(context.diagnostics, textDocument.uri); 54 | } 55 | 56 | public computeLinks(content: string): DocumentLink[] { 57 | let dockerLinks = new DockerLinks(); 58 | return dockerLinks.getLinks(content); 59 | } 60 | 61 | public resolveLink(link: DocumentLink): DocumentLink { 62 | let dockerLinks = new DockerLinks(); 63 | return dockerLinks.resolveLink(link); 64 | } 65 | 66 | public computeCommandEdits(content: string, command: string, args: any[]): TextEdit[] { 67 | let dockerCommands = new DockerCommands(); 68 | return dockerCommands.computeCommandEdits(content, command, args); 69 | } 70 | 71 | public computeCompletionItems(content: string, position: Position): CompletionItem[] | PromiseLike { 72 | const document = TextDocument.create("", "", 0, content); 73 | const dockerAssist = new DockerAssist(document, new DockerRegistryClient(this.logger), this.completionItemCapabilities); 74 | return dockerAssist.computeProposals(position); 75 | } 76 | 77 | public resolveCompletionItem(item: CompletionItem): CompletionItem { 78 | if (!item.documentation) { 79 | let dockerCompletion = new DockerCompletion(); 80 | return dockerCompletion.resolveCompletionItem(item, this.completionItemCapabilities && this.completionItemCapabilities.documentationFormat); 81 | } 82 | return item; 83 | } 84 | 85 | public computeDefinition(textDocument: TextDocumentIdentifier, content: string, position: Position): Location { 86 | let dockerDefinition = new DockerDefinition(); 87 | return dockerDefinition.computeDefinition(textDocument, content, position); 88 | } 89 | 90 | public computeFoldingRanges(content: string): FoldingRange[] { 91 | let dockerFolding = new DockerFolding(); 92 | return dockerFolding.computeFoldingRanges(content, this.foldingRangeLineFoldingOnly, this.foldingRangeLimit); 93 | } 94 | 95 | public computeHighlightRanges(content: string, position: Position): DocumentHighlight[] { 96 | let dockerHighlight = new DockerHighlight(); 97 | return dockerHighlight.computeHighlightRanges(content, position); 98 | } 99 | 100 | public computeHover(content: string, position: Position): Hover | null { 101 | let dockerHover = new DockerHover(this.markdownDocumentation, this.plainTextDocumentation); 102 | return dockerHover.onHover(content, position, this.hoverContentFormat); 103 | } 104 | 105 | public computeSymbols(textDocument: TextDocumentIdentifier, content: string): SymbolInformation[] { 106 | let dockerSymbols = new DockerSymbols(); 107 | return dockerSymbols.parseSymbolInformation(textDocument, content); 108 | } 109 | 110 | public computeSignatureHelp(content: string, position: Position): SignatureHelp { 111 | let dockerSignature = new DockerSignatures(); 112 | return dockerSignature.computeSignatures(content, position); 113 | } 114 | 115 | public computeRename(textDocument: TextDocumentIdentifier, content: string, position: Position, newName: string): TextEdit[] { 116 | let dockerRename = new DockerRename(); 117 | return dockerRename.rename(textDocument, content, position, newName); 118 | } 119 | 120 | public prepareRename(content: string, position: Position): Range | null { 121 | let dockerRename = new DockerRename(); 122 | return dockerRename.prepareRename(content, position); 123 | } 124 | 125 | public computeSemanticTokens(content: string): SemanticTokens { 126 | let dockerSemanticTokens = new DockerSemanticTokens(content); 127 | return dockerSemanticTokens.computeSemanticTokens(); 128 | } 129 | 130 | public validate(content: string, settings?: DockerfileUtils.ValidatorSettings): Diagnostic[] { 131 | return DockerfileUtils.validate(content, settings); 132 | } 133 | 134 | public format(content: string, settings: FormatterSettings): TextEdit[] { 135 | return DockerfileUtils.format(content, settings); 136 | } 137 | 138 | public formatRange(content: string, range: Range, settings: FormatterSettings): TextEdit[] { 139 | return DockerfileUtils.formatRange(content, range, settings); 140 | } 141 | 142 | public formatOnType(content: string, position: Position, ch: string, settings: FormatterSettings): TextEdit[] { 143 | return DockerfileUtils.formatOnType(content, position, ch, settings); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import { 6 | Position, CompletionItem, Range, CodeActionContext, Command, TextDocumentIdentifier, Location, DocumentHighlight, SymbolInformation, SignatureHelp, TextEdit, DocumentLink, Hover, FormattingOptions, Diagnostic, MarkupKind, FoldingRange, CompletionItemTag, SemanticTokens 7 | } from 'vscode-languageserver-types'; 8 | import { ValidatorSettings } from 'dockerfile-utils'; 9 | import { LanguageService } from './languageService'; 10 | 11 | /** 12 | * An interface for logging errors encountered in the language service. 13 | */ 14 | export interface ILogger { 15 | 16 | log(message: string): void; 17 | } 18 | 19 | export enum CommandIds { 20 | LOWERCASE = "docker.command.convertToLowercase", 21 | UPPERCASE = "docker.command.convertToUppercase", 22 | EXTRA_ARGUMENT = "docker.command.removeExtraArgument", 23 | DIRECTIVE_TO_BACKTICK = "docker.command.directiveToBacktick", 24 | DIRECTIVE_TO_BACKSLASH = "docker.command.directiveToBackslash", 25 | FLAG_TO_CHOWN = "docker.command.flagToChown", 26 | FLAG_TO_COPY_FROM = "docker.command.flagToCopyFrom", 27 | FLAG_TO_HEALTHCHECK_INTERVAL = "docker.command.flagToHealthcheckInterval", 28 | FLAG_TO_HEALTHCHECK_RETRIES = "docker.command.flagToHealthcheckRetries", 29 | FLAG_TO_HEALTHCHECK_START_PERIOD = "docker.command.flagToHealthcheckStartPeriod", 30 | FLAG_TO_HEALTHCHECK_TIMEOUT = "docker.command.flagToHealthcheckTimeout", 31 | REMOVE_EMPTY_CONTINUATION_LINE = "docker.command.removeEmptyContinuationLine", 32 | CONVERT_TO_AS = "docker.command.convertToAS" 33 | } 34 | 35 | export namespace DockerfileLanguageServiceFactory { 36 | export function createLanguageService(): DockerfileLanguageService { 37 | return new LanguageService(); 38 | } 39 | } 40 | 41 | export interface CompletionItemCapabilities { 42 | /** 43 | * Indicates whether completion items for deprecated 44 | * entries should be explicitly flagged in the item. 45 | */ 46 | deprecatedSupport?: boolean; 47 | /** 48 | * Describes the supported content types that can be used 49 | * for a CompletionItem's documentation field. 50 | */ 51 | documentationFormat?: MarkupKind[]; 52 | /** 53 | * Indicates whether the snippet syntax should be used in 54 | * returned completion items. 55 | */ 56 | snippetSupport?: boolean; 57 | /** 58 | * Indicates that the client editor supports tags in CompletionItems. 59 | */ 60 | tagSupport?: { 61 | /** 62 | * Describes the set of tags that the editor supports. 63 | */ 64 | valueSet: CompletionItemTag[]; 65 | } 66 | } 67 | 68 | export interface CompletionCapabilities { 69 | /** 70 | * Capabilities related to completion items. 71 | */ 72 | completionItem?: CompletionItemCapabilities; 73 | } 74 | 75 | export interface Capabilities { 76 | /** 77 | * Capabilities related to completion requests. 78 | */ 79 | completion?: CompletionCapabilities; 80 | /** 81 | * Capabilities related to folding range requests. 82 | */ 83 | foldingRange?: { 84 | /** 85 | * If set, the service may choose to return ranges that have 86 | * a bogus `startCharacter` and/or `endCharacter` and/or to 87 | * leave them as undefined. 88 | */ 89 | lineFoldingOnly?: boolean; 90 | /** 91 | * The maximum number of folding ranges to return. This is a 92 | * hint and the service may choose to ignore this limit. 93 | */ 94 | rangeLimit?: number; 95 | }; 96 | /** 97 | * Capabilities related to hover requests. 98 | */ 99 | hover?: { 100 | /** 101 | * Describes the content type that should be returned for hovers. 102 | */ 103 | contentFormat?: MarkupKind[]; 104 | } 105 | } 106 | export interface FormatterSettings extends FormattingOptions { 107 | 108 | /** 109 | * Flag to indicate that instructions that span multiple lines 110 | * should be ignored. 111 | */ 112 | ignoreMultilineInstructions?: boolean; 113 | } 114 | 115 | export interface DockerfileLanguageService { 116 | 117 | setCapabilities(capabilities: Capabilities): void; 118 | 119 | computeCodeActions(textDocument: TextDocumentIdentifier, range: Range, context: CodeActionContext): Command[]; 120 | 121 | computeCommandEdits(content: string, command: string, args: any[]): TextEdit[]; 122 | 123 | computeCompletionItems(content: string, position: Position): CompletionItem[] | PromiseLike; 124 | 125 | resolveCompletionItem(item: CompletionItem): CompletionItem; 126 | 127 | computeDefinition(textDocument: TextDocumentIdentifier, content: string, position: Position): Location; 128 | 129 | computeFoldingRanges(content: string): FoldingRange[]; 130 | 131 | computeHighlightRanges(content: string, position: Position): DocumentHighlight[]; 132 | 133 | computeHover(content: string, position: Position): Hover | null; 134 | 135 | computeSymbols(textDocument: TextDocumentIdentifier, content: string): SymbolInformation[]; 136 | 137 | computeSignatureHelp(content: string, position: Position): SignatureHelp; 138 | 139 | computeRename(textDocument: TextDocumentIdentifier, content: string, position: Position, newName: string): TextEdit[]; 140 | 141 | prepareRename(content: string, position: Position): Range | null; 142 | 143 | computeLinks(content: string): DocumentLink[]; 144 | 145 | resolveLink(link: DocumentLink): DocumentLink; 146 | 147 | /** 148 | * Experimental API subject to change. 149 | */ 150 | computeSemanticTokens(content: string): SemanticTokens; 151 | 152 | validate(content: string, settings?: ValidatorSettings): Diagnostic[]; 153 | 154 | format(content: string, settings: FormatterSettings): TextEdit[]; 155 | 156 | formatRange(content: string, range: Range, settings: FormatterSettings): TextEdit[]; 157 | 158 | formatOnType(content: string, position: Position, ch: string, settings: FormatterSettings): TextEdit[]; 159 | 160 | setLogger(logger: ILogger): void; 161 | } 162 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "umd", 5 | "moduleResolution": "node", 6 | "sourceMap": false, 7 | "outDir": "../lib", 8 | "declaration": true, 9 | "lib": [ 10 | "es2016" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /test/dockerAssist.registry.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as assert from "assert"; 6 | 7 | import { 8 | Position, CompletionItem, CompletionItemKind, InsertTextFormat, TextEdit 9 | } from 'vscode-languageserver-types'; 10 | import { DockerfileLanguageServiceFactory } from '../src/main'; 11 | import { DockerRegistryClient } from '../src/dockerRegistryClient'; 12 | 13 | let dockerRegistryClient = new DockerRegistryClient(null); 14 | 15 | function computePromise(content: string, line: number, character: number): PromiseLike { 16 | let service = DockerfileLanguageServiceFactory.createLanguageService(); 17 | let items = service.computeCompletionItems(content, Position.create(line, character)); 18 | return items as PromiseLike; 19 | } 20 | 21 | function assertImageTag(tag: string, item: CompletionItem, line: number, character: number, prefixLength: number) { 22 | assert.strictEqual(item.label, tag); 23 | assert.strictEqual(item.kind, CompletionItemKind.Property); 24 | assert.strictEqual(item.textEdit.newText, tag); 25 | assert.strictEqual((item.textEdit as TextEdit).range.start.line, line); 26 | assert.strictEqual((item.textEdit as TextEdit).range.start.character, character); 27 | assert.strictEqual((item.textEdit as TextEdit).range.end.line, line); 28 | assert.strictEqual((item.textEdit as TextEdit).range.end.character, character + prefixLength); 29 | assert.strictEqual(item.insertTextFormat, InsertTextFormat.PlainText); 30 | } 31 | 32 | function assertImageTags(tags: string[], items: CompletionItem[], cursorPosition: number, prefixLength: number) { 33 | assert.notStrictEqual(items.length, 0); 34 | assert.notStrictEqual(tags.length, 0); 35 | assert.strictEqual(items.length, tags.length); 36 | for (let i = 0; i < tags.length; i++) { 37 | assertImageTag(tags[i], items[i], 0, cursorPosition - prefixLength, prefixLength); 38 | } 39 | } 40 | 41 | describe("Docker Content Assist Registry Tests", () => { 42 | describe("FROM", () => { 43 | describe("image tags short name", () => { 44 | it("all", async function () { 45 | this.timeout(10000); 46 | const tags = await dockerRegistryClient.getTags("alpine"); 47 | const items = await computePromise("FROM alpine:", 0, 12); 48 | assertImageTags(tags, items, 12, 0); 49 | }); 50 | 51 | it("all ignore prefix", async function () { 52 | this.timeout(10000); 53 | const tags = await dockerRegistryClient.getTags("alpine"); 54 | const items = await computePromise("FROM alpine:lat", 0, 12); 55 | assertImageTags(tags, items, 12, 0); 56 | }); 57 | 58 | it("prefix", async function () { 59 | this.timeout(10000); 60 | const tags = await dockerRegistryClient.getTags("alpine", "lat"); 61 | const items = await computePromise("FROM alpine:lat", 0, 15); 62 | assertImageTags(tags, items, 15, 3); 63 | }); 64 | 65 | it("invalid", async function () { 66 | this.timeout(10000); 67 | const items = await computePromise("FROM alpine-abc:", 0, 16); 68 | assert.strictEqual(items.length, 0); 69 | }); 70 | }); 71 | 72 | describe("image tags full name", () => { 73 | it("all", async function () { 74 | this.timeout(10000); 75 | const tags = await dockerRegistryClient.getTags("library/alpine"); 76 | const items = await computePromise("FROM library/alpine:", 0, 20); 77 | assertImageTags(tags, items, 20, 0); 78 | }); 79 | 80 | it("all ignore prefix", async function () { 81 | this.timeout(10000); 82 | const tags = await dockerRegistryClient.getTags("library/alpine"); 83 | const items = await computePromise("FROM library/alpine:lat", 0, 20); 84 | assertImageTags(tags, items, 20, 0); 85 | }); 86 | 87 | it("prefix", async function () { 88 | this.timeout(10000); 89 | const tags = await dockerRegistryClient.getTags("library/alpine", "lat"); 90 | const items = await computePromise("FROM library/alpine:lat", 0, 23); 91 | assertImageTags(tags, items, 23, 3); 92 | }); 93 | 94 | it("invalid", async function () { 95 | this.timeout(10000); 96 | const items = await computePromise("FROM library/alpine-abc:", 0, 24); 97 | assert.strictEqual(items.length, 0); 98 | }); 99 | 100 | it("issue #39", async function () { 101 | this.timeout(10000); 102 | const tags = await dockerRegistryClient.getTags("python", "3.8-b"); 103 | const items = await computePromise("FROM python:3.8-b", 0, 17); 104 | assertImageTags(tags, items, 17, 5); 105 | }); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/dockerCommands.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as assert from "assert"; 6 | 7 | import { 8 | Position, Range, Diagnostic, DiagnosticSeverity, TextDocumentIdentifier, CodeActionContext 9 | } from 'vscode-languageserver-types'; 10 | import { ValidationCode } from 'dockerfile-utils'; 11 | import { DockerfileLanguageServiceFactory, CommandIds } from '../src/main'; 12 | 13 | let uri = "uri://host/Dockerfile.sample"; 14 | let service = DockerfileLanguageServiceFactory.createLanguageService(); 15 | 16 | function getCommands(diagnostics: Diagnostic[], uri: string) { 17 | return service.computeCodeActions(TextDocumentIdentifier.create(uri), null, CodeActionContext.create(diagnostics)); 18 | } 19 | 20 | function createExtraArgument(): Diagnostic { 21 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Warning, ValidationCode.ARGUMENT_EXTRA); 22 | } 23 | 24 | function createInvalidEscapeDirective(): Diagnostic { 25 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Warning, ValidationCode.INVALID_ESCAPE_DIRECTIVE); 26 | } 27 | 28 | function createUnknownAddFlag(): Diagnostic { 29 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Error, ValidationCode.UNKNOWN_ADD_FLAG); 30 | } 31 | 32 | function createUnknownCopyFlag(): Diagnostic { 33 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Error, ValidationCode.UNKNOWN_COPY_FLAG); 34 | } 35 | 36 | function createUnknownHealthcheckFlag(): Diagnostic { 37 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Error, ValidationCode.UNKNOWN_HEALTHCHECK_FLAG); 38 | } 39 | 40 | function createDirectiveUppercase(): Diagnostic { 41 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Warning, ValidationCode.CASING_DIRECTIVE); 42 | } 43 | 44 | function createLowercase(): Diagnostic { 45 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Warning, ValidationCode.CASING_INSTRUCTION); 46 | } 47 | 48 | function createAS(): Diagnostic { 49 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(0, 0)), "", DiagnosticSeverity.Error, ValidationCode.INVALID_AS); 50 | } 51 | 52 | function createEmptyContinuationLine(multiline: boolean): Diagnostic { 53 | return Diagnostic.create(Range.create(Position.create(0, 0), Position.create(multiline ? 2 : 1, 0)), "", DiagnosticSeverity.Error, ValidationCode.EMPTY_CONTINUATION_LINE); 54 | } 55 | 56 | function assertRange(actual: Range, expected: Range) { 57 | assert.strictEqual(actual.start.line, expected.start.line); 58 | assert.strictEqual(actual.start.character, expected.start.character); 59 | assert.strictEqual(actual.end.line, expected.end.line); 60 | assert.strictEqual(actual.end.character, expected.start.character); 61 | } 62 | 63 | describe("Dockerfile code actions", function () { 64 | it("no diagnostics", function () { 65 | let commands = getCommands([], uri); 66 | assert.strictEqual(commands.length, 0); 67 | }); 68 | 69 | it("extra argument", function () { 70 | let diagnostic = createExtraArgument(); 71 | let commands = getCommands([diagnostic], uri); 72 | assert.strictEqual(commands.length, 1); 73 | assert.strictEqual(commands[0].command, CommandIds.EXTRA_ARGUMENT); 74 | assert.strictEqual(commands[0].arguments.length, 2); 75 | assert.strictEqual(commands[0].arguments[0], uri); 76 | assertRange(commands[0].arguments[1], diagnostic.range); 77 | }); 78 | 79 | it("invalid escape directive", function () { 80 | let diagnostic = createInvalidEscapeDirective(); 81 | let commands = getCommands([diagnostic], uri); 82 | assert.strictEqual(commands.length, 2); 83 | assert.strictEqual(commands[0].command, CommandIds.DIRECTIVE_TO_BACKSLASH); 84 | assert.strictEqual(commands[0].arguments.length, 2); 85 | assert.strictEqual(commands[0].arguments[0], uri); 86 | assertRange(commands[0].arguments[1], diagnostic.range); 87 | assert.strictEqual(commands[1].command, CommandIds.DIRECTIVE_TO_BACKTICK); 88 | assert.strictEqual(commands[1].arguments.length, 2); 89 | assert.strictEqual(commands[1].arguments[0], uri); 90 | assertRange(commands[1].arguments[1], diagnostic.range); 91 | }); 92 | 93 | it("convert to uppercase", function () { 94 | let diagnostic = createLowercase(); 95 | let commands = getCommands([diagnostic], uri); 96 | assert.strictEqual(commands.length, 1); 97 | assert.strictEqual(commands[0].command, CommandIds.UPPERCASE); 98 | assert.strictEqual(commands[0].arguments.length, 2); 99 | assert.strictEqual(commands[0].arguments[0], uri); 100 | assertRange(commands[0].arguments[1], diagnostic.range); 101 | }); 102 | 103 | it("convert to lowercase", function () { 104 | let diagnostic = createDirectiveUppercase(); 105 | let commands = getCommands([diagnostic], uri); 106 | assert.strictEqual(commands.length, 1); 107 | assert.strictEqual(commands[0].command, CommandIds.LOWERCASE); 108 | assert.strictEqual(commands[0].arguments.length, 2); 109 | assert.strictEqual(commands[0].arguments[0], uri); 110 | assertRange(commands[0].arguments[1], diagnostic.range); 111 | }); 112 | 113 | it("convert to AS", function () { 114 | let diagnostic = createAS(); 115 | let commands = getCommands([diagnostic], uri); 116 | assert.strictEqual(commands.length, 1); 117 | assert.strictEqual(commands[0].command, CommandIds.CONVERT_TO_AS); 118 | assert.strictEqual(commands[0].arguments.length, 2); 119 | assert.strictEqual(commands[0].arguments[0], uri); 120 | assertRange(commands[0].arguments[1], diagnostic.range); 121 | }); 122 | 123 | it("multiple diagnostics", function () { 124 | let escape = createInvalidEscapeDirective(); 125 | let lowercase = createLowercase(); 126 | let commands = getCommands([escape, lowercase], uri); 127 | assert.strictEqual(commands.length, 3); 128 | assert.strictEqual(commands[0].command, CommandIds.DIRECTIVE_TO_BACKSLASH); 129 | assert.strictEqual(commands[0].arguments.length, 2); 130 | assert.strictEqual(commands[0].arguments[0], uri); 131 | assertRange(commands[0].arguments[1], escape.range); 132 | assert.strictEqual(commands[1].command, CommandIds.DIRECTIVE_TO_BACKTICK); 133 | assert.strictEqual(commands[1].arguments.length, 2); 134 | assert.strictEqual(commands[1].arguments[0], uri); 135 | assertRange(commands[1].arguments[1], escape.range); 136 | assert.strictEqual(commands[2].command, CommandIds.UPPERCASE); 137 | assert.strictEqual(commands[2].arguments.length, 2); 138 | assert.strictEqual(commands[2].arguments[0], uri); 139 | assertRange(commands[2].arguments[1], lowercase.range); 140 | }); 141 | 142 | it("diagnostic code as string", function () { 143 | let diagnostic = createLowercase(); 144 | diagnostic.code = diagnostic.code.toString(); 145 | let commands = getCommands([diagnostic], uri); 146 | assert.strictEqual(commands.length, 1); 147 | assert.strictEqual(commands[0].command, CommandIds.UPPERCASE); 148 | assert.strictEqual(commands[0].arguments.length, 2); 149 | assert.strictEqual(commands[0].arguments[0], uri); 150 | assertRange(commands[0].arguments[1], diagnostic.range); 151 | }); 152 | 153 | it("unknown HEALTHCHECK flags", function () { 154 | let diagnostic = createUnknownHealthcheckFlag(); 155 | let commands = getCommands([diagnostic], uri); 156 | assert.strictEqual(commands.length, 4); 157 | assert.strictEqual(commands[0].command, CommandIds.FLAG_TO_HEALTHCHECK_INTERVAL); 158 | assert.strictEqual(commands[0].arguments.length, 2); 159 | assert.strictEqual(commands[0].arguments[0], uri); 160 | assertRange(commands[0].arguments[1], diagnostic.range); 161 | assert.strictEqual(commands[1].command, CommandIds.FLAG_TO_HEALTHCHECK_RETRIES); 162 | assert.strictEqual(commands[1].arguments.length, 2); 163 | assert.strictEqual(commands[1].arguments[0], uri); 164 | assertRange(commands[1].arguments[1], diagnostic.range); 165 | assert.strictEqual(commands[2].command, CommandIds.FLAG_TO_HEALTHCHECK_START_PERIOD); 166 | assert.strictEqual(commands[2].arguments.length, 2); 167 | assert.strictEqual(commands[2].arguments[0], uri); 168 | assertRange(commands[2].arguments[1], diagnostic.range); 169 | assert.strictEqual(commands[3].command, CommandIds.FLAG_TO_HEALTHCHECK_TIMEOUT); 170 | assert.strictEqual(commands[3].arguments.length, 2); 171 | assert.strictEqual(commands[3].arguments[0], uri); 172 | assertRange(commands[3].arguments[1], diagnostic.range); 173 | }); 174 | 175 | it("unknown ADD flags", function () { 176 | let diagnostic = createUnknownAddFlag(); 177 | let commands = getCommands([diagnostic], uri); 178 | assert.strictEqual(commands.length, 1); 179 | assert.strictEqual(commands[0].command, CommandIds.FLAG_TO_CHOWN); 180 | assert.strictEqual(commands[0].arguments.length, 2); 181 | assert.strictEqual(commands[0].arguments[0], uri); 182 | assertRange(commands[0].arguments[1], diagnostic.range); 183 | }); 184 | 185 | it("unknown COPY flags", function () { 186 | let diagnostic = createUnknownCopyFlag(); 187 | let commands = getCommands([diagnostic], uri); 188 | assert.strictEqual(commands.length, 2); 189 | assert.strictEqual(commands[0].command, CommandIds.FLAG_TO_CHOWN); 190 | assert.strictEqual(commands[0].arguments.length, 2); 191 | assert.strictEqual(commands[0].arguments[0], uri); 192 | assertRange(commands[0].arguments[1], diagnostic.range); 193 | assert.strictEqual(commands[1].command, CommandIds.FLAG_TO_COPY_FROM); 194 | assert.strictEqual(commands[1].arguments.length, 2); 195 | assert.strictEqual(commands[1].arguments[0], uri); 196 | assertRange(commands[1].arguments[1], diagnostic.range); 197 | }); 198 | 199 | it("empty continuation line", function () { 200 | let diagnostic = createEmptyContinuationLine(false); 201 | let commands = getCommands([diagnostic], uri); 202 | assert.strictEqual(commands.length, 1); 203 | assert.strictEqual(commands[0].command, CommandIds.REMOVE_EMPTY_CONTINUATION_LINE); 204 | assert.strictEqual(commands[0].title, "Remove empty continuation line"); 205 | assert.strictEqual(commands[0].arguments.length, 2); 206 | assert.strictEqual(commands[0].arguments[0], uri); 207 | assertRange(commands[0].arguments[1], diagnostic.range); 208 | }); 209 | 210 | it("empty continuation lines", function () { 211 | let diagnostic = createEmptyContinuationLine(true); 212 | let commands = getCommands([diagnostic], uri); 213 | assert.strictEqual(commands.length, 1); 214 | assert.strictEqual(commands[0].command, CommandIds.REMOVE_EMPTY_CONTINUATION_LINE); 215 | assert.strictEqual(commands[0].title, "Remove empty continuation lines"); 216 | assert.strictEqual(commands[0].arguments.length, 2); 217 | assert.strictEqual(commands[0].arguments[0], uri); 218 | assertRange(commands[0].arguments[1], diagnostic.range); 219 | }); 220 | }); 221 | 222 | describe("Dockerfile execute commands", function () { 223 | it("unknown command", function () { 224 | let edits = service.computeCommandEdits("FROM node", "unknown", []); 225 | assert.strictEqual(edits, null); 226 | }); 227 | 228 | function directiveTo(commandId: string, suggestion: string) { 229 | let range = Range.create(Position.create(0, 8), Position.create(0, 9)); 230 | let edits = service.computeCommandEdits("#escape=a", commandId, [uri, range]); 231 | assert.strictEqual(edits.length, 1); 232 | assert.strictEqual(edits[0].newText, suggestion); 233 | assert.strictEqual(edits[0].range, range); 234 | } 235 | 236 | it("directive to backslash", function () { 237 | directiveTo(CommandIds.DIRECTIVE_TO_BACKSLASH, '\\'); 238 | }); 239 | 240 | it("directive to backtick", function () { 241 | directiveTo(CommandIds.DIRECTIVE_TO_BACKTICK, '`'); 242 | }); 243 | 244 | it("extra argument", function () { 245 | let range = Range.create(Position.create(0, 10), Position.create(0, 14)); 246 | let edits = service.computeCommandEdits( 247 | "FROM node node", CommandIds.EXTRA_ARGUMENT, [uri, range] 248 | ); 249 | assert.strictEqual(edits.length, 1); 250 | assert.strictEqual(edits[0].newText, ""); 251 | assert.strictEqual(edits[0].range, range); 252 | }); 253 | 254 | it("convert to uppercase", function () { 255 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 256 | let edits = service.computeCommandEdits( 257 | "from node", CommandIds.UPPERCASE, [uri, range] 258 | ); 259 | assert.strictEqual(edits.length, 1); 260 | assert.strictEqual(edits[0].newText, "FROM"); 261 | assert.strictEqual(edits[0].range, range); 262 | }); 263 | 264 | it("convert to lowercase", function () { 265 | let range = Range.create(Position.create(0, 1), Position.create(0, 7)); 266 | let edits = service.computeCommandEdits( 267 | "#ESCAPE=`", CommandIds.LOWERCASE, [uri, range] 268 | ); 269 | assert.strictEqual(edits.length, 1); 270 | assert.strictEqual(edits[0].newText, "escape"); 271 | assert.strictEqual(edits[0].range, range); 272 | }); 273 | 274 | it("convert to AS", function () { 275 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 276 | let edits = service.computeCommandEdits( 277 | "FROM node as setup", CommandIds.CONVERT_TO_AS, [uri, range] 278 | ); 279 | assert.strictEqual(edits.length, 1); 280 | assert.strictEqual(edits[0].newText, "AS"); 281 | assert.strictEqual(edits[0].range, range); 282 | }); 283 | 284 | it("HEALTHCHECK flag to --interval", function () { 285 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 286 | let edits = service.computeCommandEdits( 287 | "", CommandIds.FLAG_TO_HEALTHCHECK_INTERVAL, [uri, range] 288 | ); 289 | assert.strictEqual(edits.length, 1); 290 | assert.strictEqual(edits[0].newText, "--interval"); 291 | assert.strictEqual(edits[0].range, range); 292 | }); 293 | 294 | it("HEALTHCHECK flag to --retries", function () { 295 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 296 | let edits = service.computeCommandEdits( 297 | "", CommandIds.FLAG_TO_HEALTHCHECK_RETRIES, [uri, range] 298 | ); 299 | assert.strictEqual(edits.length, 1); 300 | assert.strictEqual(edits[0].newText, "--retries"); 301 | assert.strictEqual(edits[0].range, range); 302 | }); 303 | 304 | it("HEALTHCHECK flag to --start-period", function () { 305 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 306 | let edits = service.computeCommandEdits( 307 | "", CommandIds.FLAG_TO_HEALTHCHECK_START_PERIOD, [uri, range] 308 | ); 309 | assert.strictEqual(edits.length, 1); 310 | assert.strictEqual(edits[0].newText, "--start-period"); 311 | assert.strictEqual(edits[0].range, range); 312 | }); 313 | 314 | it("HEALTHCHECK flag to --timeout", function () { 315 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 316 | let edits = service.computeCommandEdits( 317 | "", CommandIds.FLAG_TO_HEALTHCHECK_TIMEOUT, [uri, range] 318 | ); 319 | assert.strictEqual(edits.length, 1); 320 | assert.strictEqual(edits[0].newText, "--timeout"); 321 | assert.strictEqual(edits[0].range, range); 322 | }); 323 | 324 | it("COPY flag to --from", function () { 325 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 326 | let edits = service.computeCommandEdits( 327 | "", CommandIds.FLAG_TO_COPY_FROM, [uri, range] 328 | ); 329 | assert.strictEqual(edits.length, 1); 330 | assert.strictEqual(edits[0].newText, "--from"); 331 | assert.strictEqual(edits[0].range, range); 332 | }); 333 | 334 | it("flag to --chown", function () { 335 | let range = Range.create(Position.create(0, 0), Position.create(0, 4)); 336 | let edits = service.computeCommandEdits( 337 | "", CommandIds.FLAG_TO_CHOWN, [uri, range] 338 | ); 339 | assert.strictEqual(edits.length, 1); 340 | assert.strictEqual(edits[0].newText, "--chown"); 341 | assert.strictEqual(edits[0].range, range); 342 | }); 343 | 344 | it("remove empty continuation line", function () { 345 | let range = Range.create(Position.create(0, 0), Position.create(3, 0)); 346 | let edits = service.computeCommandEdits( 347 | "", CommandIds.REMOVE_EMPTY_CONTINUATION_LINE, [uri, range] 348 | ); 349 | assert.strictEqual(edits.length, 1); 350 | assert.strictEqual(edits[0].newText, ""); 351 | assert.strictEqual(edits[0].range, range); 352 | }); 353 | }); 354 | -------------------------------------------------------------------------------- /test/dockerFolding.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as assert from "assert"; 6 | 7 | import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; 8 | import { DockerfileLanguageServiceFactory } from '../src/main'; 9 | 10 | const service = DockerfileLanguageServiceFactory.createLanguageService(); 11 | 12 | function assertFoldingRange(lineFoldingOnly: boolean, range: FoldingRange, startLine: number, startCharacter: number, endLine: number, endCharacter: number, kind?: FoldingRangeKind) { 13 | assert.strictEqual(range.kind, kind); 14 | assert.strictEqual(range.startLine, startLine); 15 | assert.strictEqual(range.startCharacter, lineFoldingOnly ? undefined : startCharacter); 16 | assert.strictEqual(range.endLine, endLine); 17 | assert.strictEqual(range.endCharacter, lineFoldingOnly ? undefined : endCharacter); 18 | } 19 | 20 | function computeFoldingRanges(content: string, lineFoldingOnly: boolean, rangeLimit: number): FoldingRange[] { 21 | service.setCapabilities({ 22 | foldingRange: { 23 | lineFoldingOnly, 24 | rangeLimit 25 | } 26 | }) 27 | return service.computeFoldingRanges(content); 28 | } 29 | 30 | describe("Dockerfile folding", () => { 31 | describe("coments", () => { 32 | function createCommentsTests(lineFoldingOnly: boolean, rangeLimit: number) { 33 | it("single line", () => { 34 | let content = "# comment\nFROM node"; 35 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 36 | assert.strictEqual(ranges.length, 0); 37 | 38 | content = "# comment\r\nFROM node"; 39 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 40 | assert.strictEqual(ranges.length, 0); 41 | }); 42 | 43 | it("single line ignores escaped newlines", () => { 44 | let content = "# comment\n\\\n# comment2"; 45 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 46 | assert.strictEqual(ranges.length, 0); 47 | 48 | content = "# comment\r\n\\\r\n# comment2"; 49 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 50 | assert.strictEqual(ranges.length, 0); 51 | }); 52 | 53 | it("multiline 2 lines", () => { 54 | let content = "# comment\n# comment2\nFROM node"; 55 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 56 | if (rangeLimit < 1) { 57 | assert.strictEqual(ranges.length, 0); 58 | } else { 59 | assert.strictEqual(ranges.length, 1); 60 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 1, 10, FoldingRangeKind.Comment); 61 | } 62 | 63 | content = "# comment\r\n# comment2\r\nFROM node"; 64 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 65 | if (rangeLimit < 1) { 66 | assert.strictEqual(ranges.length, 0); 67 | } else { 68 | assert.strictEqual(ranges.length, 1); 69 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 1, 10, FoldingRangeKind.Comment); 70 | } 71 | }); 72 | 73 | it("multiline 3 lines", () => { 74 | let content = "# comment\n# comment2\n# comment3\nFROM node"; 75 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 76 | if (rangeLimit < 1) { 77 | assert.strictEqual(ranges.length, 0); 78 | } else { 79 | assert.strictEqual(ranges.length, 1); 80 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 2, 10, FoldingRangeKind.Comment); 81 | } 82 | 83 | content = "# comment\r\n# comment2\r\n# comment3\r\nFROM node"; 84 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 85 | if (rangeLimit < 1) { 86 | assert.strictEqual(ranges.length, 0); 87 | } else { 88 | assert.strictEqual(ranges.length, 1); 89 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 2, 10, FoldingRangeKind.Comment); 90 | } 91 | }); 92 | 93 | it("multiline space between", () => { 94 | let content = "# comment\n# comment2\n\n# comment3\n# comment4\nFROM node"; 95 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 96 | if (rangeLimit < 1) { 97 | assert.strictEqual(ranges.length, 0); 98 | } else if (rangeLimit == 1) { 99 | assert.strictEqual(ranges.length, 1); 100 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 1, 10, FoldingRangeKind.Comment); 101 | } else { 102 | assert.strictEqual(ranges.length, 2); 103 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 1, 10, FoldingRangeKind.Comment); 104 | assertFoldingRange(lineFoldingOnly, ranges[1], 3, 10, 4, 10, FoldingRangeKind.Comment); 105 | } 106 | 107 | content = "# comment\r\n# comment2\r\n\r\n# comment3\r\n# comment4\r\nFROM node"; 108 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 109 | if (rangeLimit < 1) { 110 | assert.strictEqual(ranges.length, 0); 111 | } else if (rangeLimit == 1) { 112 | assert.strictEqual(ranges.length, 1); 113 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 1, 10, FoldingRangeKind.Comment); 114 | } else { 115 | assert.strictEqual(ranges.length, 2); 116 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 9, 1, 10, FoldingRangeKind.Comment); 117 | assertFoldingRange(lineFoldingOnly, ranges[1], 3, 10, 4, 10, FoldingRangeKind.Comment); 118 | } 119 | }); 120 | 121 | it("multiline ignores directive", () => { 122 | let content = "#escape=`\n# comment\n# comment2\nFROM node"; 123 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 124 | if (rangeLimit < 1) { 125 | assert.strictEqual(ranges.length, 0); 126 | } else { 127 | assert.strictEqual(ranges.length, 1); 128 | assertFoldingRange(lineFoldingOnly, ranges[0], 1, 9, 2, 10, FoldingRangeKind.Comment); 129 | } 130 | 131 | content = "#escape=`\r\n# comment\r\n# comment2\r\nFROM node"; 132 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 133 | if (rangeLimit < 1) { 134 | assert.strictEqual(ranges.length, 0); 135 | } else { 136 | assert.strictEqual(ranges.length, 1); 137 | assertFoldingRange(lineFoldingOnly, ranges[0], 1, 9, 2, 10, FoldingRangeKind.Comment); 138 | } 139 | }); 140 | 141 | it("multiline false directive", () => { 142 | let content = "\n#escape=`\n# comment\n# comment2\nFROM node"; 143 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 144 | if (rangeLimit < 1) { 145 | assert.strictEqual(ranges.length, 0); 146 | } else { 147 | assert.strictEqual(ranges.length, 1); 148 | assertFoldingRange(lineFoldingOnly, ranges[0], 1, 9, 3, 10, FoldingRangeKind.Comment); 149 | } 150 | 151 | content = "\r\n#escape=`\r\n# comment\r\n# comment2\r\nFROM node"; 152 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 153 | if (rangeLimit < 1) { 154 | assert.strictEqual(ranges.length, 0); 155 | } else { 156 | assert.strictEqual(ranges.length, 1); 157 | assertFoldingRange(lineFoldingOnly, ranges[0], 1, 9, 3, 10, FoldingRangeKind.Comment); 158 | } 159 | }); 160 | } 161 | 162 | describe("unlimited", () => { 163 | describe("standard", () => { 164 | createCommentsTests(false, Number.MAX_VALUE); 165 | }); 166 | 167 | describe("line folding only", () => { 168 | createCommentsTests(true, Number.MAX_VALUE); 169 | }); 170 | }); 171 | 172 | describe("zero folding ranges", () => { 173 | describe("standard", () => { 174 | createCommentsTests(false, 0); 175 | }); 176 | 177 | describe("line folding only", () => { 178 | createCommentsTests(true, 0); 179 | }); 180 | }); 181 | 182 | describe("one folding range", () => { 183 | describe("standard", () => { 184 | createCommentsTests(false, 1); 185 | }); 186 | 187 | describe("line folding only", () => { 188 | createCommentsTests(true, 1); 189 | }); 190 | }); 191 | 192 | describe("two folding ranges", () => { 193 | describe("standard", () => { 194 | createCommentsTests(false, 2); 195 | }); 196 | 197 | describe("line folding only", () => { 198 | createCommentsTests(true, 2); 199 | }); 200 | }); 201 | }); 202 | 203 | describe("instructions", () => { 204 | function createInstructionsTests(lineFoldingOnly: boolean, rangeLimit: number) { 205 | it("single line", () => { 206 | let content = "FROM node"; 207 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 208 | assert.strictEqual(ranges.length, 0); 209 | }); 210 | 211 | it("multiline escaped newline", () => { 212 | let content = "FROM node\\\nAS setup"; 213 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 214 | if (rangeLimit < 1) { 215 | assert.strictEqual(ranges.length, 0); 216 | } else { 217 | assert.strictEqual(ranges.length, 1); 218 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 1, 8); 219 | } 220 | 221 | content = "FROM node\\\r\nAS setup"; 222 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 223 | if (rangeLimit < 1) { 224 | assert.strictEqual(ranges.length, 0); 225 | } else { 226 | assert.strictEqual(ranges.length, 1); 227 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 1, 8); 228 | } 229 | }); 230 | 231 | it("multiline with escaped empty newlines", () => { 232 | let content = "FROM node\\\n\nAS setup"; 233 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 234 | if (rangeLimit < 1) { 235 | assert.strictEqual(ranges.length, 0); 236 | } else { 237 | assert.strictEqual(ranges.length, 1); 238 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 2, 8); 239 | } 240 | 241 | content = "FROM node\\\r\n\r\nAS setup"; 242 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 243 | if (rangeLimit < 1) { 244 | assert.strictEqual(ranges.length, 0); 245 | } else { 246 | assert.strictEqual(ranges.length, 1); 247 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 2, 8); 248 | } 249 | }); 250 | 251 | it("multiline space between", () => { 252 | let content = "FROM node\\\nAS setup\n\nFROM alpine\\\nAS base"; 253 | let ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 254 | if (rangeLimit < 1) { 255 | assert.strictEqual(ranges.length, 0); 256 | } else if (rangeLimit == 1) { 257 | assert.strictEqual(ranges.length, 1); 258 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 1, 8); 259 | } else { 260 | assert.strictEqual(ranges.length, 2); 261 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 1, 8); 262 | assertFoldingRange(lineFoldingOnly, ranges[1], 3, 12, 4, 7); 263 | } 264 | 265 | content = "FROM node\\\r\nAS setup\r\n\r\nFROM alpine\\\r\nAS base"; 266 | ranges = computeFoldingRanges(content, lineFoldingOnly, rangeLimit); 267 | if (rangeLimit < 1) { 268 | assert.strictEqual(ranges.length, 0); 269 | } else if (rangeLimit == 1) { 270 | assert.strictEqual(ranges.length, 1); 271 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 1, 8); 272 | } else { 273 | assert.strictEqual(ranges.length, 2); 274 | assertFoldingRange(lineFoldingOnly, ranges[0], 0, 10, 1, 8); 275 | assertFoldingRange(lineFoldingOnly, ranges[1], 3, 12, 4, 7); 276 | } 277 | }); 278 | } 279 | 280 | describe("unlimited", () => { 281 | describe("standard", () => { 282 | createInstructionsTests(false, Number.MAX_VALUE); 283 | }); 284 | 285 | describe("line folding only", () => { 286 | createInstructionsTests(true, Number.MAX_VALUE); 287 | }); 288 | }); 289 | 290 | describe("zero folding ranges", () => { 291 | describe("standard", () => { 292 | createInstructionsTests(false, 0); 293 | }); 294 | 295 | describe("line folding only", () => { 296 | createInstructionsTests(true, 0); 297 | }); 298 | }); 299 | 300 | describe("one folding range", () => { 301 | describe("standard", () => { 302 | createInstructionsTests(false, 1); 303 | }); 304 | 305 | describe("line folding only", () => { 306 | createInstructionsTests(true, 1); 307 | }); 308 | }); 309 | 310 | describe("two folding ranges", () => { 311 | describe("standard", () => { 312 | createInstructionsTests(false, 2); 313 | }); 314 | 315 | describe("line folding only", () => { 316 | createInstructionsTests(true, 2); 317 | }); 318 | }); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /test/dockerLinks.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as assert from "assert"; 6 | 7 | import { DocumentLink } from 'vscode-languageserver-types'; 8 | import { DockerfileLanguageServiceFactory } from '../src/main'; 9 | 10 | let service = DockerfileLanguageServiceFactory.createLanguageService(); 11 | 12 | function assertLink(documentLink: DocumentLink, target: string, data: string, startLine: number, startCharacter: number, endLine: number, endCharacter: number) { 13 | assert.strictEqual(documentLink.target, target); 14 | assert.strictEqual(documentLink.data, data); 15 | assert.strictEqual(documentLink.range.start.line, startLine); 16 | assert.strictEqual(documentLink.range.start.character, startCharacter); 17 | assert.strictEqual(documentLink.range.end.line, endLine); 18 | assert.strictEqual(documentLink.range.end.character, endCharacter); 19 | } 20 | 21 | describe("Dockerfile links", function () { 22 | describe("Docker Hub", function() { 23 | it("FROM", function () { 24 | let links = service.computeLinks("FROM"); 25 | assert.strictEqual(links.length, 0); 26 | }); 27 | 28 | it("FROM node", function () { 29 | let links = service.computeLinks("FROM node"); 30 | assert.strictEqual(links.length, 1); 31 | assertLink(links[0], undefined, "_/node/", 0, 5, 0, 9); 32 | links[0] = service.resolveLink(links[0]); 33 | assertLink(links[0], "https://hub.docker.com/_/node/", "_/node/", 0, 5, 0, 9); 34 | }); 35 | 36 | it("FROM node:latest", function () { 37 | let links = service.computeLinks("FROM node:latest"); 38 | assert.strictEqual(links.length, 1); 39 | assertLink(links[0], undefined, "_/node/", 0, 5, 0, 9); 40 | links[0] = service.resolveLink(links[0]); 41 | assertLink(links[0], "https://hub.docker.com/_/node/", "_/node/", 0, 5, 0, 9); 42 | }); 43 | 44 | it("FROM node@sha256:613685c22f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700", function () { 45 | let links = service.computeLinks("FROM node@sha256:613685c22f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700"); 46 | assert.strictEqual(links.length, 1); 47 | assertLink(links[0], undefined, "_/node/", 0, 5, 0, 9); 48 | links[0] = service.resolveLink(links[0]); 49 | assertLink(links[0], "https://hub.docker.com/_/node/", "_/node/", 0, 5, 0, 9); 50 | }); 51 | 52 | it("FROM microsoft/dotnet", function () { 53 | let links = service.computeLinks("FROM microsoft/dotnet"); 54 | assert.strictEqual(links.length, 1); 55 | assertLink(links[0], undefined, "r/microsoft/dotnet/", 0, 5, 0, 21); 56 | links[0] = service.resolveLink(links[0]); 57 | assertLink(links[0], "https://hub.docker.com/r/microsoft/dotnet/", "r/microsoft/dotnet/", 0, 5, 0, 21); 58 | }); 59 | 60 | it("FROM microsoft/dotnet:sdk", function () { 61 | let links = service.computeLinks("FROM microsoft/dotnet:sdk"); 62 | assert.strictEqual(links.length, 1); 63 | assertLink(links[0], undefined, "r/microsoft/dotnet/", 0, 5, 0, 21); 64 | links[0] = service.resolveLink(links[0]); 65 | assertLink(links[0], "https://hub.docker.com/r/microsoft/dotnet/", "r/microsoft/dotnet/", 0, 5, 0, 21); 66 | }); 67 | 68 | it("FROM microsoft/dotnet@sha256:5483e2b609c0f66c3ebd96666de7b0a74537613b43565879ecb0d0a73e845d7d", function () { 69 | let links = service.computeLinks("FROM microsoft/dotnet@sha256:5483e2b609c0f66c3ebd96666de7b0a74537613b43565879ecb0d0a73e845d7d"); 70 | assert.strictEqual(links.length, 1); 71 | assertLink(links[0], undefined, "r/microsoft/dotnet/", 0, 5, 0, 21); 72 | links[0] = service.resolveLink(links[0]); 73 | assertLink(links[0], "https://hub.docker.com/r/microsoft/dotnet/", "r/microsoft/dotnet/", 0, 5, 0, 21); 74 | }); 75 | 76 | it("FROM microsoft/dotnet:non-existent-tag@sha256:5483e2b609c0f66c3ebd96666de7b0a74537613b43565879ecb0d0a73e845d7d", () => { 77 | const links = service.computeLinks("FROM microsoft/dotnet:non-existent-tag@sha256:5483e2b609c0f66c3ebd96666de7b0a74537613b43565879ecb0d0a73e845d7d"); 78 | assert.strictEqual(links.length, 1); 79 | assertLink(links[0], undefined, "r/microsoft/dotnet/", 0, 5, 0, 21); 80 | links[0] = service.resolveLink(links[0]); 81 | assertLink(links[0], "https://hub.docker.com/r/microsoft/dotnet/", "r/microsoft/dotnet/", 0, 5, 0, 21); 82 | }); 83 | }); 84 | 85 | describe("ghcr.io", function() { 86 | it("FROM ghcr.io/super-linter/super-linter", function() { 87 | const links = service.computeLinks("FROM ghcr.io/super-linter/super-linter"); 88 | assert.strictEqual(links.length, 1); 89 | assertLink(links[0], "https://github.com/super-linter/super-linter/pkgs/container/super-linter", undefined, 0, 5, 0, 38); 90 | links[0] = service.resolveLink(links[0]); 91 | assertLink(links[0], "https://github.com/super-linter/super-linter/pkgs/container/super-linter", undefined, 0, 5, 0, 38); 92 | }); 93 | 94 | it("FROM ghcr.io/super-linter/super-linter:latest-buildcache", function() { 95 | const links = service.computeLinks("FROM ghcr.io/super-linter/super-linter:latest-buildcache"); 96 | assert.strictEqual(links.length, 1); 97 | assertLink(links[0], "https://github.com/super-linter/super-linter/pkgs/container/super-linter", undefined, 0, 5, 0, 38); 98 | links[0] = service.resolveLink(links[0]); 99 | assertLink(links[0], "https://github.com/super-linter/super-linter/pkgs/container/super-linter", undefined, 0, 5, 0, 38); 100 | }); 101 | 102 | it("FROM ghcr.io/super-linter", function() { 103 | const links = service.computeLinks("FROM ghcr.io/super-linter"); 104 | assert.strictEqual(links.length, 0); 105 | }); 106 | }); 107 | 108 | describe("mcr.microsoft.com", function() { 109 | it("FROM mcr.microsoft.com/powershell", function() { 110 | const links = service.computeLinks("FROM mcr.microsoft.com/powershell"); 111 | assert.strictEqual(links.length, 1); 112 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/powershell", undefined, 0, 5, 0, 33); 113 | links[0] = service.resolveLink(links[0]); 114 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/powershell", undefined, 0, 5, 0, 33); 115 | }); 116 | 117 | it("FROM mcr.microsoft.com/powershell:3.17", function() { 118 | const links = service.computeLinks("FROM mcr.microsoft.com/powershell:3.17"); 119 | assert.strictEqual(links.length, 1); 120 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/powershell", undefined, 0, 5, 0, 33); 121 | links[0] = service.resolveLink(links[0]); 122 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/powershell", undefined, 0, 5, 0, 33); 123 | }); 124 | 125 | it("FROM mcr.microsoft.com/windows/servercore", function() { 126 | const links = service.computeLinks("FROM mcr.microsoft.com/windows/servercore"); 127 | assert.strictEqual(links.length, 1); 128 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/windows/servercore", undefined, 0, 5, 0, 41); 129 | links[0] = service.resolveLink(links[0]); 130 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/windows/servercore", undefined, 0, 5, 0, 41); 131 | }); 132 | 133 | it("FROM mcr.microsoft.com/windows/servercore:ltsc2025", function() { 134 | const links = service.computeLinks("FROM mcr.microsoft.com/windows/servercore:ltsc2025"); 135 | assert.strictEqual(links.length, 1); 136 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/windows/servercore", undefined, 0, 5, 0, 41); 137 | links[0] = service.resolveLink(links[0]); 138 | assertLink(links[0], "https://mcr.microsoft.com/artifact/mar/windows/servercore", undefined, 0, 5, 0, 41); 139 | }); 140 | }); 141 | 142 | describe("quay.io", function() { 143 | it("FROM quay.io/prometheus/node-exporter", function() { 144 | const links = service.computeLinks("FROM quay.io/prometheus/node-exporter"); 145 | assert.strictEqual(links.length, 1); 146 | assertLink(links[0], "https://quay.io/repository/prometheus/node-exporter", undefined, 0, 5, 0, 37); 147 | links[0] = service.resolveLink(links[0]); 148 | assertLink(links[0], "https://quay.io/repository/prometheus/node-exporter", undefined, 0, 5, 0, 37); 149 | }); 150 | 151 | it("FROM quay.io/prometheus/node-exporter:v1.9.1", function() { 152 | const links = service.computeLinks("FROM quay.io/prometheus/node-exporter:v1.9.1"); 153 | assert.strictEqual(links.length, 1); 154 | assertLink(links[0], "https://quay.io/repository/prometheus/node-exporter", undefined, 0, 5, 0, 37); 155 | links[0] = service.resolveLink(links[0]); 156 | assertLink(links[0], "https://quay.io/repository/prometheus/node-exporter", undefined, 0, 5, 0, 37); 157 | }); 158 | }); 159 | 160 | describe("build stages", function () { 161 | it("valid", () => { 162 | let links = service.computeLinks("FROM node AS base\nFROM base"); 163 | assert.strictEqual(links.length, 1); 164 | assertLink(links[0], undefined, "_/node/", 0, 5, 0, 9); 165 | links[0] = service.resolveLink(links[0]); 166 | assertLink(links[0], "https://hub.docker.com/_/node/", "_/node/", 0, 5, 0, 9); 167 | }); 168 | 169 | it("wrong stage name", () => { 170 | let links = service.computeLinks("FROM node AS base\nFROM base2"); 171 | assert.strictEqual(links.length, 2); 172 | assertLink(links[0], undefined, "_/node/", 0, 5, 0, 9); 173 | links[0] = service.resolveLink(links[0]); 174 | assertLink(links[0], "https://hub.docker.com/_/node/", "_/node/", 0, 5, 0, 9); 175 | assertLink(links[1], undefined, "_/base2/", 1, 5, 1, 10); 176 | links[1] = service.resolveLink(links[1]); 177 | assertLink(links[1], "https://hub.docker.com/_/base2/", "_/base2/", 1, 5, 1, 10); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/dockerValidate.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Remy Suen. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as assert from "assert"; 6 | 7 | import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; 8 | import { ValidationCode, ValidationSeverity } from 'dockerfile-utils'; 9 | import { DockerfileLanguageServiceFactory } from '../src/main'; 10 | 11 | const service = DockerfileLanguageServiceFactory.createLanguageService(); 12 | 13 | function assertInstructionCasing(diagnostic: Diagnostic, severity: DiagnosticSeverity) { 14 | assert.strictEqual(diagnostic.code, ValidationCode.CASING_INSTRUCTION); 15 | assert.strictEqual(diagnostic.severity, severity); 16 | } 17 | 18 | describe("Docker Validation Tests", () => { 19 | it("settings ignore case default", () => { 20 | let content = "from node"; 21 | let problems = service.validate(content); 22 | assert.strictEqual(1, problems.length); 23 | assertInstructionCasing(problems[0], DiagnosticSeverity.Warning); 24 | }); 25 | 26 | it("settings ignore case ignore", () => { 27 | let content = "from node"; 28 | let problems = service.validate(content, { instructionCasing: ValidationSeverity.IGNORE }); 29 | assert.strictEqual(0, problems.length); 30 | }); 31 | 32 | it("settings ignore case warning", () => { 33 | let content = "from node"; 34 | let problems = service.validate(content, { instructionCasing: ValidationSeverity.WARNING }); 35 | assert.strictEqual(1, problems.length); 36 | assertInstructionCasing(problems[0], DiagnosticSeverity.Warning); 37 | }); 38 | 39 | it("settings ignore case error", () => { 40 | let content = "from node"; 41 | let problems = service.validate(content, { instructionCasing: ValidationSeverity.ERROR }); 42 | assert.strictEqual(1, problems.length); 43 | assertInstructionCasing(problems[0], DiagnosticSeverity.Error); 44 | }); 45 | 46 | it("issue #103", () => { 47 | const content = "FROM alpine\nCOPY --link . ."; 48 | const problems = service.validate(content, { instructionCasing: ValidationSeverity.ERROR }); 49 | assert.strictEqual(0, problems.length); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "lib": [ 8 | "es2016" 9 | ], 10 | "outDir": "out" 11 | }, 12 | "exclude": [ 13 | "example", 14 | "node_modules" 15 | ] 16 | } --------------------------------------------------------------------------------