From c1a7bcf3e2b3f34530ab9fcf3b0029c7710ddeb9 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Fri, 6 Nov 2015 10:31:24 -0800 Subject: [PATCH 1/2] Updating the extension to use the new API. --- dockerExtension.ts | 30 +-- dockerHoverProvider.ts | 82 ++++++++ dockerHubApi.ts | 40 ++-- .../dockerfileCompletionItemProvider.ts | 30 +++ dockerfile/dockerfileDef.ts | 84 -------- dockerfile/dockerfileExtraInfo.ts | 191 ----------------- dockerfile/dockerfileKeyInfo.ts | 5 +- dockerfile/dockerfileParser.ts | 53 +++++ dockerfile/dockerfileSuggestSupport.ts | 35 ---- helpers/htmlHelper.ts | 79 ------- helpers/suggestSupportHelper.ts | 100 +++++++-- package.json | 53 +++-- parser.ts | 121 +++-------- tsconfig.json | 3 +- yaml/yamlCompletionItemProvider.ts | 75 +++++++ yaml/yamlExtraInfo.ts | 197 ------------------ yaml/yamlKeyInfo.ts | 37 +++- yaml/yamlParser.ts | 85 ++++++++ yaml/yamlSuggestSupport.ts | 80 ------- 19 files changed, 550 insertions(+), 830 deletions(-) create mode 100644 dockerHoverProvider.ts create mode 100644 dockerfile/dockerfileCompletionItemProvider.ts delete mode 100644 dockerfile/dockerfileDef.ts delete mode 100644 dockerfile/dockerfileExtraInfo.ts create mode 100644 dockerfile/dockerfileParser.ts delete mode 100644 dockerfile/dockerfileSuggestSupport.ts delete mode 100644 helpers/htmlHelper.ts create mode 100644 yaml/yamlCompletionItemProvider.ts delete mode 100644 yaml/yamlExtraInfo.ts create mode 100644 yaml/yamlParser.ts delete mode 100644 yaml/yamlSuggestSupport.ts diff --git a/dockerExtension.ts b/dockerExtension.ts index b8784094d0..dcf06d37e7 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -2,21 +2,23 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import dockerfileDef = require('./dockerfile/dockerfileDef'); -import dockerExtraInfoSupport = require('./dockerfile/dockerfileExtraInfo'); -import dockerSuggestSupport = require('./dockerfile/dockerfileSuggestSupport'); -import yamlExtraInfoSupport = require('./yaml/yamlExtraInfo'); -import yamlSuggestSupport = require('./yaml/yamlSuggestSupport'); +import {DockerHoverProvider} from './dockerHoverProvider'; +import {DockerfileCompletionItemProvider} from './dockerfile/dockerfileCompletionItemProvider'; +import {YamlCompletionItemProvider} from './yaml/yamlCompletionItemProvider'; +import {DOCKER_KEY_INFO} from './dockerfile/dockerfileKeyInfo'; +import {YAML_KEY_INFO} from './yaml/yamlKeyInfo'; +import {YamlParser} from './yaml/yamlParser'; +import {DockerfileParser} from './dockerfile/dockerfileParser'; import vscode = require('vscode'); -export function activate(subscriptions: vscode.Disposable[]) { - - var DOCKERFILE_MODE_ID = 'dockerfile'; - subscriptions.push(vscode.Modes.registerMonarchDefinition(DOCKERFILE_MODE_ID, dockerfileDef.language)); - subscriptions.push(vscode.Modes.ExtraInfoSupport.register(DOCKERFILE_MODE_ID, new dockerExtraInfoSupport.ExtraInfoSupport())); - subscriptions.push(vscode.Modes.SuggestSupport.register(DOCKERFILE_MODE_ID, new dockerSuggestSupport.SuggestSupport())); +export function activate(ctx: vscode.ExtensionContext): void { + const DOCKERFILE_MODE_ID: vscode.DocumentFilter = { language: 'dockerfile', scheme: 'file' }; + var dockerHoverProvider = new DockerHoverProvider(new DockerfileParser(), DOCKER_KEY_INFO); + ctx.subscriptions.push(vscode.languages.registerHoverProvider(DOCKERFILE_MODE_ID, dockerHoverProvider)); + ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(DOCKERFILE_MODE_ID, new DockerfileCompletionItemProvider(), '.')); - var YAML_MODE_ID = 'yaml'; - subscriptions.push(vscode.Modes.ExtraInfoSupport.register(YAML_MODE_ID, new yamlExtraInfoSupport.ExtraInfoSupport())); - subscriptions.push(vscode.Modes.SuggestSupport.register(YAML_MODE_ID, new yamlSuggestSupport.SuggestSupport())); + const YAML_MODE_ID: vscode.DocumentFilter = { language: 'yaml', scheme: 'file' }; + var yamlHoverProvider = new DockerHoverProvider(new YamlParser(), YAML_KEY_INFO); + ctx.subscriptions.push(vscode.languages.registerHoverProvider(YAML_MODE_ID, yamlHoverProvider)); + ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(YAML_MODE_ID, new YamlCompletionItemProvider(), '.')) } \ No newline at end of file diff --git a/dockerHoverProvider.ts b/dockerHoverProvider.ts new file mode 100644 index 0000000000..39e9b58563 --- /dev/null +++ b/dockerHoverProvider.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import { Range, TextDocument, Position, CancellationToken, HoverProvider, Hover, MarkedString } from 'vscode'; +import parser = require('./parser'); +import hub = require('./dockerHubApi'); +import suggestHelper = require('./helpers/suggestSupportHelper'); + +export class DockerHoverProvider implements HoverProvider { + _parser: parser.Parser; + _keyInfo: { [keyName: string]: string; }; + + // Provide the parser you want to use as well as keyinfo dictionary. + constructor(wordParser: parser.Parser, keyInfo: { [keyName: string]: string; }) { + this._parser = wordParser; + this._keyInfo = keyInfo; + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { + if (!this._parser.isFileSupported(document.fileName)) { + return Promise.resolve(null); + } + + var line = document.lineAt(position.line); + + if (line.text.length === 0) { + return Promise.resolve(null); + } + + var tokens = this._parser.parseLine(line); + return this._computeInfoForLineWithTokens(line.text, tokens, position); + } + + private _computeInfoForLineWithTokens(line: string, tokens: parser.IToken[], position: Position): Promise { + var possibleTokens = this._parser.tokensAtColumn(tokens, position.character); + + return Promise.all(possibleTokens.map(tokenIndex => this._computeInfoForToken(line, tokens, tokenIndex))).then((results) => { + return possibleTokens.map((tokenIndex, arrayIndex) => { + return { + startIndex: tokens[tokenIndex].startIndex, + endIndex: tokens[tokenIndex].endIndex, + result: results[arrayIndex] + }; + }); + }).then((results) => { + var r = results.filter(r => !!r.result); + if (r.length === 0) { + return null; + } + + let range = new Range(position.line, r[0].startIndex + 1, position.line, r[0].endIndex + 1); + let hover = new Hover(r[0].result, range); + return hover; + }); + } + + private _computeInfoForToken(line: string, tokens: parser.IToken[], tokenIndex: number): Promise { + // ------------- + // Detect hovering on a key + if (tokens[tokenIndex].type === parser.TokenType.Key) { + var keyName = this._parser.keyNameFromKeyToken(this._parser.tokenValue(line, tokens[tokenIndex])).trim(); + var r = this._keyInfo[keyName]; + if (r) { + return Promise.resolve([r]); + } + } + + // ------------- + // Detect <> + // Detect <> + var helper = new suggestHelper.SuggestSupportHelper(); + var r2 = helper.getImageNameHover(line, this._parser, tokens, tokenIndex); + if (r2) { + return r2; + } + + return null; + } +} \ No newline at end of file diff --git a/dockerHubApi.ts b/dockerHubApi.ts index 57b93af87a..2fa6e0c1e0 100644 --- a/dockerHubApi.ts +++ b/dockerHubApi.ts @@ -7,8 +7,8 @@ import vscode = require('vscode'); import https = require('https'); -export function tagsForImage(image:IHubSearchResponseResult): string { - var tags:string[] = []; +export function tagsForImage(image: IHubSearchResponseResult): string { + var tags: string[] = []; if (image.is_automated) { tags.push('Automated'); } else if (image.is_trusted) { @@ -22,7 +22,7 @@ export function tagsForImage(image:IHubSearchResponseResult): string { return ''; } -export function searchImageInRegistryHub(imageName:string, cache:boolean): Promise { +export function searchImageInRegistryHub(imageName: string, cache: boolean): Promise { return invokeHubSearch(imageName, 1, cache).then((data) => { if (data.results.length === 0) { return null; @@ -32,19 +32,19 @@ export function searchImageInRegistryHub(imageName:string, cache:boolean): Promi } var popular = [ - {"is_automated":false,"name":"redis","is_trusted":false,"is_official":true,"star_count":831,"description":"Redis is an open source key-value store that functions as a data structure server."}, - {"is_automated":false,"name":"ubuntu","is_trusted":false,"is_official":true,"star_count":1827,"description":"Ubuntu is a Debian-based Linux operating system based on free software."}, - {"is_automated":false,"name":"wordpress","is_trusted":false,"is_official":true,"star_count":350,"description":"The WordPress rich content management system can utilize plugins, widgets, and themes."}, - {"is_automated":false,"name":"mysql","is_trusted":false,"is_official":true,"star_count":767,"description":"MySQL is a widely used, open-source relational database management system (RDBMS)."}, - {"is_automated":false,"name":"mongo","is_trusted":false,"is_official":true,"star_count":683,"description":"MongoDB document databases provide high availability and easy scalability."}, - {"is_automated":false,"name":"centos","is_trusted":false,"is_official":true,"star_count":1066,"description":"The official build of CentOS."}, - {"is_automated":false,"name":"node","is_trusted":false,"is_official":true,"star_count":787,"description":"Node.js is a JavaScript-based platform for server-side and networking applications."}, - {"is_automated":false,"name":"nginx","is_trusted":false,"is_official":true,"star_count":993,"description":"Official build of Nginx."}, - {"is_automated":false,"name":"postgres","is_trusted":false,"is_official":true,"star_count":778,"description":"The PostgreSQL object-relational database system provides reliability and data integrity."}, - {"is_automated":true,"name":"microsoft/aspnet","is_trusted":true,"is_official":false,"star_count":186,"description":"ASP.NET is an open source server-side Web application framework"} + { "is_automated": false, "name": "redis", "is_trusted": false, "is_official": true, "star_count": 1300, "description": "Redis is an open source key-value store that functions as a data structure server." }, + { "is_automated": false, "name": "ubuntu", "is_trusted": false, "is_official": true, "star_count": 2600, "description": "Ubuntu is a Debian-based Linux operating system based on free software." }, + { "is_automated": false, "name": "wordpress", "is_trusted": false, "is_official": true, "star_count": 582, "description": "The WordPress rich content management system can utilize plugins, widgets, and themes." }, + { "is_automated": false, "name": "mysql", "is_trusted": false, "is_official": true, "star_count": 1300, "description": "MySQL is a widely used, open-source relational database management system (RDBMS)." }, + { "is_automated": false, "name": "mongo", "is_trusted": false, "is_official": true, "star_count": 1100, "description": "MongoDB document databases provide high availability and easy scalability." }, + { "is_automated": false, "name": "centos", "is_trusted": false, "is_official": true, "star_count": 1600, "description": "The official build of CentOS." }, + { "is_automated": false, "name": "node", "is_trusted": false, "is_official": true, "star_count": 1200, "description": "Node.js is a JavaScript-based platform for server-side and networking applications." }, + { "is_automated": false, "name": "nginx", "is_trusted": false, "is_official": true, "star_count": 1600, "description": "Official build of Nginx." }, + { "is_automated": false, "name": "postgres", "is_trusted": false, "is_official": true, "star_count": 1200, "description": "The PostgreSQL object-relational database system provides reliability and data integrity." }, + { "is_automated": true, "name": "microsoft/aspnet", "is_trusted": true, "is_official": false, "star_count": 277, "description": "ASP.NET is an open source server-side Web application framework" } ]; -export function searchImagesInRegistryHub(prefix:string, cache:boolean): Promise { +export function searchImagesInRegistryHub(prefix: string, cache: boolean): Promise { if (prefix.length === 0) { // return the popular images if user invoked intellisense // right after typing the keyword and ':' (e.g. 'image:'). @@ -75,7 +75,7 @@ export function searchImagesInRegistryHub(prefix:string, cache:boolean): Promise // "query": "redis", // "page": 1 // } -function invokeHubSearch(imageName:string, count:number, cache:boolean): Promise { +function invokeHubSearch(imageName: string, count: number, cache: boolean): Promise { // https://registry.hub.docker.com/v1/search?q=redis&n=1 return fetchHttpsJson({ hostname: 'registry.hub.docker.com', @@ -101,9 +101,9 @@ export interface IHubSearchResponseResult { description: string; } -var JSON_CACHE:any = {}; +var JSON_CACHE: any = {}; -function fetchHttpsJson(opts:https.RequestOptions, cache:boolean): Promise { +function fetchHttpsJson(opts: https.RequestOptions, cache: boolean): Promise { if (!cache) { return doFetchHttpsJson(opts); } @@ -119,7 +119,7 @@ function fetchHttpsJson(opts:https.RequestOptions, cache:boolean): Promise }); } -function doFetchHttpsJson(opts:https.RequestOptions): Promise { +function doFetchHttpsJson(opts: https.RequestOptions): Promise { opts.headers = opts.headers || {}; opts.headers['Accept'] = 'application/json'; return httpsRequestAsPromise(opts).then((data) => { @@ -127,12 +127,12 @@ function doFetchHttpsJson(opts:https.RequestOptions): Promise { }) } -function httpsRequestAsPromise(opts:https.RequestOptions, token?: vscode.CancellationToken): Promise { +function httpsRequestAsPromise(opts: https.RequestOptions, token?: vscode.CancellationToken): Promise { return new Promise((resolve, reject) => { var req = https.request(opts, (res) => { var data = ''; - res.on('data', (d:string) => { + res.on('data', (d: string) => { data += d; }) res.on('end', () => { diff --git a/dockerfile/dockerfileCompletionItemProvider.ts b/dockerfile/dockerfileCompletionItemProvider.ts new file mode 100644 index 0000000000..7d4e3ff7b8 --- /dev/null +++ b/dockerfile/dockerfileCompletionItemProvider.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import { CompletionItemProvider, TextDocument, Position, CancellationToken, CompletionItem } from 'vscode'; +import helper = require('../helpers/suggestSupportHelper'); + +// IntelliSense +export class DockerfileCompletionItemProvider implements CompletionItemProvider { + + public triggerCharacters: string[] = []; + public excludeTokens: string[] = []; + + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { + var dockerSuggestSupport = new helper.SuggestSupportHelper(); + + var textLine = document.lineAt(position.line); + + // Matches strings like: 'FROM imagename' + var fromTextDocker = textLine.text.match(/^\s*FROM\s*([^"]*)$/); + + if (fromTextDocker) { + return dockerSuggestSupport.suggestImages(fromTextDocker[1]); + } + + return Promise.resolve([]); + } +} \ No newline at end of file diff --git a/dockerfile/dockerfileDef.ts b/dockerfile/dockerfileDef.ts deleted file mode 100644 index cac2c59588..0000000000 --- a/dockerfile/dockerfileDef.ts +++ /dev/null @@ -1,84 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); - -export var language = { - displayName: 'Dockerfile', - name: 'dockerfile', - defaultToken: '', - - instructions: /FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|ENTRYPOINT/, - - instructionAfter: /ONBUILD/, - - variableAfter: /ENV/, - - variable:/\${?[\w]+}?/, - - tokenizer: { - root: [ - { include: '@whitespace' }, - { include: '@comment' }, - - [/(@instructionAfter)(\s+)/, ['keyword', { token: '', next: '@instructions' }]], - ['', 'keyword', '@instructions'] - ], - - instructions: [ - [/(@variableAfter)(\s+)([\w]+)/, ['keyword', '',{token:'variable', next:'@arguments'}]], - [/(@instructions)/, 'keyword', '@arguments'] - ], - - arguments: [ - { include: '@whitespace' }, - { include: '@strings' }, - - [/(@variable)/, { cases: { '@eos': {token:'variable', next:'@popall'}, '@default': 'variable' }} ], - [/\\/, { cases: { '@eos': '', '@default': '' }}], - [/./, { cases: { '@eos': {token:'', next:'@popall'}, '@default': '' } }], - ], - - // Deal with white space, including comments - whitespace: [ - [/\s+/, { cases: { '@eos': {token:'', next:'@popall'}, '@default': '' }}], - ], - - comment: [ - [/(^#.*$)/, 'comment', '@popall'] - ], - - // Recognize strings, including those broken across lines with \ (but not without) - strings: [ - [/'$/, 'string', '@popall'], - [/'/, 'string', '@stringBody'], - [/"$/, 'string', '@popall'], - [/"/, 'string', '@dblStringBody'] - ], - stringBody: [ - [/[^\\\$']/, { cases: { '@eos': {token:'string', next:'@popall'}, '@default': 'string' }}], - - [/\\./, 'string.escape'], - [/'$/, 'string', '@popall'], - [/'/, 'string', '@pop'], - [/(@variable)/, 'variable' ], - - [/\\$/, 'string'], - [/$/, 'string', '@popall'] - ], - dblStringBody: [ - [/[^\\\$"]/, { cases: { '@eos': {token:'string', next:'@popall'}, '@default': 'string' }}], - - [/\\./, 'string.escape'], - [/"$/, 'string', '@popall'], - [/"/, 'string', '@pop'], - [/(@variable)/, 'variable' ], - - [/\\$/, 'string'], - [/$/, 'string', '@popall'] - ] - } -}; \ No newline at end of file diff --git a/dockerfile/dockerfileExtraInfo.ts b/dockerfile/dockerfileExtraInfo.ts deleted file mode 100644 index ec19255300..0000000000 --- a/dockerfile/dockerfileExtraInfo.ts +++ /dev/null @@ -1,191 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import {Range} from 'vscode'; -import htmlHelper = require('../helpers/htmlHelper'); -import parser = require('../parser'); -import hub = require('../dockerHubApi'); -import keyInfo = require('./dockerfileKeyInfo'); - -export class ExtraInfoSupport implements vscode.Modes.IExtraInfoSupport { - _parser: parser.Parser; - - constructor () { - this._parser = new parser.Parser('dockerfile'); - } - - public computeInfo(document: vscode.TextDocument, position: vscode.Position) { - - var lineEndColumn = document.getLineMaxColumn(position.line); - var line = document.getTextOnLine(position.line); - - if (line.length === 0) { - // empty line - return Promise.resolve(null); - } - - var tokens = this._parser.parseLine(line); - return this._computeInfoForLineWithTokens(line, tokens, position); - } - - private _computeInfoForLineWithTokens(line:string, tokens:parser.IToken[], position:vscode.Position): Promise { - var possibleTokens = this._parser.tokensAtColumn(tokens, position.character); - - return Promise.all(possibleTokens.map(tokenIndex => this._computeInfoForToken(line, tokens, tokenIndex))).then((results) => { - return possibleTokens.map((tokenIndex, arrayIndex) => { - return { - startIndex: tokens[tokenIndex].startIndex, - endIndex: tokens[tokenIndex].endIndex, - result: results[arrayIndex] - }; - }); - }).then((results) => { - var r = results.filter(r => !!r.result); - if (r.length === 0) { - return null; - } - return { - range: new vscode.Range(position.line, r[0].startIndex + 1, position.line, r[0].endIndex + 1), - htmlContent: r[0].result - }; - }); - } - - private _computeInfoForToken(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect hovering on a key - if (tokens[tokenIndex].type === parser.TokenType.Key) { - var keyName = this._parser.keyNameFromKeyToken(this._parser.tokenValue(line, tokens[tokenIndex])); - var r = ExtraInfoSupport.getInfoForKey(keyName); - if (r) { - return Promise.resolve(r); - } - } - - // ------------- - // Detect <> - // Detect <> - var r2 = this._getImageNameHover(line, tokens, tokenIndex); - if (r2) { - return r2; - } - - return null; - } - - private _getImageNameHover(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect <> - // Detect <> - var originalValue = this._parser.tokenValue(line, tokens[tokenIndex]); - - var keyToken:string = null; - tokenIndex--; - // = is for Dockerfile - while (tokenIndex >= 0) { - var type = tokens[tokenIndex].type; - if (type === parser.TokenType.String || type === parser.TokenType.Text) { - return null; - } - if (type === parser.TokenType.Key) { - keyToken = this._parser.tokenValue(line, tokens[tokenIndex]); - break; - } - tokenIndex--; - } - - if (!keyToken) { - return null; - } - var keyName = this._parser.keyNameFromKeyToken(keyToken); - if (keyName === 'FROM') { - var imageName = originalValue.replace(/^"/, '').replace(/"$/, ''); - return Promise.all([searchImageInRegistryHub(imageName)]).then((results) => { - if (results[0] && results[1]) { - return [{ - tagName: 'strong', - text: 'DockerHub:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[0] - }, { - tagName: 'strong', - text: 'DockerRuntime:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[1] - }] - } - if (results[0]) { - return results[0]; - } - return results[1]; - }); - } - } - - private static getInfoForKey(keyName:string): vscode.IHTMLContentElement[] { - if (ExtraInfoSupport._KEY_INFO === null) { - ExtraInfoSupport._KEY_INFO = {}; - Object.keys(keyInfo.KEY_INFO).forEach((keyName) => { - ExtraInfoSupport._KEY_INFO[keyName] = htmlHelper.simpleMarkDownToHTMLContent(keyInfo.KEY_INFO[keyName]); - }); - } - return ExtraInfoSupport._KEY_INFO[keyName] || null; - } - private static _KEY_INFO: { [keyName:string]: vscode.IHTMLContentElement[]; } = null; -} - -function searchImageInRegistryHub(imageName:string): Promise { - return hub.searchImageInRegistryHub(imageName, true).then((result) => { - if (result) { - var r: vscode.IHTMLContentElement[] = []; - - // Name - r.push({ - tagName: 'strong', - className: 'token keyword', - text: result.name - }); - - var tags = hub.tagsForImage(result); - if (tags.length > 0) { - r.push({ - tagName: 'strong', - text: ' ' + tags + ' ' - }); - } - - if (result.star_count) { - var plural = (result.star_count > 1); - r.push({ - tagName: 'strong', - text: String(result.star_count) - }) - r.push({ - tagName: 'span', - text: (plural ? ' stars' : ' star') - }); - } - - // Description - r.push({ - tagName: 'br' - }); - r.push({ - tagName: 'span', - text: result.description - }); - - return r; - } - }) -} \ No newline at end of file diff --git a/dockerfile/dockerfileKeyInfo.ts b/dockerfile/dockerfileKeyInfo.ts index 4402d028e7..e011376fbe 100644 --- a/dockerfile/dockerfileKeyInfo.ts +++ b/dockerfile/dockerfileKeyInfo.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ // https://docs.docker.com/reference/builder/ -export var KEY_INFO:{[keyName:string]:string;} = { +export var DOCKER_KEY_INFO:{[keyName:string]:string;} = { 'FROM': ( "Sets the *Base Image* for subsequent instructions." ), @@ -49,5 +49,8 @@ export var KEY_INFO:{[keyName:string]:string;} = { 'ONBUILD': ( "Adds to the image a trigger instruction to be executed at a later time, when the image is used as the " + "base for another build." + ), + 'STOPSIGNAL': ( + "Sets the system call signal that will be sent to the container to exit." ) } \ No newline at end of file diff --git a/dockerfile/dockerfileParser.ts b/dockerfile/dockerfileParser.ts new file mode 100644 index 0000000000..171b96649f --- /dev/null +++ b/dockerfile/dockerfileParser.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import {TextLine} from 'vscode'; +import {Parser, TokenType, IToken} from '../parser'; + +export class DockerfileParser extends Parser { + constructor() { + var parseRegex = /\ +$/g; + super(parseRegex, undefined); + } + + parseLine(textLine: TextLine): IToken[] { + if (textLine.isEmptyOrWhitespace) { + return null; + } + + var startIndex = textLine.firstNonWhitespaceCharacterIndex; + + // Check for comment + if (textLine.text.charAt(startIndex) === '#') { + return null; + } + + var tokens: IToken[] = []; + var previousTokenIndex = 0; + var keyFound: boolean = false; + + for (var j = startIndex, len = textLine.text.length; j < len; j++) { + var ch = textLine.text.charAt(j); + + if (ch === ' ' || ch === '\t') { + previousTokenIndex = j; + tokens.push({ + startIndex: 0, + endIndex: j, + type: TokenType.Key + }); + break; + } + + tokens.push({ + startIndex: previousTokenIndex, + endIndex: textLine.text.length, + type: TokenType.String + }); + return tokens; + } + } +} \ No newline at end of file diff --git a/dockerfile/dockerfileSuggestSupport.ts b/dockerfile/dockerfileSuggestSupport.ts deleted file mode 100644 index ecbac25d95..0000000000 --- a/dockerfile/dockerfileSuggestSupport.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import helper = require('../helpers/suggestSupportHelper'); - -// IntelliSense -export class SuggestSupport implements vscode.Modes.ISuggestSupport { - - public triggerCharacters:string[] = []; - public excludeTokens:string[] = []; - - public suggest(document:vscode.TextDocument, position:vscode.Position): Promise { - var dockerSuggestSupport = new helper.SuggestSupportHelper(); - - // Get the line where intellisense was invoked on (e.g. 'FROM '). - var line = document.getTextOnLine(position.line); - - var textBefore = line.substring(0, position.character); - - // Matches strings like: 'FROM imagename' - var fromTextDocker = textBefore.match(/^\s*FROM\s*([^"]*)$/); - - if (fromTextDocker) { - var imageText = fromTextDocker[1]; - // Doesn't have a leading quote - return dockerSuggestSupport.suggestImages(imageText, false); - } - - return Promise.resolve([]); - } -} \ No newline at end of file diff --git a/helpers/htmlHelper.ts b/helpers/htmlHelper.ts deleted file mode 100644 index 71b514adca..0000000000 --- a/helpers/htmlHelper.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); - -enum TagType { - pre, - bold, - normal -} - -export function simpleMarkDownToHTMLContent(source:string): vscode.IHTMLContentElement[] { - var r:vscode.IHTMLContentElement[] = []; - - var lastPushedTo:number; - var push = (to:number, type:TagType) => { - if (lastPushedTo >= to) { - return; - } - var text = source.substring(lastPushedTo, to); - - if (type === TagType.pre) { - r.push({ - tagName: "span", - style: "font-family:monospace", - className: "token keyword", - text: text - }); - } else if (type === TagType.bold) { - r.push({ - tagName: "strong", - text: text - }); - } else if (type === TagType.normal) { - r.push({ - tagName: "span", - text: text - }); - } - lastPushedTo = to; - } - - var currentTagType = () => { - if (inPre) { - return TagType.pre; - } - if (inBold) { - return TagType.bold; - } - return TagType.normal; - } - - var inPre = false, inBold = false; - for (var i = 0, len = source.length; i < len; i++) { - var ch = source.charAt(i); - - if (ch === '\n') { - push(i, currentTagType()); - r.push({ - tagName: 'br' - }); - lastPushedTo = i + 1; - } else if (ch === '`') { - push(i, currentTagType()); - lastPushedTo = i + 1; - inPre = !inPre; - } else if (ch === '*') { - push(i, currentTagType()); - lastPushedTo = i + 1; - inBold = !inBold; - } - } - push(source.length, currentTagType()); - - return r; -} \ No newline at end of file diff --git a/helpers/suggestSupportHelper.ts b/helpers/suggestSupportHelper.ts index fd78d19042..70f4f92e85 100644 --- a/helpers/suggestSupportHelper.ts +++ b/helpers/suggestSupportHelper.ts @@ -6,33 +6,101 @@ import vscode = require('vscode'); import hub = require('../dockerHubApi'); +import parser = require('../parser'); -export class SuggestSupportHelper { - suggestImages(word:string, hasLeadingQuote:boolean): Promise { - return this.suggestHubImages(word).then((results) => { - return [{ - incomplete: true, - currentWord: (hasLeadingQuote ? '"' + word : word), - suggestions: results - }] - }); - } - - suggestHubImages(word:string): Promise { +export class SuggestSupportHelper { + suggestImages(word: string): Promise { return hub.searchImagesInRegistryHub(word, true).then((results) => { return results.map((image) => { var stars = ''; if (image.star_count > 0) { stars = ' ' + image.star_count + ' ' + (image.star_count > 1 ? 'stars' : 'star'); } + return { label: image.name, - codeSnippet: image.name, - type: 'value', - documentationLabel: image.description, - typeLabel: hub.tagsForImage(image) + stars + kind: vscode.CompletionItemKind.Value, + detail: hub.tagsForImage(image) + stars, + insertText: image.name, + documentation: image.description, } }); }); } + + searchImageInRegistryHub(imageName: string): Promise { + return hub.searchImageInRegistryHub(imageName, true).then((result) => { + if (result) { + var r: vscode.MarkedString[] = []; + var tags = hub.tagsForImage(result); + + // Name, tags and stars. + var nameString = ''; + if (tags.length > 0) { + nameString = '**' + result.name + ' ' + tags + '** '; + } else { + nameString = '**' + result.name + '**'; + } + + if (result.star_count) { + var plural = (result.star_count > 1); + nameString += '**' + String(result.star_count) + (plural ? ' stars' : ' star') + '**'; + } + + r.push(nameString); + + // Description + r.push(result.description); + + return r; + } + }) + } + + getImageNameHover(line: string, _parser: parser.Parser, tokens: parser.IToken[], tokenIndex: number): Promise { + // ------------- + // Detect <> + // Detect <> + var originalValue = _parser.tokenValue(line, tokens[tokenIndex]); + + var keyToken: string = null; + tokenIndex--; + while (tokenIndex >= 0) { + var type = tokens[tokenIndex].type; + if (type === parser.TokenType.String || type === parser.TokenType.Text) { + return null; + } + if (type === parser.TokenType.Key) { + keyToken = _parser.tokenValue(line, tokens[tokenIndex]); + break; + } + tokenIndex--; + } + + if (!keyToken) { + return null; + } + var keyName = _parser.keyNameFromKeyToken(keyToken); + if (keyName === 'image' || keyName === 'FROM') { + + var imageName = originalValue.replace(/^"/, '').replace(/"$/, ''); + return Promise.all([this.searchImageInRegistryHub(imageName)]).then((results) => { + if (results[0] && results[1]) { + return [{ + value: '**DockerHub:**' + }, { + value: results[0] + }, { + value: '**DockerRuntime**' + }, { + value: results[1] + }] + } + if (results[0]) { + return results[0]; + } + return results[1]; + }); + } + } } \ No newline at end of file diff --git a/package.json b/package.json index eba3175871..bf107c3f1b 100644 --- a/package.json +++ b/package.json @@ -4,38 +4,57 @@ "publisher": "DockerTools", "description": "Dockerfile and Docker yaml file support for Visual Studio Code", "engines": { - "vscode": "*" + "vscode": "0.10.x" }, "private": true, "repository": { "type": "git", "url": "https://github.com/Microsoft/vscode-docker.git" }, - "activationEvents": ["onLanguage:dockerfile", "onLanguage:yaml"], + "activationEvents": [ + "onLanguage:dockerfile", + "onLanguage:yaml" + ], "main": "./out/dockerExtension", "contributes": { - "languages": [{ - "id": "dockerfile", - "extensions": [ ".dockerfile" ], - "filenames": [ "Dockerfile" ], - "aliases": [ "Dockerfile" ] - }, - { - "id": "yaml", - "aliases": ["YAML", "yaml"], - "extensions": [".yml"], - "filenames": ["Docker-compose.yml"] - }] + "languages": [ + { + "id": "dockerfile", + "extensions": [ + ".dockerfile" + ], + "filenames": [ + "Dockerfile", + "dockerfile" + ], + "aliases": [ + "Dockerfile" + ] + }, + { + "id": "yaml", + "aliases": [ + "YAML", + "yaml" + ], + "extensions": [ + ".yml" + ], + "filenames": [ + "Docker-compose.yml" + ] + } + ] }, "scripts": { "vscode:prepublish": "tsc", - "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" + "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" }, "extensionDependencies": [ "vscode.docker", "vscode.yaml" ], "devDependencies": { - "vscode": "*" + "vscode": "0.10.x" } -} +} \ No newline at end of file diff --git a/parser.ts b/parser.ts index 1cc874f078..c0f4fcbda2 100644 --- a/parser.ts +++ b/parser.ts @@ -4,42 +4,43 @@ 'use strict'; -import vscode = require('vscode'); +import {TextLine} from 'vscode'; -export class Parser { - _modeId: string; - _keySeparator: string; - _tokenParseRegex: RegExp; - - constructor(modeId: string) { - this._modeId = modeId; - - // Set the parser settings, depending on the mode. - if (this._modeId === "dockerfile") { - this._keySeparator = ' '; - this._tokenParseRegex = /\ +$/g; - } else if (this._modeId === 'yaml') { - this._keySeparator = ':'; - this._tokenParseRegex = /\:+$/g; +export abstract class Parser { + _tokenParseRegex: RegExp; + _fileNameRegex: RegExp; + + constructor(parseTokenRegex: RegExp, fileNameRegex: RegExp) { + this._tokenParseRegex = parseTokenRegex; + this._fileNameRegex = fileNameRegex; + } + + isFileSupported(fileName: string): boolean { + if (this._fileNameRegex !== undefined) { + if (this._fileNameRegex.test(fileName)) { + return false; + } } - } - - keyNameFromKeyToken(keyToken:string): string { - return keyToken.replace(this._tokenParseRegex, ''); + + return true; } - tokenValue(line:string, token:IToken): string { + keyNameFromKeyToken(keyToken: string): string { + return keyToken.replace(this._tokenParseRegex, ''); + } + + tokenValue(line: string, token: IToken): string { return line.substring(token.startIndex, token.endIndex); } - tokensAtColumn(tokens:IToken[], charIndex:number): number[] { + tokensAtColumn(tokens: IToken[], charIndex: number): number[] { for (var i = 0, len = tokens.length; i < len; i++) { var token = tokens[i]; - + if (token.endIndex < charIndex) { continue; } - + if (token.endIndex === charIndex && i + 1 < len) { return [i, i + 1] } @@ -49,76 +50,8 @@ export class Parser { // should not happen: no token found? => return the last one return [tokens.length - 1]; } - - /** - ** A super simplified parser only looking out for strings and comments - **/ - parseLine(line:string): IToken[] { - var r: IToken[] = []; - var lastTokenEndIndex = 0, lastPushedToken:IToken = null; - - var emit = (end:number, type:TokenType) => { - if (end <= lastTokenEndIndex) { - return; - } - - if (lastPushedToken && lastPushedToken.type === type) { - // merge with last pushed token - lastPushedToken.endIndex = end; - lastTokenEndIndex = end; - return; - } - - lastPushedToken = { - startIndex: lastTokenEndIndex, - endIndex: end, - type: type - }; - - r.push(lastPushedToken); - lastTokenEndIndex = end; - }; - - var inString = false; - - for (var i = 0, len = line.length; i < len; i++) { - var ch = line.charAt(i); - - if (inString) { - if (ch === '"' && line.charAt(i-1) !== '\\') { - inString = false; - emit(i + 1, TokenType.String); - } - - continue; - } - - if (ch === '"') { - emit(i, TokenType.Text); - inString = true; - continue; - } - - if (ch === '#') { - // Comment the rest of the line - emit(i, TokenType.Text); - emit(line.length, TokenType.Comment); - break; - } - - if (ch === this._keySeparator) { - emit(i + 1, TokenType.Key); - } - - if (ch === ' ' || ch === '\t') { - emit(i, TokenType.Text); - emit(i + 1, TokenType.Whitespace); - } - } - - emit(line.length, TokenType.Text); - return r; - } + + abstract parseLine(textLine: TextLine): IToken[]; } export enum TokenType { diff --git a/tsconfig.json b/tsconfig.json index 2e121bf4e7..99b0719a8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "module": "commonjs", "outDir": "out", "noLib": true, - "sourceMap": true + "sourceMap": true, + "target": "es5" }, "exclude": [ "node_modules" diff --git a/yaml/yamlCompletionItemProvider.ts b/yaml/yamlCompletionItemProvider.ts new file mode 100644 index 0000000000..24e3a0dfa8 --- /dev/null +++ b/yaml/yamlCompletionItemProvider.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import {TextDocument, Position, CancellationToken, CompletionItem, CompletionItemProvider, CompletionItemKind, Uri} from 'vscode'; +import helper = require('../helpers/suggestSupportHelper'); +import {YAML_KEY_INFO} from './yamlKeyInfo'; +import hub = require('../dockerHubApi'); + +function isDockerCompose(resource: string): boolean { + return /docker\-compose\.yml$/.test(resource); +} + +export class YamlCompletionItemProvider implements CompletionItemProvider { + + public triggerCharacters: string[] = []; + public excludeTokens: string[] = []; + + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { + if (!isDockerCompose(document.fileName)) { + return Promise.resolve(null); + } + + var yamlSuggestSupport = new helper.SuggestSupportHelper(); + + // Get the line where intellisense was invoked on (e.g. 'image: u'). + var line = document.lineAt(position.line).text; + + if (line.length === 0) { + // empty line + return Promise.resolve(this.suggestKeys('')); + } + + let range = document.getWordRangeAtPosition(position); + + // Get the text where intellisense was invoked on (e.g. 'u'). + let word = range && document.getText(range) || ''; + + var textBefore = line.substring(0, position.character); + if (/^\s*[\w_]*$/.test(textBefore)) { + // on the first token + return Promise.resolve(this.suggestKeys(word)); + } + + // Matches strings like: 'image: "ubuntu' + var imageTextWithQuoteMatchYaml = textBefore.match(/^\s*image\s*\:\s*"([^"]*)$/); + + if (imageTextWithQuoteMatchYaml) { + var imageText = imageTextWithQuoteMatchYaml[1]; + return yamlSuggestSupport.suggestImages(imageText); + } + + // Matches strings like: 'image: ubuntu' + var imageTextWithoutQuoteMatch = textBefore.match(/^\s*image\s*\:\s*([\w\:\/]*)/); + + if (imageTextWithoutQuoteMatch) { + var imageText = imageTextWithoutQuoteMatch[1]; + return yamlSuggestSupport.suggestImages(imageText); + } + + return Promise.resolve([]); + } + + private suggestKeys(word: string): CompletionItem[] { + return Object.keys(YAML_KEY_INFO).map(ruleName => { + var completionItem = new CompletionItem(ruleName); + completionItem.kind = CompletionItemKind.Keyword; + completionItem.insertText = ruleName + ': '; + completionItem.documentation = YAML_KEY_INFO[ruleName]; + return completionItem; + }); + } +} \ No newline at end of file diff --git a/yaml/yamlExtraInfo.ts b/yaml/yamlExtraInfo.ts deleted file mode 100644 index 7bff50d682..0000000000 --- a/yaml/yamlExtraInfo.ts +++ /dev/null @@ -1,197 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import parser = require('../parser'); -import keyInfo = require('./yamlKeyInfo') -import hub = require('../dockerHubApi'); -import htmlHelper = require('../helpers/htmlHelper'); - -function isDockerCompose(resource:vscode.Uri): boolean { - return /docker\-compose\.yml$/.test(resource.toString()); -} - -export class ExtraInfoSupport implements vscode.Modes.IExtraInfoSupport { - _parser: parser.Parser; - - constructor () { - this._parser = new parser.Parser('yaml'); - } - - public computeInfo(document: vscode.TextDocument, position: vscode.Position) { - if (!isDockerCompose(document.getUri())) { - return Promise.resolve(null); - } - - var lineEndColumn = document.getLineMaxColumn(position.line); - var line = document.getTextOnLine(position.line); - - if (line.length === 0) { - // empty line - return Promise.resolve(null); - } - - var tokens = this._parser.parseLine(line); - return this._computeInfoForLineWithTokens(line, tokens, position); - } - - private _computeInfoForLineWithTokens(line:string, tokens:parser.IToken[], position:vscode.Position): Promise { - var possibleTokens = this._parser.tokensAtColumn(tokens, position.character); - - return Promise.all(possibleTokens.map(tokenIndex => this._computeInfoForToken(line, tokens, tokenIndex))).then((results) => { - return possibleTokens.map((tokenIndex, arrayIndex) => { - return { - startIndex: tokens[tokenIndex].startIndex, - endIndex: tokens[tokenIndex].endIndex, - result: results[arrayIndex] - }; - }); - }).then((results) => { - var r = results.filter(r => !!r.result); - if (r.length === 0) { - return null; - } - return { - range: new vscode.Range(position.line, r[0].startIndex + 1, position.line, r[0].endIndex + 1), - htmlContent: r[0].result - }; - }); - } - - private _computeInfoForToken(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect hovering on a key - if (tokens[tokenIndex].type === parser.TokenType.Key) { - var keyName = this._parser.keyNameFromKeyToken(this._parser.tokenValue(line, tokens[tokenIndex])); - var r = ExtraInfoSupport.getInfoForKey(keyName); - if (r) { - return Promise.resolve(r); - } - } - - // ------------- - // Detect <> - // Detect <> - var r2 = this._getImageNameHover(line, tokens, tokenIndex); - if (r2) { - return r2; - } - - return null; - } - - // hovering over image name. - private _getImageNameHover(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect <> - // Detect <> - var originalValue = this._parser.tokenValue(line, tokens[tokenIndex]); - - var keyToken:string = null; - tokenIndex--; - while (tokenIndex > 0) { - var type = tokens[tokenIndex].type; - if (type === parser.TokenType.String || type === parser.TokenType.Text) { - return null; - } - if (type === parser.TokenType.Key) { - keyToken = this._parser.tokenValue(line, tokens[tokenIndex]); - break; - } - tokenIndex--; - } - - if (!keyToken) { - return null; - } - var keyName = this._parser.keyNameFromKeyToken(keyToken); - if (keyName === 'image') { - var imageName = originalValue.replace(/^"/, '').replace(/"$/, ''); - return Promise.all([searchImageInRegistryHub(imageName)]).then((results) => { - if (results[0] && results[1]) { - return [{ - tagName: 'strong', - text: 'DockerHub:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[0] - }, { - tagName: 'strong', - text: 'DockerRuntime:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[1] - }] - } - if (results[0]) { - return results[0]; - } - return results[1]; - }); - } - } - - private static getInfoForKey(keyName:string): vscode.IHTMLContentElement[] { - if (ExtraInfoSupport._KEY_INFO === null) { - ExtraInfoSupport._KEY_INFO = {}; - Object.keys(keyInfo.KEY_INFO).forEach((keyName) => { - ExtraInfoSupport._KEY_INFO[keyName] = htmlHelper.simpleMarkDownToHTMLContent(keyInfo.KEY_INFO[keyName]); - }); - } - return ExtraInfoSupport._KEY_INFO[keyName] || null; - } - private static _KEY_INFO: { [keyName:string]: vscode.IHTMLContentElement[]; } = null; -} - -function searchImageInRegistryHub(imageName:string): Promise { - return hub.searchImageInRegistryHub(imageName, true).then((result) => { - if (result) { - var r: vscode.IHTMLContentElement[] = []; - - // Name - r.push({ - tagName: 'strong', - className: 'token keyword', - text: result.name - }); - - var tags = hub.tagsForImage(result); - if (tags.length > 0) { - r.push({ - tagName: 'strong', - text: ' ' + tags + ' ' - }); - } - - if (result.star_count) { - var plural = (result.star_count > 1); - r.push({ - tagName: 'strong', - text: String(result.star_count) - }) - r.push({ - tagName: 'span', - text: (plural ? ' stars' : ' star') - }); - } - - // Description - r.push({ - tagName: 'br' - }); - r.push({ - tagName: 'span', - text: result.description - }); - - return r; - } - }) -} \ No newline at end of file diff --git a/yaml/yamlKeyInfo.ts b/yaml/yamlKeyInfo.ts index 16dd074f0e..5212ce3446 100644 --- a/yaml/yamlKeyInfo.ts +++ b/yaml/yamlKeyInfo.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ // https://docs.docker.com/compose/yml/ -export var KEY_INFO:{[keyName:string]:string;} = { +export var YAML_KEY_INFO: { [keyName: string]: string; } = { 'image': ( "Tag or partial image ID. Can be local or remote - Compose will attempt to pull if it doesn't exist locally." ), @@ -68,4 +68,39 @@ export var KEY_INFO:{[keyName:string]:string;} = { 'dns_search': ( "Custom DNS search domains. Can be a single value or a list." ), + 'cgroup_parent': ( + "Specify an optional parent cgroup for the container." + ), + 'container_name': ( + "Specify custom container name, rather than a generated default name." + ), + 'devices': ( + "List of device mappings. Uses the same format as the `--device` docker client create option." + ), + 'dockerfile': ( + "Alternate dockerfile. Compose will use an alternate file to build with. Using `dockerfile` together with `image` is not allowed. Attempting to do so results in an error." + ), + 'extends': ( + "Extend another service, in the current file or another, optionally overriding configuration.\nYou can use `extends` on any service together with other configuration keys. " + + "The `extends` value must be a dictionary defined with a required `service` and an optional `file` key." + ), + 'extra_hosts': ( + "Add hostname mappings. Use the same values as the docker client `--add-host` parameter." + ), + 'labels': ( + "Add metadata to containers using Docker labels. You can either use an array or a dictionary.\n" + + "It's recommended that you use reverse-DNS notation to prevent your labels from conflicting with those used by other software." + ), + 'log_driver': ( + "Specify a logging driver for the service's containers, as with the `--log-driver` option for docker run. The default value is json-file." + ), + 'log_opt': ( + "Specify logging options with `log_opt` for the logging driver, as with the `--log-opt` option for docker run." + ), + 'security_opt': ( + "Override the default labeling scheme for each container." + ), + 'volume_driver': ( + "If you use a volume name (instead of a volume path), you may also specify a `volume_driver`." + ) } \ No newline at end of file diff --git a/yaml/yamlParser.ts b/yaml/yamlParser.ts new file mode 100644 index 0000000000..6599944323 --- /dev/null +++ b/yaml/yamlParser.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import vscode = require('vscode'); +import {Parser, TokenType, IToken} from '../parser'; + +export class YamlParser extends Parser { + constructor() { + var parseRegex = /\:+$/g; + super(parseRegex, new RegExp("/docker\-compose\.yml$/")); + } + + parseLine(textLine: vscode.TextLine): IToken[] { + var r: IToken[] = []; + var lastTokenEndIndex = 0, lastPushedToken: IToken = null; + + var emit = (end: number, type: TokenType) => { + if (end <= lastTokenEndIndex) { + return; + } + + if (lastPushedToken && lastPushedToken.type === type) { + // merge with last pushed token + lastPushedToken.endIndex = end; + lastTokenEndIndex = end; + return; + } + + lastPushedToken = { + startIndex: lastTokenEndIndex, + endIndex: end, + type: type + }; + + r.push(lastPushedToken); + lastTokenEndIndex = end; + }; + + var inString = false; + + var idx = textLine.firstNonWhitespaceCharacterIndex; + var line = textLine.text; + + for (var i = idx, len = line.length; i < len; i++) { + var ch = line.charAt(i); + + if (inString) { + if (ch === '"' && line.charAt(i - 1) !== '\\') { + inString = false; + emit(i + 1, TokenType.String); + } + + continue; + } + + if (ch === '"') { + emit(i, TokenType.Text); + inString = true; + continue; + } + + if (ch === '#') { + // Comment the rest of the line + emit(i, TokenType.Text); + emit(line.length, TokenType.Comment); + break; + } + + if (ch === ':') { + emit(i + 1, TokenType.Key); + } + + if (ch === ' ' || ch === '\t') { + emit(i, TokenType.Text); + emit(i + 1, TokenType.Whitespace); + } + } + + emit(line.length, TokenType.Text); + return r; + } +} \ No newline at end of file diff --git a/yaml/yamlSuggestSupport.ts b/yaml/yamlSuggestSupport.ts deleted file mode 100644 index 5d4af6dd0e..0000000000 --- a/yaml/yamlSuggestSupport.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import helper = require('../helpers/suggestSupportHelper'); -import keyInfo = require('./yamlKeyInfo'); -import hub = require('../dockerHubApi'); - -function isDockerCompose(resource:vscode.Uri): boolean { - return /docker\-compose\.yml$/.test(resource.toString()); -} - -// IntelliSense -export class SuggestSupport implements vscode.Modes.ISuggestSupport { - - public triggerCharacters:string[] = []; - public excludeTokens:string[] = []; - - public suggest(document:vscode.TextDocument, position:vscode.Position): Promise { - if (!isDockerCompose(document.getUri())) { - return Promise.resolve(null); - } - - var yamlSuggestSupport = new helper.SuggestSupportHelper(); - - // Get the line where intellisense was invoked on (e.g. 'image: u'). - var line = document.getTextOnLine(position.line); - - if (line.length === 0) { - // empty line - return Promise.resolve([this.suggestKeys('')]); - } - - let range = document.getWordRangeAtPosition(position); - - // Get the text where intellisense was invoked on (e.g. 'u'). - let word = range && document.getTextInRange(range) || ''; - - var textBefore = line.substring(0, position.character); - if (/^\s*[\w_]*$/.test(textBefore)) { - // on the first token - return Promise.resolve([this.suggestKeys(word)]); - } - - // Matches strings like: 'image: "ubuntu' - var imageTextWithQuoteMatchYaml = textBefore.match(/^\s*image\s*\:\s*"([^"]*)$/); - - if (imageTextWithQuoteMatchYaml) { - var imageText = imageTextWithQuoteMatchYaml[1]; - return yamlSuggestSupport.suggestImages(imageText, true); - } - - // Matches strings like: 'image: ubuntu' - var imageTextWithoutQuoteMatch = textBefore.match(/^\s*image\s*\:\s*([\w\:\/]*)/); - - if (imageTextWithoutQuoteMatch) { - var imageText = imageTextWithoutQuoteMatch[1]; - return yamlSuggestSupport.suggestImages(imageText, false); - } - - return Promise.resolve([]); - } - - private suggestKeys(word:string): vscode.Modes.ISuggestions { - return { - currentWord: word, - suggestions: Object.keys(keyInfo.KEY_INFO).map((ruleName) => { - return { - label: ruleName, - codeSnippet: ruleName + ': ', - type: 'property', - documentationLabel: keyInfo.KEY_INFO[ruleName] - } - }) - }; - } -} \ No newline at end of file From 30613b5a982154f689acf15ca769460ba9a0ec92 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Fri, 6 Nov 2015 10:31:24 -0800 Subject: [PATCH 2/2] Updating the extension to use the new API. --- .../dockerComposeCompletionItemProvider.ts | 67 ++++++ dockerCompose/dockerComposeKeyInfo.ts | 106 ++++++++++ dockerCompose/dockerComposeParser.ts | 84 ++++++++ dockerExtension.ts | 30 +-- dockerHoverProvider.ts | 82 ++++++++ dockerHubApi.ts | 189 ++++++++--------- .../dockerfileCompletionItemProvider.ts | 30 +++ dockerfile/dockerfileDef.ts | 84 -------- dockerfile/dockerfileExtraInfo.ts | 191 ----------------- dockerfile/dockerfileKeyInfo.ts | 97 ++++----- dockerfile/dockerfileParser.ts | 53 +++++ dockerfile/dockerfileSuggestSupport.ts | 35 ---- helpers/htmlHelper.ts | 79 ------- helpers/suggestSupportHelper.ts | 124 ++++++++--- package.json | 78 ++++--- parser.ts | 171 +++++---------- snippets/dockerfile.json | 114 +++++----- tsconfig.json | 3 +- yaml/yamlExtraInfo.ts | 197 ------------------ yaml/yamlKeyInfo.ts | 71 ------- yaml/yamlSuggestSupport.ts | 80 ------- 21 files changed, 823 insertions(+), 1142 deletions(-) create mode 100644 dockerCompose/dockerComposeCompletionItemProvider.ts create mode 100644 dockerCompose/dockerComposeKeyInfo.ts create mode 100644 dockerCompose/dockerComposeParser.ts create mode 100644 dockerHoverProvider.ts create mode 100644 dockerfile/dockerfileCompletionItemProvider.ts delete mode 100644 dockerfile/dockerfileDef.ts delete mode 100644 dockerfile/dockerfileExtraInfo.ts create mode 100644 dockerfile/dockerfileParser.ts delete mode 100644 dockerfile/dockerfileSuggestSupport.ts delete mode 100644 helpers/htmlHelper.ts delete mode 100644 yaml/yamlExtraInfo.ts delete mode 100644 yaml/yamlKeyInfo.ts delete mode 100644 yaml/yamlSuggestSupport.ts diff --git a/dockerCompose/dockerComposeCompletionItemProvider.ts b/dockerCompose/dockerComposeCompletionItemProvider.ts new file mode 100644 index 0000000000..2fa6f000ee --- /dev/null +++ b/dockerCompose/dockerComposeCompletionItemProvider.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import {TextDocument, Position, CancellationToken, CompletionItem, CompletionItemProvider, CompletionItemKind, Uri} from 'vscode'; +import helper = require('../helpers/suggestSupportHelper'); +import {DOCKER_COMPOSE_KEY_INFO} from './dockerComposeKeyInfo'; +import hub = require('../dockerHubApi'); + +export class DockerComposeCompletionItemProvider implements CompletionItemProvider { + + public triggerCharacters: string[] = []; + public excludeTokens: string[] = []; + + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { + var yamlSuggestSupport = new helper.SuggestSupportHelper(); + + // Get the line where intellisense was invoked on (e.g. 'image: u'). + var line = document.lineAt(position.line).text; + + if (line.length === 0) { + // empty line + return Promise.resolve(this.suggestKeys('')); + } + + let range = document.getWordRangeAtPosition(position); + + // Get the text where intellisense was invoked on (e.g. 'u'). + let word = range && document.getText(range) || ''; + + var textBefore = line.substring(0, position.character); + if (/^\s*[\w_]*$/.test(textBefore)) { + // on the first token + return Promise.resolve(this.suggestKeys(word)); + } + + // Matches strings like: 'image: "ubuntu' + var imageTextWithQuoteMatchYaml = textBefore.match(/^\s*image\s*\:\s*"([^"]*)$/); + + if (imageTextWithQuoteMatchYaml) { + var imageText = imageTextWithQuoteMatchYaml[1]; + return yamlSuggestSupport.suggestImages(imageText); + } + + // Matches strings like: 'image: ubuntu' + var imageTextWithoutQuoteMatch = textBefore.match(/^\s*image\s*\:\s*([\w\:\/]*)/); + + if (imageTextWithoutQuoteMatch) { + var imageText = imageTextWithoutQuoteMatch[1]; + return yamlSuggestSupport.suggestImages(imageText); + } + + return Promise.resolve([]); + } + + private suggestKeys(word: string): CompletionItem[] { + return Object.keys(DOCKER_COMPOSE_KEY_INFO).map(ruleName => { + var completionItem = new CompletionItem(ruleName); + completionItem.kind = CompletionItemKind.Keyword; + completionItem.insertText = ruleName + ': '; + completionItem.documentation = DOCKER_COMPOSE_KEY_INFO[ruleName]; + return completionItem; + }); + } +} \ No newline at end of file diff --git a/dockerCompose/dockerComposeKeyInfo.ts b/dockerCompose/dockerComposeKeyInfo.ts new file mode 100644 index 0000000000..73bd94fa3e --- /dev/null +++ b/dockerCompose/dockerComposeKeyInfo.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +// https://docs.docker.com/compose/yml/ +export var DOCKER_COMPOSE_KEY_INFO: { [keyName: string]: string; } = { + 'image': ( + "Tag or partial image ID. Can be local or remote - Compose will attempt to pull if it doesn't exist locally." + ), + 'build': ( + "Path to a directory containing a Dockerfile. When the value supplied is a relative path, it is interpreted as relative to the " + + "location of the yml file itself. This directory is also the build context that is sent to the Docker daemon.\n\n" + + "Compose will build and tag it with a generated name, and use that image thereafter." + ), + 'command': ( + "Override the default command." + ), + 'links': ( + "Link to containers in another service. Either specify both the service name and the link alias (`CONTAINER:ALIAS`), or " + + "just the service name (which will also be used for the alias)." + ), + 'external_links': ( + "Link to containers started outside this `docker-compose.yml` or even outside of Compose, especially for containers that " + + "provide shared or common services. `external_links` follow " + + "semantics similar to `links` when specifying both the container name and the link alias (`CONTAINER:ALIAS`)." + ), + 'ports': ( + "Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen).\n\n" + + "**Note**: When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port " + + "lower than 60, because YAML will parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, we recommend " + + "always explicitly specifying your port mappings as strings." + ), + 'expose': ( + "Expose ports without publishing them to the host machine - they'll only be accessible to linked services. \n" + + "Only the internal port can be specified." + ), + 'volumes': ( + "Mount paths as volumes, optionally specifying a path on the host machine (`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`)." + ), + 'volumes_from': ( + "Mount all of the volumes from another service or container." + ), + 'environment': ( + "Add environment variables. You can use either an array or a dictionary.\n\n" + + "Environment variables with only a key are resolved to their values on the machine Compose is running on, which can be helpful for secret or host-specific values." + ), + 'env_file': ( + "Add environment variables from a file. Can be a single value or a list.\n\n" + + "If you have specified a Compose file with `docker-compose -f FILE`, paths in `env_file` are relative to the directory that file is in.\n\n" + + "Environment variables specified in `environment` override these values." + ), + 'net': ( + "Networking mode. Use the same values as the docker client `--net` parameter." + ), + 'pid': ( + "Sets the PID mode to the host PID mode. This turns on sharing between container and the host operating system the PID address space. " + + "Containers launched with this flag will be able to access and manipulate other containers in the bare-metal machine's namespace and vise-versa." + ), + 'dns': ( + "Custom DNS servers. Can be a single value or a list." + ), + 'cap_add': ( + "Add or drop container capabilities. See `man 7 capabilities` for a full list." + ), + 'cap_drop': ( + "Add or drop container capabilities. See `man 7 capabilities` for a full list." + ), + 'dns_search': ( + "Custom DNS search domains. Can be a single value or a list." + ), + 'cgroup_parent': ( + "Specify an optional parent cgroup for the container." + ), + 'container_name': ( + "Specify custom container name, rather than a generated default name." + ), + 'devices': ( + "List of device mappings. Uses the same format as the `--device` docker client create option." + ), + 'dockerfile': ( + "Alternate dockerfile. Compose will use an alternate file to build with. Using `dockerfile` together with `image` is not allowed. Attempting to do so results in an error." + ), + 'extends': ( + "Extend another service, in the current file or another, optionally overriding configuration.\nYou can use `extends` on any service together with other configuration keys. " + + "The `extends` value must be a dictionary defined with a required `service` and an optional `file` key." + ), + 'extra_hosts': ( + "Add hostname mappings. Use the same values as the docker client `--add-host` parameter." + ), + 'labels': ( + "Add metadata to containers using Docker labels. You can either use an array or a dictionary.\n" + + "It's recommended that you use reverse-DNS notation to prevent your labels from conflicting with those used by other software." + ), + 'log_driver': ( + "Specify a logging driver for the service's containers, as with the `--log-driver` option for docker run. The default value is json-file." + ), + 'log_opt': ( + "Specify logging options with `log_opt` for the logging driver, as with the `--log-opt` option for docker run." + ), + 'security_opt': ( + "Override the default labeling scheme for each container." + ), + 'volume_driver': ( + "If you use a volume name (instead of a volume path), you may also specify a `volume_driver`." + ) +} \ No newline at end of file diff --git a/dockerCompose/dockerComposeParser.ts b/dockerCompose/dockerComposeParser.ts new file mode 100644 index 0000000000..0c8d69e8d6 --- /dev/null +++ b/dockerCompose/dockerComposeParser.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import vscode = require('vscode'); +import {Parser, TokenType, IToken} from '../parser'; + +export class DockerComposeParser extends Parser { + constructor() { + var parseRegex = /\:+$/g; + super(parseRegex, new RegExp("/docker\-compose\.yml$/")); + } + + parseLine(textLine: vscode.TextLine): IToken[] { + var r: IToken[] = []; + var lastTokenEndIndex = 0, lastPushedToken: IToken = null; + + var emit = (end: number, type: TokenType) => { + if (end <= lastTokenEndIndex) { + return; + } + + if (lastPushedToken && lastPushedToken.type === type) { + // merge with last pushed token + lastPushedToken.endIndex = end; + lastTokenEndIndex = end; + return; + } + + lastPushedToken = { + startIndex: lastTokenEndIndex, + endIndex: end, + type: type + }; + + r.push(lastPushedToken); + lastTokenEndIndex = end; + }; + + var inString = false; + var idx = textLine.firstNonWhitespaceCharacterIndex; + var line = textLine.text; + + for (var i = idx, len = line.length; i < len; i++) { + var ch = line.charAt(i); + + if (inString) { + if (ch === '"' && line.charAt(i - 1) !== '\\') { + inString = false; + emit(i + 1, TokenType.String); + } + + continue; + } + + if (ch === '"') { + emit(i, TokenType.Text); + inString = true; + continue; + } + + if (ch === '#') { + // Comment the rest of the line + emit(i, TokenType.Text); + emit(line.length, TokenType.Comment); + break; + } + + if (ch === ':') { + emit(i + 1, TokenType.Key); + } + + if (ch === ' ' || ch === '\t') { + emit(i, TokenType.Text); + emit(i + 1, TokenType.Whitespace); + } + } + + emit(line.length, TokenType.Text); + return r; + } +} \ No newline at end of file diff --git a/dockerExtension.ts b/dockerExtension.ts index b8784094d0..5298ee7ed9 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -2,21 +2,23 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import dockerfileDef = require('./dockerfile/dockerfileDef'); -import dockerExtraInfoSupport = require('./dockerfile/dockerfileExtraInfo'); -import dockerSuggestSupport = require('./dockerfile/dockerfileSuggestSupport'); -import yamlExtraInfoSupport = require('./yaml/yamlExtraInfo'); -import yamlSuggestSupport = require('./yaml/yamlSuggestSupport'); +import {DockerHoverProvider} from './dockerHoverProvider'; +import {DockerfileCompletionItemProvider} from './dockerfile/dockerfileCompletionItemProvider'; +import {DockerComposeCompletionItemProvider} from './dockerCompose/dockerComposeCompletionItemProvider'; +import {DOCKERFILE_KEY_INFO} from './dockerfile/dockerfileKeyInfo'; +import {DOCKER_COMPOSE_KEY_INFO} from './dockerCompose/dockerComposeKeyInfo'; +import {DockerComposeParser} from './dockerCompose/dockerComposeParser'; +import {DockerfileParser} from './dockerfile/dockerfileParser'; import vscode = require('vscode'); -export function activate(subscriptions: vscode.Disposable[]) { - - var DOCKERFILE_MODE_ID = 'dockerfile'; - subscriptions.push(vscode.Modes.registerMonarchDefinition(DOCKERFILE_MODE_ID, dockerfileDef.language)); - subscriptions.push(vscode.Modes.ExtraInfoSupport.register(DOCKERFILE_MODE_ID, new dockerExtraInfoSupport.ExtraInfoSupport())); - subscriptions.push(vscode.Modes.SuggestSupport.register(DOCKERFILE_MODE_ID, new dockerSuggestSupport.SuggestSupport())); +export function activate(ctx: vscode.ExtensionContext): void { + const DOCKERFILE_MODE_ID: vscode.DocumentFilter = { language: 'dockerfile', scheme: 'file' }; + var dockerHoverProvider = new DockerHoverProvider(new DockerfileParser(), DOCKERFILE_KEY_INFO); + ctx.subscriptions.push(vscode.languages.registerHoverProvider(DOCKERFILE_MODE_ID, dockerHoverProvider)); + ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(DOCKERFILE_MODE_ID, new DockerfileCompletionItemProvider(), '.')); - var YAML_MODE_ID = 'yaml'; - subscriptions.push(vscode.Modes.ExtraInfoSupport.register(YAML_MODE_ID, new yamlExtraInfoSupport.ExtraInfoSupport())); - subscriptions.push(vscode.Modes.SuggestSupport.register(YAML_MODE_ID, new yamlSuggestSupport.SuggestSupport())); + const YAML_MODE_ID: vscode.DocumentFilter = { language: 'yaml', scheme: 'file', pattern:'**\docker-compose.yml' }; + var yamlHoverProvider = new DockerHoverProvider(new DockerComposeParser(), DOCKER_COMPOSE_KEY_INFO); + ctx.subscriptions.push(vscode.languages.registerHoverProvider(YAML_MODE_ID, yamlHoverProvider)); + ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(YAML_MODE_ID, new DockerComposeCompletionItemProvider(), '.')) } \ No newline at end of file diff --git a/dockerHoverProvider.ts b/dockerHoverProvider.ts new file mode 100644 index 0000000000..a400c15ab4 --- /dev/null +++ b/dockerHoverProvider.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import { Range, TextDocument, Position, CancellationToken, HoverProvider, Hover, MarkedString } from 'vscode'; +import parser = require('./parser'); +import hub = require('./dockerHubApi'); +import suggestHelper = require('./helpers/suggestSupportHelper'); + +export class DockerHoverProvider implements HoverProvider { + _parser: parser.Parser; + _keyInfo: { [keyName: string]: string; }; + + // Provide the parser you want to use as well as keyinfo dictionary. + constructor(wordParser: parser.Parser, keyInfo: { [keyName: string]: string; }) { + this._parser = wordParser; + this._keyInfo = keyInfo; + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { + if (!this._parser.isFileSupported(document.fileName)) { + return Promise.resolve(null); + } + + var line = document.lineAt(position.line); + + if (line.text.length === 0) { + return Promise.resolve(null); + } + + var tokens = this._parser.parseLine(line); + return this._computeInfoForLineWithTokens(line.text, tokens, position); + } + + private _computeInfoForLineWithTokens(line: string, tokens: parser.IToken[], position: Position): Promise { + var possibleTokens = this._parser.tokensAtColumn(tokens, position.character); + + return Promise.all(possibleTokens.map(tokenIndex => this._computeInfoForToken(line, tokens, tokenIndex))).then((results) => { + return possibleTokens.map((tokenIndex, arrayIndex) => { + return { + startIndex: tokens[tokenIndex].startIndex, + endIndex: tokens[tokenIndex].endIndex, + result: results[arrayIndex] + }; + }); + }).then((results) => { + var r = results.filter(r => !!r.result); + if (r.length === 0) { + return null; + } + + let range = new Range(position.line, r[0].startIndex, position.line, r[0].endIndex); + let hover = new Hover(r[0].result, range); + return hover; + }); + } + + private _computeInfoForToken(line: string, tokens: parser.IToken[], tokenIndex: number): Promise { + // ------------- + // Detect hovering on a key + if (tokens[tokenIndex].type === parser.TokenType.Key) { + var keyName = this._parser.keyNameFromKeyToken(this._parser.tokenValue(line, tokens[tokenIndex])).trim(); + var r = this._keyInfo[keyName]; + if (r) { + return Promise.resolve([r]); + } + } + + // ------------- + // Detect <> + // Detect <> + var helper = new suggestHelper.SuggestSupportHelper(); + var r2 = helper.getImageNameHover(line, this._parser, tokens, tokenIndex); + if (r2) { + return r2; + } + + return null; + } +} \ No newline at end of file diff --git a/dockerHubApi.ts b/dockerHubApi.ts index 57b93af87a..8d6cab8ec6 100644 --- a/dockerHubApi.ts +++ b/dockerHubApi.ts @@ -7,54 +7,54 @@ import vscode = require('vscode'); import https = require('https'); -export function tagsForImage(image:IHubSearchResponseResult): string { - var tags:string[] = []; - if (image.is_automated) { - tags.push('Automated'); - } else if (image.is_trusted) { - tags.push('Trusted'); - } else if (image.is_official) { - tags.push('Official'); - } - if (tags.length > 0) { - return '[' + tags.join('] [') + ']'; - } - return ''; +export function tagsForImage(image: IHubSearchResponseResult): string { + var tags: string[] = []; + if (image.is_automated) { + tags.push('Automated'); + } else if (image.is_trusted) { + tags.push('Trusted'); + } else if (image.is_official) { + tags.push('Official'); + } + if (tags.length > 0) { + return '[' + tags.join('] [') + ']'; + } + return ''; } -export function searchImageInRegistryHub(imageName:string, cache:boolean): Promise { - return invokeHubSearch(imageName, 1, cache).then((data) => { - if (data.results.length === 0) { - return null; - } - return data.results[0]; - }); +export function searchImageInRegistryHub(imageName: string, cache: boolean): Promise { + return invokeHubSearch(imageName, 1, cache).then((data) => { + if (data.results.length === 0) { + return null; + } + return data.results[0]; + }); } var popular = [ - {"is_automated":false,"name":"redis","is_trusted":false,"is_official":true,"star_count":831,"description":"Redis is an open source key-value store that functions as a data structure server."}, - {"is_automated":false,"name":"ubuntu","is_trusted":false,"is_official":true,"star_count":1827,"description":"Ubuntu is a Debian-based Linux operating system based on free software."}, - {"is_automated":false,"name":"wordpress","is_trusted":false,"is_official":true,"star_count":350,"description":"The WordPress rich content management system can utilize plugins, widgets, and themes."}, - {"is_automated":false,"name":"mysql","is_trusted":false,"is_official":true,"star_count":767,"description":"MySQL is a widely used, open-source relational database management system (RDBMS)."}, - {"is_automated":false,"name":"mongo","is_trusted":false,"is_official":true,"star_count":683,"description":"MongoDB document databases provide high availability and easy scalability."}, - {"is_automated":false,"name":"centos","is_trusted":false,"is_official":true,"star_count":1066,"description":"The official build of CentOS."}, - {"is_automated":false,"name":"node","is_trusted":false,"is_official":true,"star_count":787,"description":"Node.js is a JavaScript-based platform for server-side and networking applications."}, - {"is_automated":false,"name":"nginx","is_trusted":false,"is_official":true,"star_count":993,"description":"Official build of Nginx."}, - {"is_automated":false,"name":"postgres","is_trusted":false,"is_official":true,"star_count":778,"description":"The PostgreSQL object-relational database system provides reliability and data integrity."}, - {"is_automated":true,"name":"microsoft/aspnet","is_trusted":true,"is_official":false,"star_count":186,"description":"ASP.NET is an open source server-side Web application framework"} + { "is_automated": false, "name": "redis", "is_trusted": false, "is_official": true, "star_count": 1300, "description": "Redis is an open source key-value store that functions as a data structure server." }, + { "is_automated": false, "name": "ubuntu", "is_trusted": false, "is_official": true, "star_count": 2600, "description": "Ubuntu is a Debian-based Linux operating system based on free software." }, + { "is_automated": false, "name": "wordpress", "is_trusted": false, "is_official": true, "star_count": 582, "description": "The WordPress rich content management system can utilize plugins, widgets, and themes." }, + { "is_automated": false, "name": "mysql", "is_trusted": false, "is_official": true, "star_count": 1300, "description": "MySQL is a widely used, open-source relational database management system (RDBMS)." }, + { "is_automated": false, "name": "mongo", "is_trusted": false, "is_official": true, "star_count": 1100, "description": "MongoDB document databases provide high availability and easy scalability." }, + { "is_automated": false, "name": "centos", "is_trusted": false, "is_official": true, "star_count": 1600, "description": "The official build of CentOS." }, + { "is_automated": false, "name": "node", "is_trusted": false, "is_official": true, "star_count": 1200, "description": "Node.js is a JavaScript-based platform for server-side and networking applications." }, + { "is_automated": false, "name": "nginx", "is_trusted": false, "is_official": true, "star_count": 1600, "description": "Official build of Nginx." }, + { "is_automated": false, "name": "postgres", "is_trusted": false, "is_official": true, "star_count": 1200, "description": "The PostgreSQL object-relational database system provides reliability and data integrity." }, + { "is_automated": true, "name": "microsoft/aspnet", "is_trusted": true, "is_official": false, "star_count": 277, "description": "ASP.NET is an open source server-side Web application framework" } ]; -export function searchImagesInRegistryHub(prefix:string, cache:boolean): Promise { - if (prefix.length === 0) { - // return the popular images if user invoked intellisense - // right after typing the keyword and ':' (e.g. 'image:'). - return Promise.resolve(popular.slice(0)); - } - - // Do an image search on Docker hub and return the results - return invokeHubSearch(prefix, 100, cache).then((data) => { - return data.results; - }); +export function searchImagesInRegistryHub(prefix: string, cache: boolean): Promise { + if (prefix.length === 0) { + // return the popular images if user invoked intellisense + // right after typing the keyword and ':' (e.g. 'image:'). + return Promise.resolve(popular.slice(0)); + } + + // Do an image search on Docker hub and return the results + return invokeHubSearch(prefix, 100, cache).then((data) => { + return data.results; + }); } // https://registry.hub.docker.com/v1/search?q=redis&n=1 @@ -75,76 +75,71 @@ export function searchImagesInRegistryHub(prefix:string, cache:boolean): Promise // "query": "redis", // "page": 1 // } -function invokeHubSearch(imageName:string, count:number, cache:boolean): Promise { - // https://registry.hub.docker.com/v1/search?q=redis&n=1 - return fetchHttpsJson({ - hostname: 'registry.hub.docker.com', - port: 443, - path: '/v1/search?q=' + encodeURIComponent(imageName) + '&n=' + count, - method: 'GET', - }, cache); +function invokeHubSearch(imageName: string, count: number, cache: boolean): Promise { + // https://registry.hub.docker.com/v1/search?q=redis&n=1 + return fetchHttpsJson({ + hostname: 'registry.hub.docker.com', + port: 443, + path: '/v1/search?q=' + encodeURIComponent(imageName) + '&n=' + count, + method: 'GET', + }, cache); } export interface IHubSearchResponse { - num_pages: number; - num_results: number; - results: [IHubSearchResponseResult]; - page_size: number; - query: string; - page: number; + num_pages: number; + num_results: number; + results: [IHubSearchResponseResult]; + page_size: number; + query: string; + page: number; } export interface IHubSearchResponseResult { - is_automated: boolean; - name: string; - is_trusted: boolean; - is_official: boolean; - star_count: number; - description: string; + is_automated: boolean; + name: string; + is_trusted: boolean; + is_official: boolean; + star_count: number; + description: string; } -var JSON_CACHE:any = {}; +var JSON_CACHE: any = {}; -function fetchHttpsJson(opts:https.RequestOptions, cache:boolean): Promise { - if (!cache) { - return doFetchHttpsJson(opts); - } +function fetchHttpsJson(opts: https.RequestOptions, cache: boolean): Promise { + if (!cache) { + return doFetchHttpsJson(opts); + } - var cache_key = (opts.method + ' ' + opts.hostname + ' ' + opts.path); - if (!JSON_CACHE[cache_key]) { - JSON_CACHE[cache_key] = doFetchHttpsJson(opts); - } + var cache_key = (opts.method + ' ' + opts.hostname + ' ' + opts.path); + if (!JSON_CACHE[cache_key]) { + JSON_CACHE[cache_key] = doFetchHttpsJson(opts); + } - // new promise to avoid cancelling - return new Promise((resolve, reject) => { - JSON_CACHE[cache_key].then(resolve, reject); - }); + // new promise to avoid cancelling + return new Promise((resolve, reject) => { + JSON_CACHE[cache_key].then(resolve, reject); + }); } -function doFetchHttpsJson(opts:https.RequestOptions): Promise { - opts.headers = opts.headers || {}; - opts.headers['Accept'] = 'application/json'; - return httpsRequestAsPromise(opts).then((data) => { - return JSON.parse(data); - }) +function doFetchHttpsJson(opts: https.RequestOptions): Promise { + opts.headers = opts.headers || {}; + opts.headers['Accept'] = 'application/json'; + return httpsRequestAsPromise(opts).then((data) => { + return JSON.parse(data); + }) } -function httpsRequestAsPromise(opts:https.RequestOptions, token?: vscode.CancellationToken): Promise { +function httpsRequestAsPromise(opts: https.RequestOptions): Promise { return new Promise((resolve, reject) => { - var req = https.request(opts, (res) => { - var data = ''; - res.on('data', (d:string) => { - data += d; - }) - res.on('end', () => { - resolve(data); - }) - }); - req.end(); + var req = https.request(opts, (res) => { + var data = ''; + res.on('data', (d: string) => { + data += d; + }) + res.on('end', () => { + resolve(data); + }) + }); + req.end(); req.on('error', reject); - - if (token) { - token.onCancellationRequested(req.abort, req); - reject(); - } - }); + }); } \ No newline at end of file diff --git a/dockerfile/dockerfileCompletionItemProvider.ts b/dockerfile/dockerfileCompletionItemProvider.ts new file mode 100644 index 0000000000..bebf381c4a --- /dev/null +++ b/dockerfile/dockerfileCompletionItemProvider.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import { CompletionItemProvider, TextDocument, Position, CancellationToken, CompletionItem } from 'vscode'; +import helper = require('../helpers/suggestSupportHelper'); + +// IntelliSense +export class DockerfileCompletionItemProvider implements CompletionItemProvider { + + public triggerCharacters: string[] = []; + public excludeTokens: string[] = []; + + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { + var dockerSuggestSupport = new helper.SuggestSupportHelper(); + + var textLine = document.lineAt(position.line); + + // Matches strings like: 'FROM imagename' + var fromTextDocker = textLine.text.match(/^\s*FROM\s*([^"]*)$/); + + if (fromTextDocker) { + return dockerSuggestSupport.suggestImages(fromTextDocker[1]); + } + + return Promise.resolve([]); + } +} \ No newline at end of file diff --git a/dockerfile/dockerfileDef.ts b/dockerfile/dockerfileDef.ts deleted file mode 100644 index cac2c59588..0000000000 --- a/dockerfile/dockerfileDef.ts +++ /dev/null @@ -1,84 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); - -export var language = { - displayName: 'Dockerfile', - name: 'dockerfile', - defaultToken: '', - - instructions: /FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|ENTRYPOINT/, - - instructionAfter: /ONBUILD/, - - variableAfter: /ENV/, - - variable:/\${?[\w]+}?/, - - tokenizer: { - root: [ - { include: '@whitespace' }, - { include: '@comment' }, - - [/(@instructionAfter)(\s+)/, ['keyword', { token: '', next: '@instructions' }]], - ['', 'keyword', '@instructions'] - ], - - instructions: [ - [/(@variableAfter)(\s+)([\w]+)/, ['keyword', '',{token:'variable', next:'@arguments'}]], - [/(@instructions)/, 'keyword', '@arguments'] - ], - - arguments: [ - { include: '@whitespace' }, - { include: '@strings' }, - - [/(@variable)/, { cases: { '@eos': {token:'variable', next:'@popall'}, '@default': 'variable' }} ], - [/\\/, { cases: { '@eos': '', '@default': '' }}], - [/./, { cases: { '@eos': {token:'', next:'@popall'}, '@default': '' } }], - ], - - // Deal with white space, including comments - whitespace: [ - [/\s+/, { cases: { '@eos': {token:'', next:'@popall'}, '@default': '' }}], - ], - - comment: [ - [/(^#.*$)/, 'comment', '@popall'] - ], - - // Recognize strings, including those broken across lines with \ (but not without) - strings: [ - [/'$/, 'string', '@popall'], - [/'/, 'string', '@stringBody'], - [/"$/, 'string', '@popall'], - [/"/, 'string', '@dblStringBody'] - ], - stringBody: [ - [/[^\\\$']/, { cases: { '@eos': {token:'string', next:'@popall'}, '@default': 'string' }}], - - [/\\./, 'string.escape'], - [/'$/, 'string', '@popall'], - [/'/, 'string', '@pop'], - [/(@variable)/, 'variable' ], - - [/\\$/, 'string'], - [/$/, 'string', '@popall'] - ], - dblStringBody: [ - [/[^\\\$"]/, { cases: { '@eos': {token:'string', next:'@popall'}, '@default': 'string' }}], - - [/\\./, 'string.escape'], - [/"$/, 'string', '@popall'], - [/"/, 'string', '@pop'], - [/(@variable)/, 'variable' ], - - [/\\$/, 'string'], - [/$/, 'string', '@popall'] - ] - } -}; \ No newline at end of file diff --git a/dockerfile/dockerfileExtraInfo.ts b/dockerfile/dockerfileExtraInfo.ts deleted file mode 100644 index ec19255300..0000000000 --- a/dockerfile/dockerfileExtraInfo.ts +++ /dev/null @@ -1,191 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import {Range} from 'vscode'; -import htmlHelper = require('../helpers/htmlHelper'); -import parser = require('../parser'); -import hub = require('../dockerHubApi'); -import keyInfo = require('./dockerfileKeyInfo'); - -export class ExtraInfoSupport implements vscode.Modes.IExtraInfoSupport { - _parser: parser.Parser; - - constructor () { - this._parser = new parser.Parser('dockerfile'); - } - - public computeInfo(document: vscode.TextDocument, position: vscode.Position) { - - var lineEndColumn = document.getLineMaxColumn(position.line); - var line = document.getTextOnLine(position.line); - - if (line.length === 0) { - // empty line - return Promise.resolve(null); - } - - var tokens = this._parser.parseLine(line); - return this._computeInfoForLineWithTokens(line, tokens, position); - } - - private _computeInfoForLineWithTokens(line:string, tokens:parser.IToken[], position:vscode.Position): Promise { - var possibleTokens = this._parser.tokensAtColumn(tokens, position.character); - - return Promise.all(possibleTokens.map(tokenIndex => this._computeInfoForToken(line, tokens, tokenIndex))).then((results) => { - return possibleTokens.map((tokenIndex, arrayIndex) => { - return { - startIndex: tokens[tokenIndex].startIndex, - endIndex: tokens[tokenIndex].endIndex, - result: results[arrayIndex] - }; - }); - }).then((results) => { - var r = results.filter(r => !!r.result); - if (r.length === 0) { - return null; - } - return { - range: new vscode.Range(position.line, r[0].startIndex + 1, position.line, r[0].endIndex + 1), - htmlContent: r[0].result - }; - }); - } - - private _computeInfoForToken(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect hovering on a key - if (tokens[tokenIndex].type === parser.TokenType.Key) { - var keyName = this._parser.keyNameFromKeyToken(this._parser.tokenValue(line, tokens[tokenIndex])); - var r = ExtraInfoSupport.getInfoForKey(keyName); - if (r) { - return Promise.resolve(r); - } - } - - // ------------- - // Detect <> - // Detect <> - var r2 = this._getImageNameHover(line, tokens, tokenIndex); - if (r2) { - return r2; - } - - return null; - } - - private _getImageNameHover(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect <> - // Detect <> - var originalValue = this._parser.tokenValue(line, tokens[tokenIndex]); - - var keyToken:string = null; - tokenIndex--; - // = is for Dockerfile - while (tokenIndex >= 0) { - var type = tokens[tokenIndex].type; - if (type === parser.TokenType.String || type === parser.TokenType.Text) { - return null; - } - if (type === parser.TokenType.Key) { - keyToken = this._parser.tokenValue(line, tokens[tokenIndex]); - break; - } - tokenIndex--; - } - - if (!keyToken) { - return null; - } - var keyName = this._parser.keyNameFromKeyToken(keyToken); - if (keyName === 'FROM') { - var imageName = originalValue.replace(/^"/, '').replace(/"$/, ''); - return Promise.all([searchImageInRegistryHub(imageName)]).then((results) => { - if (results[0] && results[1]) { - return [{ - tagName: 'strong', - text: 'DockerHub:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[0] - }, { - tagName: 'strong', - text: 'DockerRuntime:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[1] - }] - } - if (results[0]) { - return results[0]; - } - return results[1]; - }); - } - } - - private static getInfoForKey(keyName:string): vscode.IHTMLContentElement[] { - if (ExtraInfoSupport._KEY_INFO === null) { - ExtraInfoSupport._KEY_INFO = {}; - Object.keys(keyInfo.KEY_INFO).forEach((keyName) => { - ExtraInfoSupport._KEY_INFO[keyName] = htmlHelper.simpleMarkDownToHTMLContent(keyInfo.KEY_INFO[keyName]); - }); - } - return ExtraInfoSupport._KEY_INFO[keyName] || null; - } - private static _KEY_INFO: { [keyName:string]: vscode.IHTMLContentElement[]; } = null; -} - -function searchImageInRegistryHub(imageName:string): Promise { - return hub.searchImageInRegistryHub(imageName, true).then((result) => { - if (result) { - var r: vscode.IHTMLContentElement[] = []; - - // Name - r.push({ - tagName: 'strong', - className: 'token keyword', - text: result.name - }); - - var tags = hub.tagsForImage(result); - if (tags.length > 0) { - r.push({ - tagName: 'strong', - text: ' ' + tags + ' ' - }); - } - - if (result.star_count) { - var plural = (result.star_count > 1); - r.push({ - tagName: 'strong', - text: String(result.star_count) - }) - r.push({ - tagName: 'span', - text: (plural ? ' stars' : ' star') - }); - } - - // Description - r.push({ - tagName: 'br' - }); - r.push({ - tagName: 'span', - text: result.description - }); - - return r; - } - }) -} \ No newline at end of file diff --git a/dockerfile/dockerfileKeyInfo.ts b/dockerfile/dockerfileKeyInfo.ts index 4402d028e7..2291d15ae9 100644 --- a/dockerfile/dockerfileKeyInfo.ts +++ b/dockerfile/dockerfileKeyInfo.ts @@ -3,51 +3,54 @@ *--------------------------------------------------------*/ // https://docs.docker.com/reference/builder/ -export var KEY_INFO:{[keyName:string]:string;} = { - 'FROM': ( - "Sets the *Base Image* for subsequent instructions." - ), - 'MAINTAINER': ( - "Set the *Author* field of the generated images." - ), - 'RUN': ( - "Executes any commands in a new layer on top of the current image and commits the results." - ), - 'CMD': ( - "Provides defaults for an executing container." - ), - 'LABEL': ( - "Adds metadata to an image. A *LABEL* is a key-value pair." - ), - 'EXPOSE': ( - "Informs Docker that the container will listen on the specified network ports at runtime." - ), - 'ENV': ( - "Sets the environment variable `key` to the value `value`." - ), - 'ADD': ( - "The *ADD* instruction copies new files, directories or remote file URLs from `src` and adds them " + - "to the filesystem of the container at the path `dest`." - ), - 'COPY': ( - "Copies new files or directories from `src` and adds them to the filesystem of the container at the path `dest`." - ), - 'ENTRYPOINT': ( - "Configures a container that will run as an executable." - ), - 'VOLUME': ( - "Creates a mount point with the specified name and marks it as holding externally mounted volumes " + - "from native host or other containers." - ), - 'USER': ( - "Sets the user name or UID to use when running the image and for any `RUN`, `CMD` and `ENTRYPOINT` " + - "instructions that follow it in the Dockerfile." - ), - 'WORKDIR': ( - "Sets the working directory for any `RUN`, `CMD`, `ENTRYPOINT`, `COPY` and `ADD` instructions that follow it in the Dockerfile." - ), - 'ONBUILD': ( - "Adds to the image a trigger instruction to be executed at a later time, when the image is used as the " + - "base for another build." - ) +export var DOCKERFILE_KEY_INFO: { [keyName: string]: string; } = { + 'FROM': ( + "Sets the **Base Image** for subsequent instructions." + ), + 'MAINTAINER': ( + "Set the **Author** field of the generated images." + ), + 'RUN': ( + "Executes any commands in a new layer on top of the current image and commits the results." + ), + 'CMD': ( + "Provides defaults for an executing container." + ), + 'LABEL': ( + "Adds metadata to an image. A **LABEL** is a key-value pair." + ), + 'EXPOSE': ( + "Informs Docker that the container will listen on the specified network ports at runtime." + ), + 'ENV': ( + "Sets the environment variable `key` to the value `value`." + ), + 'ADD': ( + "The **ADD** instruction copies new files, directories or remote file URLs from `src` and adds them " + + "to the filesystem of the container at the path `dest`." + ), + 'COPY': ( + "Copies new files or directories from `src` and adds them to the filesystem of the container at the path `dest`." + ), + 'ENTRYPOINT': ( + "Configures a container that will run as an executable." + ), + 'VOLUME': ( + "Creates a mount point with the specified name and marks it as holding externally mounted volumes " + + "from native host or other containers." + ), + 'USER': ( + "Sets the user name or UID to use when running the image and for any `RUN`, `CMD` and `ENTRYPOINT` " + + "instructions that follow it in the Dockerfile." + ), + 'WORKDIR': ( + "Sets the working directory for any `RUN`, `CMD`, `ENTRYPOINT`, `COPY` and `ADD` instructions that follow it in the Dockerfile." + ), + 'ONBUILD': ( + "Adds to the image a trigger instruction to be executed at a later time, when the image is used as the " + + "base for another build." + ), + 'STOPSIGNAL': ( + "Sets the system call signal that will be sent to the container to exit." + ) } \ No newline at end of file diff --git a/dockerfile/dockerfileParser.ts b/dockerfile/dockerfileParser.ts new file mode 100644 index 0000000000..356536245d --- /dev/null +++ b/dockerfile/dockerfileParser.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import {TextLine} from 'vscode'; +import {Parser, TokenType, IToken} from '../parser'; + +export class DockerfileParser extends Parser { + constructor() { + var parseRegex = /\ +$/g; + super(parseRegex, undefined); + } + + parseLine(textLine: TextLine): IToken[] { + if (textLine.isEmptyOrWhitespace) { + return null; + } + + var startIndex = textLine.firstNonWhitespaceCharacterIndex; + + // Check for comment + if (textLine.text.charAt(startIndex) === '#') { + return null; + } + + var tokens: IToken[] = []; + var previousTokenIndex = 0; + var keyFound: boolean = false; + + for (var j = startIndex, len = textLine.text.length; j < len; j++) { + var ch = textLine.text.charAt(j); + + if (ch === ' ' || ch === '\t') { + previousTokenIndex = j; + tokens.push({ + startIndex: 0, + endIndex: j, + type: TokenType.Key + }); + break; + } + } + + tokens.push({ + startIndex: previousTokenIndex, + endIndex: textLine.text.length, + type: TokenType.String + }); + return tokens; + } +} \ No newline at end of file diff --git a/dockerfile/dockerfileSuggestSupport.ts b/dockerfile/dockerfileSuggestSupport.ts deleted file mode 100644 index ecbac25d95..0000000000 --- a/dockerfile/dockerfileSuggestSupport.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import helper = require('../helpers/suggestSupportHelper'); - -// IntelliSense -export class SuggestSupport implements vscode.Modes.ISuggestSupport { - - public triggerCharacters:string[] = []; - public excludeTokens:string[] = []; - - public suggest(document:vscode.TextDocument, position:vscode.Position): Promise { - var dockerSuggestSupport = new helper.SuggestSupportHelper(); - - // Get the line where intellisense was invoked on (e.g. 'FROM '). - var line = document.getTextOnLine(position.line); - - var textBefore = line.substring(0, position.character); - - // Matches strings like: 'FROM imagename' - var fromTextDocker = textBefore.match(/^\s*FROM\s*([^"]*)$/); - - if (fromTextDocker) { - var imageText = fromTextDocker[1]; - // Doesn't have a leading quote - return dockerSuggestSupport.suggestImages(imageText, false); - } - - return Promise.resolve([]); - } -} \ No newline at end of file diff --git a/helpers/htmlHelper.ts b/helpers/htmlHelper.ts deleted file mode 100644 index 71b514adca..0000000000 --- a/helpers/htmlHelper.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); - -enum TagType { - pre, - bold, - normal -} - -export function simpleMarkDownToHTMLContent(source:string): vscode.IHTMLContentElement[] { - var r:vscode.IHTMLContentElement[] = []; - - var lastPushedTo:number; - var push = (to:number, type:TagType) => { - if (lastPushedTo >= to) { - return; - } - var text = source.substring(lastPushedTo, to); - - if (type === TagType.pre) { - r.push({ - tagName: "span", - style: "font-family:monospace", - className: "token keyword", - text: text - }); - } else if (type === TagType.bold) { - r.push({ - tagName: "strong", - text: text - }); - } else if (type === TagType.normal) { - r.push({ - tagName: "span", - text: text - }); - } - lastPushedTo = to; - } - - var currentTagType = () => { - if (inPre) { - return TagType.pre; - } - if (inBold) { - return TagType.bold; - } - return TagType.normal; - } - - var inPre = false, inBold = false; - for (var i = 0, len = source.length; i < len; i++) { - var ch = source.charAt(i); - - if (ch === '\n') { - push(i, currentTagType()); - r.push({ - tagName: 'br' - }); - lastPushedTo = i + 1; - } else if (ch === '`') { - push(i, currentTagType()); - lastPushedTo = i + 1; - inPre = !inPre; - } else if (ch === '*') { - push(i, currentTagType()); - lastPushedTo = i + 1; - inBold = !inBold; - } - } - push(source.length, currentTagType()); - - return r; -} \ No newline at end of file diff --git a/helpers/suggestSupportHelper.ts b/helpers/suggestSupportHelper.ts index fd78d19042..56ca714b6b 100644 --- a/helpers/suggestSupportHelper.ts +++ b/helpers/suggestSupportHelper.ts @@ -6,33 +6,101 @@ import vscode = require('vscode'); import hub = require('../dockerHubApi'); +import parser = require('../parser'); -export class SuggestSupportHelper { - suggestImages(word:string, hasLeadingQuote:boolean): Promise { - return this.suggestHubImages(word).then((results) => { - return [{ - incomplete: true, - currentWord: (hasLeadingQuote ? '"' + word : word), - suggestions: results - }] - }); - } - - suggestHubImages(word:string): Promise { - return hub.searchImagesInRegistryHub(word, true).then((results) => { - return results.map((image) => { - var stars = ''; - if (image.star_count > 0) { - stars = ' ' + image.star_count + ' ' + (image.star_count > 1 ? 'stars' : 'star'); - } - return { - label: image.name, - codeSnippet: image.name, - type: 'value', - documentationLabel: image.description, - typeLabel: hub.tagsForImage(image) + stars - } - }); - }); - } +export class SuggestSupportHelper { + suggestImages(word: string): Promise { + return hub.searchImagesInRegistryHub(word, true).then((results) => { + return results.map((image) => { + var stars = ''; + if (image.star_count > 0) { + stars = ' ' + image.star_count + ' ' + (image.star_count > 1 ? 'stars' : 'star'); + } + + return { + label: image.name, + kind: vscode.CompletionItemKind.Value, + detail: hub.tagsForImage(image) + stars, + insertText: image.name, + documentation: image.description, + } + }); + }); + } + + searchImageInRegistryHub(imageName: string): Promise { + return hub.searchImageInRegistryHub(imageName, true).then((result) => { + if (result) { + var r: vscode.MarkedString[] = []; + var tags = hub.tagsForImage(result); + + // Name, tags and stars. + var nameString = ''; + if (tags.length > 0) { + nameString = '**' + result.name + ' ' + tags + '** '; + } else { + nameString = '**' + result.name + '**'; + } + + if (result.star_count) { + var plural = (result.star_count > 1); + nameString += '**' + String(result.star_count) + (plural ? ' stars' : ' star') + '**'; + } + + r.push(nameString); + + // Description + r.push(result.description); + + return r; + } + }) + } + + getImageNameHover(line: string, _parser: parser.Parser, tokens: parser.IToken[], tokenIndex: number): Promise { + // ------------- + // Detect <> + // Detect <> + var originalValue = _parser.tokenValue(line, tokens[tokenIndex]); + + var keyToken: string = null; + tokenIndex--; + while (tokenIndex >= 0) { + var type = tokens[tokenIndex].type; + if (type === parser.TokenType.String || type === parser.TokenType.Text) { + return null; + } + if (type === parser.TokenType.Key) { + keyToken = _parser.tokenValue(line, tokens[tokenIndex]); + break; + } + tokenIndex--; + } + + if (!keyToken) { + return null; + } + var keyName = _parser.keyNameFromKeyToken(keyToken); + if (keyName === 'image' || keyName === 'FROM') { + + var imageName = originalValue.replace(/^"/, '').replace(/"$/, ''); + return Promise.all([this.searchImageInRegistryHub(imageName)]).then((results) => { + if (results[0] && results[1]) { + return [{ + value: '**DockerHub:**' + }, { + value: results[0] + }, { + value: '**DockerRuntime**' + }, { + value: results[1] + }] + } + if (results[0]) { + return results[0]; + } + return results[1]; + }); + } + } } \ No newline at end of file diff --git a/package.json b/package.json index eba3175871..d774c02edf 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,37 @@ -{ - "name": "vscode-docker", - "version": "0.0.2", - "publisher": "DockerTools", - "description": "Dockerfile and Docker yaml file support for Visual Studio Code", - "engines": { - "vscode": "*" - }, - "private": true, - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/vscode-docker.git" - }, - "activationEvents": ["onLanguage:dockerfile", "onLanguage:yaml"], - "main": "./out/dockerExtension", - "contributes": { - "languages": [{ - "id": "dockerfile", - "extensions": [ ".dockerfile" ], - "filenames": [ "Dockerfile" ], - "aliases": [ "Dockerfile" ] - }, - { - "id": "yaml", - "aliases": ["YAML", "yaml"], - "extensions": [".yml"], - "filenames": ["Docker-compose.yml"] - }] - }, - "scripts": { - "vscode:prepublish": "tsc", - "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" - }, - "extensionDependencies": [ - "vscode.docker", - "vscode.yaml" - ], - "devDependencies": { - "vscode": "*" - } -} +{ + "name": "vscode-docker", + "version": "0.0.2", + "publisher": "DockerTools", + "description": "Dockerfile and Docker yaml file support for Visual Studio Code", + "engines": { + "vscode": "0.10.x" + }, + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-docker.git" + }, + "activationEvents": [ + "onLanguage:dockerfile", + "onLanguage:yaml" + ], + "main": "./out/dockerExtension", + "contributes": { + "snippets": [ + { + "language": "dockerfile", + "path": "./snippets/dockerfile.json" + } + ] + }, + "scripts": { + "vscode:prepublish": "tsc", + "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" + }, + "extensionDependencies": [ + "vscode.yaml" + ], + "devDependencies": { + "vscode": "0.10.x" + } +} diff --git a/parser.ts b/parser.ts index 1cc874f078..6f80ec3009 100644 --- a/parser.ts +++ b/parser.ts @@ -4,133 +4,64 @@ 'use strict'; -import vscode = require('vscode'); +import {TextLine} from 'vscode'; -export class Parser { - _modeId: string; - _keySeparator: string; - _tokenParseRegex: RegExp; - - constructor(modeId: string) { - this._modeId = modeId; - - // Set the parser settings, depending on the mode. - if (this._modeId === "dockerfile") { - this._keySeparator = ' '; - this._tokenParseRegex = /\ +$/g; - } else if (this._modeId === 'yaml') { - this._keySeparator = ':'; - this._tokenParseRegex = /\:+$/g; - } - } - - keyNameFromKeyToken(keyToken:string): string { - return keyToken.replace(this._tokenParseRegex, ''); - } +export abstract class Parser { + _tokenParseRegex: RegExp; + _fileNameRegex: RegExp; - tokenValue(line:string, token:IToken): string { - return line.substring(token.startIndex, token.endIndex); - } + constructor(parseTokenRegex: RegExp, fileNameRegex: RegExp) { + this._tokenParseRegex = parseTokenRegex; + this._fileNameRegex = fileNameRegex; + } - tokensAtColumn(tokens:IToken[], charIndex:number): number[] { - for (var i = 0, len = tokens.length; i < len; i++) { - var token = tokens[i]; - - if (token.endIndex < charIndex) { - continue; - } - - if (token.endIndex === charIndex && i + 1 < len) { - return [i, i + 1] - } - return [i]; - } - - // should not happen: no token found? => return the last one - return [tokens.length - 1]; - } - - /** - ** A super simplified parser only looking out for strings and comments - **/ - parseLine(line:string): IToken[] { - var r: IToken[] = []; - var lastTokenEndIndex = 0, lastPushedToken:IToken = null; - - var emit = (end:number, type:TokenType) => { - if (end <= lastTokenEndIndex) { - return; - } - - if (lastPushedToken && lastPushedToken.type === type) { - // merge with last pushed token - lastPushedToken.endIndex = end; - lastTokenEndIndex = end; - return; - } - - lastPushedToken = { - startIndex: lastTokenEndIndex, - endIndex: end, - type: type - }; - - r.push(lastPushedToken); - lastTokenEndIndex = end; - }; - - var inString = false; - - for (var i = 0, len = line.length; i < len; i++) { - var ch = line.charAt(i); - - if (inString) { - if (ch === '"' && line.charAt(i-1) !== '\\') { - inString = false; - emit(i + 1, TokenType.String); - } - - continue; - } - - if (ch === '"') { - emit(i, TokenType.Text); - inString = true; - continue; - } - - if (ch === '#') { - // Comment the rest of the line - emit(i, TokenType.Text); - emit(line.length, TokenType.Comment); - break; - } - - if (ch === this._keySeparator) { - emit(i + 1, TokenType.Key); - } - - if (ch === ' ' || ch === '\t') { - emit(i, TokenType.Text); - emit(i + 1, TokenType.Whitespace); - } - } - - emit(line.length, TokenType.Text); - return r; - } + isFileSupported(fileName: string): boolean { + if (this._fileNameRegex !== undefined) { + return this._fileNameRegex.test(fileName); + } + + return true; + } + + keyNameFromKeyToken(keyToken: string): string { + return keyToken.replace(this._tokenParseRegex, ''); + } + + tokenValue(line: string, token: IToken): string { + return line.substring(token.startIndex, token.endIndex); + } + + tokensAtColumn(tokens: IToken[], charIndex: number): number[] { + for (var i = 0, len = tokens.length; i < len; i++) { + var token = tokens[i]; + + if (token.endIndex < charIndex) { + continue; + } + + if (token.endIndex === charIndex && i + 1 < len) { + return [i, i + 1] + } + return [i]; + } + + // should not happen: no token found? => return the last one + return [tokens.length - 1]; + } + + abstract parseLine(textLine: TextLine): IToken[]; } export enum TokenType { - Whitespace, - Text, - String, - Comment, - Key + Whitespace, + Text, + String, + Comment, + Key } export interface IToken { - startIndex: number; - endIndex: number; - type: TokenType; + startIndex: number; + endIndex: number; + type: TokenType; } \ No newline at end of file diff --git a/snippets/dockerfile.json b/snippets/dockerfile.json index 59d36d5b07..695d68dd63 100644 --- a/snippets/dockerfile.json +++ b/snippets/dockerfile.json @@ -2,113 +2,113 @@ "FROM": { "prefix": "FROM", "body": [ - "FROM ${baseImage}", - "$0" - ], - "description": "FROM" + "FROM ${baseImage}", + "$0" + ], + "description": "FROM" }, "MAINTAINER": { "prefix": "MAINTAINER", "body": [ - "MAINTAINER ${name}", - "$0" - ], - "description": "MAINTAINER" + "MAINTAINER ${name}", + "$0" + ], + "description": "MAINTAINER" }, "RUN": { "prefix": "RUN", "body": [ - "RUN ${cmd}", - "$0" - ], - "description": "RUN" + "RUN ${cmd}", + "$0" + ], + "description": "RUN" }, "CMD": { "prefix": "CMD", "body": [ - "CMD ${command}", - "$0" - ], - "description": "CMD" + "CMD ${command}", + "$0" + ], + "description": "CMD" }, "LABEL": { "prefix": "LABEL", "body": [ - "LABEL ${key}=${value}", - "$0" - ], - "description": "LABEL" + "LABEL ${key}=${value}", + "$0" + ], + "description": "LABEL" }, "EXPOSE": { "prefix": "EXPOSE", "body": [ - "EXPOSE ${port}", - "$0" - ], - "description": "EXPOSE" + "EXPOSE ${port}", + "$0" + ], + "description": "EXPOSE" }, "ENV": { "prefix": "ENV", "body": [ - "ENV ${key}=${value}", - "$0" - ], - "description": "ENV" + "ENV ${key}=${value}", + "$0" + ], + "description": "ENV" }, "ADD": { "prefix": "ADD", "body": [ - "ADD ${source} ${dest}", - "$0" - ], - "description": "ADD" + "ADD ${source} ${dest}", + "$0" + ], + "description": "ADD" }, "COPY": { "prefix": "COPY", "body": [ - "COPY ${source} ${dest}", - "$0" - ], - "description": "COPY" + "COPY ${source} ${dest}", + "$0" + ], + "description": "COPY" }, "ENTRYPOINT": { "prefix": "ENTRYPOINT", "body": [ - "ENTRYPOINT ${command}", - "$0" - ], - "description": "ENTRYPOINT" + "ENTRYPOINT ${command}", + "$0" + ], + "description": "ENTRYPOINT" }, "VOLUME": { "prefix": "VOLUME", "body": [ - "VOLUME ${name}", - "$0" - ], - "description": "VOLUME" + "VOLUME ${name}", + "$0" + ], + "description": "VOLUME" }, "USER": { "prefix": "USER", "body": [ - "USER ${daemon}", - "$0" - ], - "description": "USER" + "USER ${daemon}", + "$0" + ], + "description": "USER" }, "WORKDIR": { "prefix": "WORKDIR", "body": [ - "WORKDIR ${path}", - "$0" - ], - "description": "WORKDIR" + "WORKDIR ${path}", + "$0" + ], + "description": "WORKDIR" }, - "ONBUILD": { + "ONBUILD": { "prefix": "ONBUILD", "body": [ - "ONBUILD ${instruction}", - "$0" - ], - "description": "ONBUILD" + "ONBUILD ${instruction}", + "$0" + ], + "description": "ONBUILD" } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2e121bf4e7..99b0719a8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "module": "commonjs", "outDir": "out", "noLib": true, - "sourceMap": true + "sourceMap": true, + "target": "es5" }, "exclude": [ "node_modules" diff --git a/yaml/yamlExtraInfo.ts b/yaml/yamlExtraInfo.ts deleted file mode 100644 index 7bff50d682..0000000000 --- a/yaml/yamlExtraInfo.ts +++ /dev/null @@ -1,197 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import parser = require('../parser'); -import keyInfo = require('./yamlKeyInfo') -import hub = require('../dockerHubApi'); -import htmlHelper = require('../helpers/htmlHelper'); - -function isDockerCompose(resource:vscode.Uri): boolean { - return /docker\-compose\.yml$/.test(resource.toString()); -} - -export class ExtraInfoSupport implements vscode.Modes.IExtraInfoSupport { - _parser: parser.Parser; - - constructor () { - this._parser = new parser.Parser('yaml'); - } - - public computeInfo(document: vscode.TextDocument, position: vscode.Position) { - if (!isDockerCompose(document.getUri())) { - return Promise.resolve(null); - } - - var lineEndColumn = document.getLineMaxColumn(position.line); - var line = document.getTextOnLine(position.line); - - if (line.length === 0) { - // empty line - return Promise.resolve(null); - } - - var tokens = this._parser.parseLine(line); - return this._computeInfoForLineWithTokens(line, tokens, position); - } - - private _computeInfoForLineWithTokens(line:string, tokens:parser.IToken[], position:vscode.Position): Promise { - var possibleTokens = this._parser.tokensAtColumn(tokens, position.character); - - return Promise.all(possibleTokens.map(tokenIndex => this._computeInfoForToken(line, tokens, tokenIndex))).then((results) => { - return possibleTokens.map((tokenIndex, arrayIndex) => { - return { - startIndex: tokens[tokenIndex].startIndex, - endIndex: tokens[tokenIndex].endIndex, - result: results[arrayIndex] - }; - }); - }).then((results) => { - var r = results.filter(r => !!r.result); - if (r.length === 0) { - return null; - } - return { - range: new vscode.Range(position.line, r[0].startIndex + 1, position.line, r[0].endIndex + 1), - htmlContent: r[0].result - }; - }); - } - - private _computeInfoForToken(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect hovering on a key - if (tokens[tokenIndex].type === parser.TokenType.Key) { - var keyName = this._parser.keyNameFromKeyToken(this._parser.tokenValue(line, tokens[tokenIndex])); - var r = ExtraInfoSupport.getInfoForKey(keyName); - if (r) { - return Promise.resolve(r); - } - } - - // ------------- - // Detect <> - // Detect <> - var r2 = this._getImageNameHover(line, tokens, tokenIndex); - if (r2) { - return r2; - } - - return null; - } - - // hovering over image name. - private _getImageNameHover(line:string, tokens:parser.IToken[], tokenIndex:number): Promise { - // ------------- - // Detect <> - // Detect <> - var originalValue = this._parser.tokenValue(line, tokens[tokenIndex]); - - var keyToken:string = null; - tokenIndex--; - while (tokenIndex > 0) { - var type = tokens[tokenIndex].type; - if (type === parser.TokenType.String || type === parser.TokenType.Text) { - return null; - } - if (type === parser.TokenType.Key) { - keyToken = this._parser.tokenValue(line, tokens[tokenIndex]); - break; - } - tokenIndex--; - } - - if (!keyToken) { - return null; - } - var keyName = this._parser.keyNameFromKeyToken(keyToken); - if (keyName === 'image') { - var imageName = originalValue.replace(/^"/, '').replace(/"$/, ''); - return Promise.all([searchImageInRegistryHub(imageName)]).then((results) => { - if (results[0] && results[1]) { - return [{ - tagName: 'strong', - text: 'DockerHub:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[0] - }, { - tagName: 'strong', - text: 'DockerRuntime:' - }, { - tagName: 'br' - }, { - tagName: 'div', - children: results[1] - }] - } - if (results[0]) { - return results[0]; - } - return results[1]; - }); - } - } - - private static getInfoForKey(keyName:string): vscode.IHTMLContentElement[] { - if (ExtraInfoSupport._KEY_INFO === null) { - ExtraInfoSupport._KEY_INFO = {}; - Object.keys(keyInfo.KEY_INFO).forEach((keyName) => { - ExtraInfoSupport._KEY_INFO[keyName] = htmlHelper.simpleMarkDownToHTMLContent(keyInfo.KEY_INFO[keyName]); - }); - } - return ExtraInfoSupport._KEY_INFO[keyName] || null; - } - private static _KEY_INFO: { [keyName:string]: vscode.IHTMLContentElement[]; } = null; -} - -function searchImageInRegistryHub(imageName:string): Promise { - return hub.searchImageInRegistryHub(imageName, true).then((result) => { - if (result) { - var r: vscode.IHTMLContentElement[] = []; - - // Name - r.push({ - tagName: 'strong', - className: 'token keyword', - text: result.name - }); - - var tags = hub.tagsForImage(result); - if (tags.length > 0) { - r.push({ - tagName: 'strong', - text: ' ' + tags + ' ' - }); - } - - if (result.star_count) { - var plural = (result.star_count > 1); - r.push({ - tagName: 'strong', - text: String(result.star_count) - }) - r.push({ - tagName: 'span', - text: (plural ? ' stars' : ' star') - }); - } - - // Description - r.push({ - tagName: 'br' - }); - r.push({ - tagName: 'span', - text: result.description - }); - - return r; - } - }) -} \ No newline at end of file diff --git a/yaml/yamlKeyInfo.ts b/yaml/yamlKeyInfo.ts deleted file mode 100644 index 16dd074f0e..0000000000 --- a/yaml/yamlKeyInfo.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -// https://docs.docker.com/compose/yml/ -export var KEY_INFO:{[keyName:string]:string;} = { - 'image': ( - "Tag or partial image ID. Can be local or remote - Compose will attempt to pull if it doesn't exist locally." - ), - 'build': ( - "Path to a directory containing a Dockerfile. When the value supplied is a relative path, it is interpreted as relative to the " + - "location of the yml file itself. This directory is also the build context that is sent to the Docker daemon.\n\n" + - "Compose will build and tag it with a generated name, and use that image thereafter." - ), - 'command': ( - "Override the default command." - ), - 'links': ( - "Link to containers in another service. Either specify both the service name and the link alias (`CONTAINER:ALIAS`), or " + - "just the service name (which will also be used for the alias)." - ), - 'external_links': ( - "Link to containers started outside this `docker-compose.yml` or even outside of Compose, especially for containers that " + - "provide shared or common services. `external_links` follow " + - "semantics similar to `links` when specifying both the container name and the link alias (`CONTAINER:ALIAS`)." - ), - 'ports': ( - "Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen).\n\n" + - "*Note*: When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port " + - "lower than 60, because YAML will parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, we recommend " + - "always explicitly specifying your port mappings as strings." - ), - 'expose': ( - "Expose ports without publishing them to the host machine - they'll only be accessible to linked services. \n" + - "Only the internal port can be specified." - ), - 'volumes': ( - "Mount paths as volumes, optionally specifying a path on the host machine (`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`)." - ), - 'volumes_from': ( - "Mount all of the volumes from another service or container." - ), - 'environment': ( - "Add environment variables. You can use either an array or a dictionary.\n\n" + - "Environment variables with only a key are resolved to their values on the machine Compose is running on, which can be helpful for secret or host-specific values." - ), - 'env_file': ( - "Add environment variables from a file. Can be a single value or a list.\n\n" + - "If you have specified a Compose file with `docker-compose -f FILE`, paths in `env_file` are relative to the directory that file is in.\n\n" + - "Environment variables specified in `environment` override these values." - ), - 'net': ( - "Networking mode. Use the same values as the docker client `--net` parameter." - ), - 'pid': ( - "Sets the PID mode to the host PID mode. This turns on sharing between container and the host operating system the PID address space. " + - "Containers launched with this flag will be able to access and manipulate other containers in the bare-metal machine's namespace and vise-versa." - ), - 'dns': ( - "Custom DNS servers. Can be a single value or a list." - ), - 'cap_add': ( - "Add or drop container capabilities. See `man 7 capabilities` for a full list." - ), - 'cap_drop': ( - "Add or drop container capabilities. See `man 7 capabilities` for a full list." - ), - 'dns_search': ( - "Custom DNS search domains. Can be a single value or a list." - ), -} \ No newline at end of file diff --git a/yaml/yamlSuggestSupport.ts b/yaml/yamlSuggestSupport.ts deleted file mode 100644 index 5d4af6dd0e..0000000000 --- a/yaml/yamlSuggestSupport.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import helper = require('../helpers/suggestSupportHelper'); -import keyInfo = require('./yamlKeyInfo'); -import hub = require('../dockerHubApi'); - -function isDockerCompose(resource:vscode.Uri): boolean { - return /docker\-compose\.yml$/.test(resource.toString()); -} - -// IntelliSense -export class SuggestSupport implements vscode.Modes.ISuggestSupport { - - public triggerCharacters:string[] = []; - public excludeTokens:string[] = []; - - public suggest(document:vscode.TextDocument, position:vscode.Position): Promise { - if (!isDockerCompose(document.getUri())) { - return Promise.resolve(null); - } - - var yamlSuggestSupport = new helper.SuggestSupportHelper(); - - // Get the line where intellisense was invoked on (e.g. 'image: u'). - var line = document.getTextOnLine(position.line); - - if (line.length === 0) { - // empty line - return Promise.resolve([this.suggestKeys('')]); - } - - let range = document.getWordRangeAtPosition(position); - - // Get the text where intellisense was invoked on (e.g. 'u'). - let word = range && document.getTextInRange(range) || ''; - - var textBefore = line.substring(0, position.character); - if (/^\s*[\w_]*$/.test(textBefore)) { - // on the first token - return Promise.resolve([this.suggestKeys(word)]); - } - - // Matches strings like: 'image: "ubuntu' - var imageTextWithQuoteMatchYaml = textBefore.match(/^\s*image\s*\:\s*"([^"]*)$/); - - if (imageTextWithQuoteMatchYaml) { - var imageText = imageTextWithQuoteMatchYaml[1]; - return yamlSuggestSupport.suggestImages(imageText, true); - } - - // Matches strings like: 'image: ubuntu' - var imageTextWithoutQuoteMatch = textBefore.match(/^\s*image\s*\:\s*([\w\:\/]*)/); - - if (imageTextWithoutQuoteMatch) { - var imageText = imageTextWithoutQuoteMatch[1]; - return yamlSuggestSupport.suggestImages(imageText, false); - } - - return Promise.resolve([]); - } - - private suggestKeys(word:string): vscode.Modes.ISuggestions { - return { - currentWord: word, - suggestions: Object.keys(keyInfo.KEY_INFO).map((ruleName) => { - return { - label: ruleName, - codeSnippet: ruleName + ': ', - type: 'property', - documentationLabel: keyInfo.KEY_INFO[ruleName] - } - }) - }; - } -} \ No newline at end of file