diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 6851930bc9c..cfee800ca19 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -41,7 +41,7 @@ const nodeModules = ['electron', 'original-fs'] const builtInExtensions = [ { name: 'ms-vscode.node-debug', version: '1.9.10' }, - { name: 'ms-vscode.node-debug2', version: '1.9.7' } + { name: 'ms-vscode.node-debug2', version: '1.9.8' } ]; const vscodeEntryPoints = _.flatten([ diff --git a/extensions/git/package.json b/extensions/git/package.json index afd79ceaff7..25f361bad99 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -347,44 +347,7 @@ "group": "inline" } ] - }, - "languages": [ - { - "id": "git-commit", - "aliases": [ - "%language-alias.git-commit%", - "git-commit" - ], - "filenames": [ - "COMMIT_EDITMSG", - "MERGE_MSG" - ], - "configuration": "./git-commit.language-configuration.json" - }, - { - "id": "git-rebase", - "aliases": [ - "%language-alias.git-rebase%", - "git-rebase" - ], - "filenames": [ - "git-rebase-todo" - ], - "configuration": "./git-rebase.language-configuration.json" - } - ], - "grammars": [ - { - "language": "git-commit", - "scopeName": "text.git-commit", - "path": "./syntaxes/git-commit.tmLanguage" - }, - { - "language": "git-rebase", - "scopeName": "text.git-rebase", - "path": "./syntaxes/git-rebase.tmLanguage" - } - ] + } }, "dependencies": { "denodeify": "^1.2.1", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 282f4830455..01aa344df75 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -21,7 +21,5 @@ "command.pushTo": "Push to...", "command.sync": "Sync", "command.publish": "Publish", - "command.showOutput": "Show Git Output", - "language-alias.git-commit": "Git Commit Message", - "language-alias.git-rebase": "Git Rebase Message" + "command.showOutput": "Show Git Output" } \ No newline at end of file diff --git a/extensions/gitsyntax/OSSREADME.json b/extensions/gitsyntax/OSSREADME.json new file mode 100644 index 00000000000..0e3ddd52faf --- /dev/null +++ b/extensions/gitsyntax/OSSREADME.json @@ -0,0 +1,29 @@ +// ATTENTION - THIS DIRECTORY CONTAINS THIRD PARTY OPEN SOURCE MATERIALS: +[{ + "name": "textmate/git.tmbundle", + "version": "0.0.0", + "license": "MIT", + "repositoryURL": "https://github.com/textmate/git.tmbundle", + "licenseDetail": [ + "Copyright (c) 2008 Tim Harper", + "", + "Permission is hereby granted, free of charge, to any person obtaining", + "a copy of this software and associated documentation files (the\"", + "Software\"), to deal in the Software without restriction, including", + "without limitation the rights to use, copy, modify, merge, publish,", + "distribute, sublicense, and/or sell copies of the Software, and to", + "permit persons to whom the Software is furnished to do so, subject to", + "the following conditions:", + "", + "The above copyright notice and this permission notice shall be", + "included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,", + "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF", + "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND", + "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE", + "LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", + "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION", + "WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] +}] \ No newline at end of file diff --git a/extensions/git/git-commit.language-configuration.json b/extensions/gitsyntax/git-commit.language-configuration.json similarity index 100% rename from extensions/git/git-commit.language-configuration.json rename to extensions/gitsyntax/git-commit.language-configuration.json diff --git a/extensions/git/git-rebase.language-configuration.json b/extensions/gitsyntax/git-rebase.language-configuration.json similarity index 100% rename from extensions/git/git-rebase.language-configuration.json rename to extensions/gitsyntax/git-rebase.language-configuration.json diff --git a/extensions/gitsyntax/package.json b/extensions/gitsyntax/package.json new file mode 100644 index 00000000000..8ce5bd1ba32 --- /dev/null +++ b/extensions/gitsyntax/package.json @@ -0,0 +1,52 @@ +{ + "name": "gitsyntax", + "publisher": "vscode", + "displayName": "gitsyntax", + "description": "Git Syntax", + "version": "0.0.1", + "engines": { + "vscode": "^1.5.0" + }, + "categories": [ + "Other" + ], + "contributes": { + "languages": [ + { + "id": "git-commit", + "aliases": [ + "Git Commit Message", + "git-commit" + ], + "filenames": [ + "COMMIT_EDITMSG", + "MERGE_MSG" + ], + "configuration": "./git-commit.language-configuration.json" + }, + { + "id": "git-rebase", + "aliases": [ + "Git Rebase Message", + "git-rebase" + ], + "filenames": [ + "git-rebase-todo" + ], + "configuration": "./git-rebase.language-configuration.json" + } + ], + "grammars": [ + { + "language": "git-commit", + "scopeName": "text.git-commit", + "path": "./syntaxes/git-commit.tmLanguage" + }, + { + "language": "git-rebase", + "scopeName": "text.git-rebase", + "path": "./syntaxes/git-rebase.tmLanguage" + } + ] + } +} \ No newline at end of file diff --git a/extensions/git/syntaxes/git-commit.tmLanguage b/extensions/gitsyntax/syntaxes/git-commit.tmLanguage similarity index 100% rename from extensions/git/syntaxes/git-commit.tmLanguage rename to extensions/gitsyntax/syntaxes/git-commit.tmLanguage diff --git a/extensions/git/syntaxes/git-rebase.tmLanguage b/extensions/gitsyntax/syntaxes/git-rebase.tmLanguage similarity index 100% rename from extensions/git/syntaxes/git-rebase.tmLanguage rename to extensions/gitsyntax/syntaxes/git-rebase.tmLanguage diff --git a/extensions/git/test/colorize-fixtures/COMMIT_EDITMSG b/extensions/gitsyntax/test/colorize-fixtures/COMMIT_EDITMSG similarity index 100% rename from extensions/git/test/colorize-fixtures/COMMIT_EDITMSG rename to extensions/gitsyntax/test/colorize-fixtures/COMMIT_EDITMSG diff --git a/extensions/git/test/colorize-fixtures/git-rebase-todo b/extensions/gitsyntax/test/colorize-fixtures/git-rebase-todo similarity index 100% rename from extensions/git/test/colorize-fixtures/git-rebase-todo rename to extensions/gitsyntax/test/colorize-fixtures/git-rebase-todo diff --git a/extensions/git/test/colorize-results/COMMIT_EDITMSG.json b/extensions/gitsyntax/test/colorize-results/COMMIT_EDITMSG.json similarity index 100% rename from extensions/git/test/colorize-results/COMMIT_EDITMSG.json rename to extensions/gitsyntax/test/colorize-results/COMMIT_EDITMSG.json diff --git a/extensions/git/test/colorize-results/git-rebase-todo.json b/extensions/gitsyntax/test/colorize-results/git-rebase-todo.json similarity index 100% rename from extensions/git/test/colorize-results/git-rebase-todo.json rename to extensions/gitsyntax/test/colorize-results/git-rebase-todo.json diff --git a/extensions/html/server/src/modes/javascriptMode.ts b/extensions/html/server/src/modes/javascriptMode.ts index 71f651cb121..deb1bc2977e 100644 --- a/extensions/html/server/src/modes/javascriptMode.ts +++ b/extensions/html/server/src/modes/javascriptMode.ts @@ -64,10 +64,10 @@ export function getJavascriptMode(documentRegions: LanguageModelCache { + return diagnostics.map((diag): Diagnostic => { return { range: convertRange(currentTextDocument, diag), - severity: DiagnosticSeverity.Error, + severity: DiagnosticSeverity.Error, message: ts.flattenDiagnosticMessageText(diag.messageText, '\n') }; }); diff --git a/extensions/markdown/media/main.js b/extensions/markdown/media/main.js index 28b5db2a6b2..1c3414b843c 100644 --- a/extensions/markdown/media/main.js +++ b/extensions/markdown/media/main.js @@ -13,15 +13,16 @@ * returns the element prior to and the element after the given line. */ function getElementsForSourceLine(targetLine) { - let previous = null; - for (const element of document.getElementsByClassName('code-line')) { + const lines = document.getElementsByClassName('code-line'); + let previous = lines[0] && +lines[0].getAttribute('data-line') ? { line: +lines[0].getAttribute('data-line'), element: lines[0] } : null; + for (const element of lines) { const lineNumber = +element.getAttribute('data-line'); if (isNaN(lineNumber)) { continue; } const entry = { line: lineNumber, element: element }; if (lineNumber === targetLine) { - return { before: entry, next: null }; + return { previous: entry, next: null }; } else if (lineNumber > targetLine) { return { previous, next: entry }; } @@ -34,21 +35,22 @@ * Find the html elements that are at a specific pixel offset on the page. */ function getLineElementsAtPageOffset(offset) { - let before = null; - for (const element of document.getElementsByClassName('code-line')) { + const lines = document.getElementsByClassName('code-line'); + let previous = null; + for (const element of lines) { const line = +element.getAttribute('data-line'); if (isNaN(line)) { continue; } - const entry = {element, line }; + const entry = { element, line }; if (offset >= window.scrollY + element.getBoundingClientRect().top && offset <= window.scrollY + element.getBoundingClientRect().top + element.getBoundingClientRect().height) { - return { before: entry }; + return { previous: entry }; } else if (offset < window.scrollY + element.getBoundingClientRect().top) { - return { before, after: entry}; + return { previous, next: entry }; } - before = entry; + previous = entry; } - return {before}; + return { previous }; } function getSourceRevealAddedOffset() { @@ -76,14 +78,14 @@ } function didUpdateScrollPosition(offset) { - const {before, after } = getLineElementsAtPageOffset(offset); - if (before) { + const {previous, next} = getLineElementsAtPageOffset(offset); + if (previous) { let line = 0; - if (after) { - const betweenProgress = (offset - window.scrollY - before.element.getBoundingClientRect().top) / (after.element.getBoundingClientRect().top - before.element.getBoundingClientRect().top); - line = before.line + Math.floor(betweenProgress * (after.line - before.line)); + if (next) { + const betweenProgress = (offset - window.scrollY - previous.element.getBoundingClientRect().top) / (next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top); + line = previous.line + Math.floor(betweenProgress * (next.line - previous.line)); } else { - line = before.line; + line = previous.line; } const args = [window.initialData.source, line]; diff --git a/extensions/markdown/media/markdown.css b/extensions/markdown/media/markdown.css index e5c11cbbd33..b64f3689cf5 100644 --- a/extensions/markdown/media/markdown.css +++ b/extensions/markdown/media/markdown.css @@ -19,16 +19,7 @@ body.scrollBeyondLastLine { position: relative; } -.code-active-line:before { - content: ""; - display: block; - position: absolute; - top: 0; - left: -12px; - height: 100%; - border-left: 3px solid rgba(0, 122, 204, 0.5); -} - +.code-active-line:before, .code-line:hover:before { content: ""; display: block; @@ -36,6 +27,13 @@ body.scrollBeyondLastLine { top: 0; left: -12px; height: 100%; +} + +.code-active-line:before { + border-left: 3px solid rgba(0, 122, 204, 0.5); +} + +.code-line:hover:before { border-left: 3px solid #4080D0; } diff --git a/extensions/markdown/package.json b/extensions/markdown/package.json index 6b730418d27..fcec5143c1f 100644 --- a/extensions/markdown/package.json +++ b/extensions/markdown/package.json @@ -13,9 +13,11 @@ "Languages" ], "activationEvents": [ + "onLanguage:markdown", "onCommand:markdown.showPreview", "onCommand:markdown.showPreviewToSide", - "onCommand:markdown.showSource" + "onCommand:markdown.showSource", + "onLanguage:markdown" ], "contributes": { "languages": [ diff --git a/extensions/markdown/src/documentLinkProvider.ts b/extensions/markdown/src/documentLinkProvider.ts new file mode 100644 index 00000000000..bab89479a3d --- /dev/null +++ b/extensions/markdown/src/documentLinkProvider.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as path from 'path'; + +export default class MarkdownDocumentLinkProvider implements vscode.DocumentLinkProvider { + + private _linkPattern = /(\[[^\]]*\]\(\s*?)(\S+?)(\s+[^\)]*)?\)/g; + + public provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.DocumentLink[] { + const results: vscode.DocumentLink[] = []; + const base = path.dirname(document.uri.fsPath); + const text = document.getText(); + + this._linkPattern.lastIndex = 0; + let match: RegExpMatchArray | null; + while ((match = this._linkPattern.exec(text))) { + const pre = match[1]; + const link = match[2]; + const offset = match.index + pre.length; + const linkStart = document.positionAt(offset); + const linkEnd = document.positionAt(offset + link.length); + try { + let uri = vscode.Uri.parse(link); + if (!uri.scheme) { + // assume it must be a file + let file; + if (uri.path[0] === '/') { + file = path.join(vscode.workspace.rootPath, uri.path); + } else { + file = path.join(base, uri.path); + } + uri = vscode.Uri.file(file); + } + results.push(new vscode.DocumentLink( + new vscode.Range(linkStart, linkEnd), + uri)); + } catch (e) { + // noop + } + } + + return results; + } +}; diff --git a/extensions/markdown/src/documentSymbolProvider.ts b/extensions/markdown/src/documentSymbolProvider.ts new file mode 100644 index 00000000000..01b69324637 --- /dev/null +++ b/extensions/markdown/src/documentSymbolProvider.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; + +import { MarkdownEngine } from './markdownEngine'; + +export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolProvider { + + constructor(private engine: MarkdownEngine) { } + + provideDocumentSymbols(document: vscode.TextDocument): vscode.ProviderResult { + const tokens = this.engine.parse(document.getText()); + const headings = tokens.filter(token => token.type === 'heading_open'); + + return headings.map(heading => { + const lineNumber = heading.map[0]; + const line = document.lineAt(lineNumber); + const location = new vscode.Location(document.uri, line.range); + + // # Header => 'Header' + // ## Header ## => 'Header' + // ## Header #### => 'Header' + // Header ## => 'Header ##' + // ========= + const text = line.text.replace(/^\s*(#)+\s*(.*?)\s*\1*$/, '$2'); + + return new vscode.SymbolInformation(text, vscode.SymbolKind.Module, '', location); + }); + } +} \ No newline at end of file diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index 85c47c90422..a0ec0c1bbf2 100644 --- a/extensions/markdown/src/extension.ts +++ b/extensions/markdown/src/extension.ts @@ -8,6 +8,9 @@ import * as vscode from 'vscode'; import * as path from 'path'; import TelemetryReporter from 'vscode-extension-telemetry'; +import { MarkdownEngine } from './markdownEngine'; +import DocumentLinkProvider from './documentLinkProvider'; +import MDDocumentSymbolProvider from './documentSymbolProvider'; interface IPackageInfo { name: string; @@ -22,8 +25,16 @@ export function activate(context: vscode.ExtensionContext) { let packageInfo = getPackageInfo(context); telemetryReporter = packageInfo && new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); - let provider = new MDDocumentContentProvider(context); - context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('markdown', provider)); + const engine = new MarkdownEngine(); + + const contentProvider = new MDDocumentContentProvider(engine, context); + const contentProviderRegistration = vscode.workspace.registerTextDocumentContentProvider('markdown', contentProvider); + + const symbolsProvider = new MDDocumentSymbolProvider(engine); + const symbolsProviderRegistration = vscode.languages.registerDocumentSymbolProvider({ language: 'markdown' }, symbolsProvider); + context.subscriptions.push(contentProviderRegistration, symbolsProviderRegistration); + + context.subscriptions.push(vscode.languages.registerDocumentLinkProvider('markdown', new DocumentLinkProvider())); context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreview', showPreview)); context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreviewToSide', uri => showPreview(uri, true))); @@ -38,14 +49,14 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => { if (isMarkdownFile(document)) { const uri = getMarkdownUri(document.uri); - provider.update(uri); + contentProvider.update(uri); } })); context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(event => { if (isMarkdownFile(event.document)) { const uri = getMarkdownUri(event.document.uri); - provider.update(uri); + contentProvider.update(uri); } })); @@ -53,7 +64,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.workspace.textDocuments.forEach(document => { if (document.uri.scheme === 'markdown') { // update all generated md documents - provider.update(document.uri); + contentProvider.update(document.uri); } }); })); @@ -162,58 +173,15 @@ function getPackageInfo(context: vscode.ExtensionContext): IPackageInfo | null { return null; } - -interface IRenderer { - render(text: string): string; -} - class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { private _onDidChange = new vscode.EventEmitter(); private _waiting: boolean; - private _renderer: IRenderer; - constructor(private context: vscode.ExtensionContext) { + constructor( + private engine: MarkdownEngine, + private context: vscode.ExtensionContext + ) { this._waiting = false; - this._renderer = this.createRenderer(); - } - - private createRenderer(): IRenderer { - const hljs = require('highlight.js'); - const mdnh = require('markdown-it-named-headers'); - const md = require('markdown-it')({ - html: true, - highlight: (str: string, lang: string) => { - if (lang && hljs.getLanguage(lang)) { - try { - return `
${hljs.highlight(lang, str, true).value}
`; - } catch (error) { } - } - return `
${md.utils.escapeHtml(str)}
`; - } - }).use(mdnh, {}); - - function createLineNumberRenderer(ruleName: string) { - const original = md.renderer.rules[ruleName]; - return (tokens: any, idx: number, options: any, env: any, self: any) => { - const token = tokens[idx]; - if (token.level === 0 && token.map && token.map.length) { - token.attrSet('data-line', token.map[0]); - token.attrJoin('class', 'code-line'); - } - if (original) { - return original(tokens, idx, options, env, self); - } else { - return self.renderToken(tokens, idx, options, env, self); - } - }; - } - - md.renderer.rules.paragraph_open = createLineNumberRenderer('paragraph_open'); - md.renderer.rules.heading_open = createLineNumberRenderer('heading_open'); - md.renderer.rules.image = createLineNumberRenderer('image'); - md.renderer.rules.code_block = createLineNumberRenderer('code_block'); - - return md; } private getMediaPath(mediaFile: string): string { @@ -279,6 +247,7 @@ class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { const scrollBeyondLastLine = vscode.workspace.getConfiguration('editor')['scrollBeyondLastLine']; const wordWrap = vscode.workspace.getConfiguration('editor')['wordWrap']; const enablePreviewSync = vscode.workspace.getConfiguration('markdown').get('preview.experimentalSyncronizationEnabled', true); + const previewFrontMatter = vscode.workspace.getConfiguration('markdown')['previewFrontMatter']; let initialLine = 0; const editor = vscode.window.activeTextEditor; @@ -286,6 +255,8 @@ class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { initialLine = editor.selection.start.line; } + const body = this.engine.render(sourceUri, previewFrontMatter === 'hide', document.getText()); + return ` @@ -297,7 +268,7 @@ class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { - ${this._renderer.render(this.getDocumentContentForPreview(document))} + ${body} + + + + + \ No newline at end of file diff --git a/src/vs/editor/test/browser/view/minimapFontCreator.ts b/src/vs/editor/test/browser/view/minimapFontCreator.ts new file mode 100644 index 00000000000..31906e869a3 --- /dev/null +++ b/src/vs/editor/test/browser/view/minimapFontCreator.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Constants, MinimapCharRenderer } from 'vs/editor/common/view/minimapCharRenderer'; +import { MinimapCharRendererFactory } from 'vs/editor/test/common/view/minimapCharRendererFactory'; +import { createMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer'; + +let canvas = document.getElementById('my-canvas'); +let ctx = canvas.getContext('2d'); + +canvas.style.height = 100 + 'px'; +canvas.height = 100; + +canvas.width = Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH; +canvas.style.width = (Constants.CHAR_COUNT * Constants.SAMPLED_CHAR_WIDTH) + 'px'; + +ctx.fillStyle = '#ffffff'; +ctx.font = 'bold 16px monospace'; +for (let chCode = Constants.START_CH_CODE; chCode <= Constants.END_CH_CODE; chCode++) { + ctx.fillText(String.fromCharCode(chCode), (chCode - Constants.START_CH_CODE) * Constants.SAMPLED_CHAR_WIDTH, Constants.SAMPLED_CHAR_HEIGHT); +} + +let sampleData = ctx.getImageData(0, 4, Constants.SAMPLED_CHAR_WIDTH * Constants.CHAR_COUNT, Constants.SAMPLED_CHAR_HEIGHT); +let minimapCharRenderer = MinimapCharRendererFactory.create(sampleData.data); + +renderImageData(sampleData.data, sampleData.width, sampleData.height, 10, 100); +renderMinimapCharRenderer(minimapCharRenderer, 400); +renderMinimapCharRenderer(createMinimapCharRenderer(), 600); + +function renderMinimapCharRenderer(minimapCharRenderer: MinimapCharRenderer, y: number): void { + + let x2 = new Uint8ClampedArray(Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT); + for (let chCode = Constants.START_CH_CODE; chCode <= Constants.END_CH_CODE; chCode++) { + minimapCharRenderer.x2RenderChar(x2, Constants.CHAR_COUNT, 0, chCode - Constants.START_CH_CODE, chCode); + } + renderImageData(x2, Constants.x2_CHAR_WIDTH * Constants.CHAR_COUNT, Constants.x2_CHAR_HEIGHT, 10, y); + + let x1 = new Uint8ClampedArray(Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT); + for (let chCode = Constants.START_CH_CODE; chCode <= Constants.END_CH_CODE; chCode++) { + minimapCharRenderer.x1RenderChar(x1, Constants.CHAR_COUNT, 0, chCode - Constants.START_CH_CODE, chCode); + } + renderImageData(x1, Constants.x1_CHAR_WIDTH * Constants.CHAR_COUNT, Constants.x1_CHAR_HEIGHT, 10, y + 100); +} + +(function () { + let r = 'let x2Data = [', offset = 0; + for (let charIndex = 0; charIndex < Constants.CHAR_COUNT; charIndex++) { + let charCode = charIndex + Constants.START_CH_CODE; + r += '\n\n// ' + String.fromCharCode(charCode); + + for (let i = 0; i < Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.CA_CHANNELS_CNT; i++) { + if (i % 4 === 0) { + r += '\n'; + } + r += minimapCharRenderer.x2charData[offset] + ','; + offset++; + } + + } + r += '\n\n]'; + console.log(r); +})(); + +(function () { + let r = 'let x1Data = [', offset = 0; + for (let charIndex = 0; charIndex < Constants.CHAR_COUNT; charIndex++) { + let charCode = charIndex + Constants.START_CH_CODE; + r += '\n\n// ' + String.fromCharCode(charCode); + + for (let i = 0; i < Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.CA_CHANNELS_CNT; i++) { + if (i % 2 === 0) { + r += '\n'; + } + r += minimapCharRenderer.x1charData[offset] + ','; + offset++; + } + + } + r += '\n\n]'; + console.log(r); +})(); + + + +function renderImageData(data: Uint8ClampedArray, width: number, height: number, left: number, top: number): void { + let output = ''; + var offset = 0; + var PX_SIZE = 15; + for (var i = 0; i < height; i++) { + for (var j = 0; j < width; j++) { + var R = data[offset]; + var G = data[offset + 1]; + var B = data[offset + 2]; + var A = data[offset + 3]; + offset += 4; + + output += `
`; + } + } + + var domNode = document.createElement('div'); + domNode.style.position = 'absolute'; + domNode.style.top = top + 'px'; + domNode.style.left = left + 'px'; + domNode.style.width = (width * PX_SIZE) + 'px'; + domNode.style.height = (height * PX_SIZE) + 'px'; + domNode.style.border = '1px solid #ccc'; + domNode.style.background = '#000000'; + domNode.innerHTML = output; + document.body.appendChild(domNode); +} diff --git a/src/vs/editor/test/common/view/minimapCharRenderer.test.ts b/src/vs/editor/test/common/view/minimapCharRenderer.test.ts new file mode 100644 index 00000000000..1c9187c9084 --- /dev/null +++ b/src/vs/editor/test/common/view/minimapCharRenderer.test.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import { Constants } from 'vs/editor/common/view/minimapCharRenderer'; +import { MinimapCharRendererFactory } from 'vs/editor/test/common/view/minimapCharRendererFactory'; +import { createMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer'; + +suite('MinimapCharRenderer', () => { + + let sampleData: Uint8ClampedArray = null; + + suiteSetup(() => { + sampleData = new Uint8ClampedArray(Constants.SAMPLED_CHAR_HEIGHT * Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT); + }); + + suiteTeardown(() => { + sampleData = null; + }); + + setup(() => { + for (let i = 0; i < sampleData.length; i++) { + sampleData[i] = 0; + } + + }); + + const sampleD = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xd0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xd0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xd0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x0d, 0xff, 0xff, 0xff, 0xa3, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xe5, 0xff, 0xff, 0xff, 0x5e, 0xff, 0xff, 0xff, 0xd0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xa4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x10, 0xff, 0xff, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x94, 0xff, 0xff, 0xff, 0x02, 0xff, 0xff, 0xff, 0x6a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x22, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x47, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xd6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x0e, 0xff, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x69, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xb9, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x0e, 0xff, 0xff, 0xff, 0xa7, 0xff, 0xff, 0xff, 0xf5, 0xff, 0xff, 0xff, 0xe8, 0xff, 0xff, 0xff, 0x71, 0xff, 0xff, 0xff, 0xd0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + function setSampleData(charCode: number, data: number[]) { + const rowWidth = Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT; + let chIndex = charCode - Constants.START_CH_CODE; + + let globalOutputOffset = chIndex * Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT; + let inputOffset = 0; + for (let i = 0; i < Constants.SAMPLED_CHAR_HEIGHT; i++) { + let outputOffset = globalOutputOffset; + for (let j = 0; j < Constants.SAMPLED_CHAR_WIDTH; j++) { + for (let channel = 0; channel < Constants.RGBA_CHANNELS_CNT; channel++) { + sampleData[outputOffset] = data[inputOffset]; + inputOffset++; + outputOffset++; + } + } + globalOutputOffset += rowWidth; + } + } + + test('letter d @ 2x', () => { + setSampleData('d'.charCodeAt(0), sampleD); + let renderer = MinimapCharRendererFactory.create(sampleData); + + let dest = new Uint8ClampedArray(Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT); + renderer.x2RenderChar(dest, 1, 0, 0, 'd'.charCodeAt(0)); + + let actual: number[] = []; + for (let i = 0; i < dest.length; i++) { + actual[i] = dest[i]; + } + assert.deepEqual(actual, [ + 0x00, 0x00, 0x00, 0x00, 0xbf, 0xbf, 0xbf, 0x92, + 0xff, 0xff, 0xff, 0xbb, 0xff, 0xff, 0xff, 0xbe, + 0xff, 0xff, 0xff, 0x94, 0xd4, 0xd4, 0xd4, 0x97, + 0xff, 0xff, 0xff, 0xb1, 0xff, 0xff, 0xff, 0xbb, + ]); + }); + + test('letter d @ 2x at runtime', () => { + let renderer = createMinimapCharRenderer(); + + let dest = new Uint8ClampedArray(Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT); + renderer.x2RenderChar(dest, 1, 0, 0, 'd'.charCodeAt(0)); + + let actual: number[] = []; + for (let i = 0; i < dest.length; i++) { + actual[i] = dest[i]; + } + assert.deepEqual(actual, [ + 0x00, 0x00, 0x00, 0x00, 0xbf, 0xbf, 0xbf, 0x92, + 0xff, 0xff, 0xff, 0xbb, 0xff, 0xff, 0xff, 0xbe, + 0xff, 0xff, 0xff, 0x94, 0xd4, 0xd4, 0xd4, 0x97, + 0xff, 0xff, 0xff, 0xb1, 0xff, 0xff, 0xff, 0xbb, + ]); + }); + + test('letter d @ 1x', () => { + setSampleData('d'.charCodeAt(0), sampleD); + let renderer = MinimapCharRendererFactory.create(sampleData); + + let dest = new Uint8ClampedArray(Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT); + renderer.x1RenderChar(dest, 1, 0, 0, 'd'.charCodeAt(0)); + + let actual: number[] = []; + for (let i = 0; i < dest.length; i++) { + actual[i] = dest[i]; + } + assert.deepEqual(actual, [ + 0xad, 0xad, 0xad, 0x7d, + 0xeb, 0xeb, 0xeb, 0x9f, + ]); + }); + + test('letter d @ 1x at runtime', () => { + let renderer = createMinimapCharRenderer(); + + let dest = new Uint8ClampedArray(Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT); + renderer.x1RenderChar(dest, 1, 0, 0, 'd'.charCodeAt(0)); + + let actual: number[] = []; + for (let i = 0; i < dest.length; i++) { + actual[i] = dest[i]; + } + assert.deepEqual(actual, [ + 0xad, 0xad, 0xad, 0x7d, + 0xeb, 0xeb, 0xeb, 0x9f, + ]); + }); + +}); \ No newline at end of file diff --git a/src/vs/editor/test/common/view/minimapCharRendererFactory.ts b/src/vs/editor/test/common/view/minimapCharRendererFactory.ts new file mode 100644 index 00000000000..195ced2c8bf --- /dev/null +++ b/src/vs/editor/test/common/view/minimapCharRendererFactory.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Constants, MinimapCharRenderer } from 'vs/editor/common/view/minimapCharRenderer'; + +export class MinimapCharRendererFactory { + + public static create(source: Uint8ClampedArray): MinimapCharRenderer { + const expectedLength = (Constants.SAMPLED_CHAR_HEIGHT * Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT); + if (source.length !== expectedLength) { + throw new Error('Unexpected source in MinimapCharRenderer'); + } + + let x2CharData = MinimapCharRendererFactory._downsample2x(source); + let x1CharData = MinimapCharRendererFactory._downsample1x(source); + return new MinimapCharRenderer(x2CharData, x1CharData); + } + + private static _extractSampledChar(source: Uint8ClampedArray, charIndex: number, dest: Uint8ClampedArray) { + let destOffset = 0; + for (let i = 0; i < Constants.SAMPLED_CHAR_HEIGHT; i++) { + let sourceOffset = ( + Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * Constants.CHAR_COUNT * i + + Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT * charIndex + ); + for (let j = 0; j < Constants.SAMPLED_CHAR_WIDTH; j++) { + for (let c = 0; c < Constants.RGBA_CHANNELS_CNT; c++) { + dest[destOffset] = source[sourceOffset]; + sourceOffset++; + destOffset++; + } + } + } + } + + private static _downsample2xChar(source: Uint8ClampedArray, dest: Uint8ClampedArray): void { + // chars are 2 x 4px (width x height) + const resultLen = Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.CA_CHANNELS_CNT; + const result = new Uint16Array(resultLen); + for (let i = 0; i < resultLen; i++) { + result[i] = 0; + } + + let inputOffset = 0, globalOutputOffset = 0; + for (let i = 0; i < Constants.SAMPLED_CHAR_HEIGHT; i++) { + + let outputOffset = globalOutputOffset; + + let color = 0; + let alpha = 0; + for (let j = 0; j < Constants.SAMPLED_HALF_CHAR_WIDTH; j++) { + color += source[inputOffset]; // R + alpha += source[inputOffset + 3]; // A + inputOffset += Constants.RGBA_CHANNELS_CNT; + } + result[outputOffset] += color; + result[outputOffset + 1] += alpha; + outputOffset += Constants.CA_CHANNELS_CNT; + + color = 0; + alpha = 0; + for (let j = 0; j < Constants.SAMPLED_HALF_CHAR_WIDTH; j++) { + color += source[inputOffset]; // R + alpha += source[inputOffset + 3]; // A + inputOffset += Constants.RGBA_CHANNELS_CNT; + } + result[outputOffset] += color; + result[outputOffset + 1] += alpha; + outputOffset += Constants.CA_CHANNELS_CNT; + + if (i === 2 || i === 5 || i === 8) { + globalOutputOffset = outputOffset; + } + } + + for (let i = 0; i < resultLen; i++) { + dest[i] = result[i] / 12; // 15 it should be + } + } + + private static _downsample2x(data: Uint8ClampedArray): Uint8ClampedArray { + const resultLen = Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.CA_CHANNELS_CNT * Constants.CHAR_COUNT; + const result = new Uint8ClampedArray(resultLen); + + const sampledChar = new Uint8ClampedArray(Constants.SAMPLED_CHAR_HEIGHT * Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT); + const downsampledChar = new Uint8ClampedArray(Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.CA_CHANNELS_CNT); + + for (let charIndex = 0; charIndex < Constants.CHAR_COUNT; charIndex++) { + this._extractSampledChar(data, charIndex, sampledChar); + this._downsample2xChar(sampledChar, downsampledChar); + let resultOffset = (Constants.x2_CHAR_HEIGHT * Constants.x2_CHAR_WIDTH * Constants.CA_CHANNELS_CNT * charIndex); + for (let i = 0; i < downsampledChar.length; i++) { + result[resultOffset + i] = downsampledChar[i]; + } + } + + return result; + } + + private static _downsample1xChar(source: Uint8ClampedArray, dest: Uint8ClampedArray): void { + // chars are 1 x 2px (width x height) + const resultLen = Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.CA_CHANNELS_CNT; + const result = new Uint16Array(resultLen); + for (let i = 0; i < resultLen; i++) { + result[i] = 0; + } + + let inputOffset = 0, globalOutputOffset = 0; + for (let i = 0; i < Constants.SAMPLED_CHAR_HEIGHT; i++) { + + let outputOffset = globalOutputOffset; + + let color = 0; + let alpha = 0; + for (let j = 0; j < Constants.SAMPLED_CHAR_WIDTH; j++) { + color += source[inputOffset]; // R + alpha += source[inputOffset + 3]; // A + inputOffset += Constants.RGBA_CHANNELS_CNT; + } + result[outputOffset] += color; + result[outputOffset + 1] += alpha; + outputOffset += Constants.CA_CHANNELS_CNT; + + if (i === 5) { + globalOutputOffset = outputOffset; + } + } + + for (let i = 0; i < resultLen; i++) { + dest[i] = result[i] / 50; // 60 it should be + } + } + + private static _downsample1x(data: Uint8ClampedArray): Uint8ClampedArray { + const resultLen = Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.CA_CHANNELS_CNT * Constants.CHAR_COUNT; + const result = new Uint8ClampedArray(resultLen); + + const sampledChar = new Uint8ClampedArray(Constants.SAMPLED_CHAR_HEIGHT * Constants.SAMPLED_CHAR_WIDTH * Constants.RGBA_CHANNELS_CNT); + const downsampledChar = new Uint8ClampedArray(Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.CA_CHANNELS_CNT); + + for (let charIndex = 0; charIndex < Constants.CHAR_COUNT; charIndex++) { + this._extractSampledChar(data, charIndex, sampledChar); + this._downsample1xChar(sampledChar, downsampledChar); + let resultOffset = (Constants.x1_CHAR_HEIGHT * Constants.x1_CHAR_WIDTH * Constants.CA_CHANNELS_CNT * charIndex); + for (let i = 0; i < downsampledChar.length; i++) { + result[resultOffset + i] = downsampledChar[i]; + } + } + + return result; + } +} diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 4b8f1df755c..c83456f66bf 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -45,6 +45,7 @@ export interface IConfigurationRegistry { */ getConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema }; + registerOverrideIdentifiers(identifiers: string[]): void; } export interface IConfigurationPropertySchema extends IJSONSchema { @@ -70,12 +71,15 @@ class ConfigurationRegistry implements IConfigurationRegistry { private configurationProperties: { [qualifiedKey: string]: IJSONSchema }; private configurationSchema: IJSONSchema; private _onDidRegisterConfiguration: Emitter; + private overrideIdentifiers: string[] = []; + private overridePropertyPattern: string; constructor() { this.configurationContributors = []; this.configurationSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; this._onDidRegisterConfiguration = new Emitter(); this.configurationProperties = {}; + this.computeOverridePropertyPattern(); contributionRegistry.registerSchema(schemaId, this.configurationSchema); this.registerOverrideSettingsConfiguration(); @@ -100,6 +104,11 @@ class ConfigurationRegistry implements IConfigurationRegistry { this._onDidRegisterConfiguration.fire(this); } + public registerOverrideIdentifiers(overrideIdentifiers: string[]): void { + this.overrideIdentifiers.push(...overrideIdentifiers); + this.updateOverridePropertyPatternKey(); + } + private registerProperties(configuration: IConfigurationNode, overridable: boolean = false) { overridable = configuration.overridable || overridable; let properties = configuration.properties; @@ -150,7 +159,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { } }; if (configuration.id === SETTINGS_OVERRRIDE_NODE_ID) { - configurationSchema.patternProperties[OVERRIDE_PROPERTY] = objects.clone(configuration.properties['[]']); + configurationSchema.patternProperties[this.overridePropertyPattern] = objects.clone(configuration.properties['[]']); } else { register(configuration); } @@ -159,16 +168,27 @@ class ConfigurationRegistry implements IConfigurationRegistry { private updateSchemaForOverrideSettingsConfiguration(configuration: IConfigurationNode): void { if (configuration.id !== SETTINGS_OVERRRIDE_NODE_ID) { - let patternProperties = this.configurationSchema.patternProperties[OVERRIDE_PROPERTY]; + let patternProperties = this.configurationSchema.patternProperties[this.overridePropertyPattern]; if (patternProperties) { if (!patternProperties.properties) { patternProperties.properties = {}; } this.update(configuration, patternProperties); + contributionRegistry.registerSchema(schemaId, this.configurationSchema); } } } + private updateOverridePropertyPatternKey(): void { + let patternProperties = this.configurationSchema.patternProperties[this.overridePropertyPattern]; + if (patternProperties) { + delete this.configurationSchema.patternProperties[this.overridePropertyPattern]; + this.computeOverridePropertyPattern(); + this.configurationSchema.patternProperties[this.overridePropertyPattern] = patternProperties; + contributionRegistry.registerSchema(schemaId, this.configurationSchema); + } + } + private update(configuration: IConfigurationNode, overridePropertiesSchema: IJSONSchema): void { let properties = configuration.properties; if (properties) { @@ -184,13 +204,17 @@ class ConfigurationRegistry implements IConfigurationRegistry { } } + private computeOverridePropertyPattern(): void { + this.overridePropertyPattern = this.overrideIdentifiers.length ? OVERRIDE_PATTERN_WITH_SUBSTITUTION.replace('${0}', this.overrideIdentifiers.join('|')) : OVERRIDE_PROPERTY; + } + private registerOverrideSettingsConfiguration(): void { const properties = { '[]': { type: 'object', description: nls.localize('overrideSettings.description', "Configure settings to be overridden for a set of language identifiers."), additionalProperties: false, - errorMessage: 'Unknown configuration setting' + errorMessage: 'Unknown Identifier. Use language identifiers' } }; this.registerConfiguration({ @@ -204,6 +228,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { const SETTINGS_OVERRRIDE_NODE_ID = 'override'; const OVERRIDE_PROPERTY = '\\[.*\\]$'; +const OVERRIDE_PATTERN_WITH_SUBSTITUTION = '\\[(${0})\\]$'; export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY); function getDefaultValue(type: string | string[]): any { diff --git a/src/vs/platform/configuration/common/model.ts b/src/vs/platform/configuration/common/model.ts index 0538b6fea9e..b4b4168b369 100644 --- a/src/vs/platform/configuration/common/model.ts +++ b/src/vs/platform/configuration/common/model.ts @@ -8,6 +8,7 @@ import { Registry } from 'vs/platform/platform'; import * as types from 'vs/base/common/types'; import * as json from 'vs/base/common/json'; import * as objects from 'vs/base/common/objects'; +import * as arrays from 'vs/base/common/arrays'; import { IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IConfigModel, IOverrides } from 'vs/platform/configuration/common/configuration'; @@ -88,7 +89,7 @@ interface Overrides extends IOverrides { export class ConfigModel implements IConfigModel { protected _contents: T; - protected _overrides: IOverrides[] = null; + protected _overrides: IOverrides[] = []; private _raw: any = {}; private _parseErrors: any[] = []; @@ -121,12 +122,23 @@ export class ConfigModel implements IConfigModel { public merge(other: IConfigModel, overwrite: boolean = true): ConfigModel { const mergedModel = new ConfigModel(null); - mergedModel._contents = objects.clone(this.contents); - merge(mergedModel.contents, other.contents, overwrite); - mergedModel._overrides = other.overrides ? other.overrides : this.overrides; + this.doMerge(mergedModel, this, overwrite); + this.doMerge(mergedModel, other, overwrite); return mergedModel; } + protected doMerge(source: ConfigModel, target: IConfigModel, overwrite: boolean = true) { + source._contents = objects.clone(this.contents); + merge(source.contents, target.contents, overwrite); + const overrides = objects.clone(target.overrides); + for (const override of source.overrides) { + if (overrides.every(o => !arrays.equals(o.identifiers, override.identifiers))) { + overrides.push(override); + } + } + source._overrides = overrides; + } + public config(section: string): ConfigModel { const result = new ConfigModel(null); result._contents = objects.clone(this.contents[section]); @@ -148,7 +160,7 @@ export class ConfigModel implements IConfigModel { } public update(content: string): void { - let overrides: Overrides[] = null; + let overrides: Overrides[] = []; let currentProperty: string = null; let currentParent: any = []; let previousParents: any[] = []; @@ -166,7 +178,6 @@ export class ConfigModel implements IConfigModel { } function onOverrideSettingsValue(property: string, value: any): void { - overrides = overrides || []; overrides.push({ identifiers: [property.substring(1, property.length - 1).trim()], raw: value, @@ -213,7 +224,7 @@ export class ConfigModel implements IConfigModel { } this._contents = toValuesTree(this._raw, message => console.error(`Conflict in settings file ${this.name}: ${message}`)); const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); - this._overrides = overrides ? overrides.map>(override => { + this._overrides = overrides.map>(override => { // Filter unknown and non-overridable properties const raw = {}; for (const key in override.raw) { @@ -225,7 +236,7 @@ export class ConfigModel implements IConfigModel { identifiers: override.identifiers, contents: toValuesTree(raw, message => console.error(`Conflict in settings file ${this.name}: ${message}`)) }; - }) : null; + }); } } diff --git a/src/vs/platform/extensions/common/ipcRemoteCom.ts b/src/vs/platform/extensions/node/ipcRemoteCom.ts similarity index 100% rename from src/vs/platform/extensions/common/ipcRemoteCom.ts rename to src/vs/platform/extensions/node/ipcRemoteCom.ts diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index f3b6312c5d0..e51b03adac2 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -936,6 +936,15 @@ declare module 'vscode' { */ edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + /** + * Enters snippet mode in the editor with the specified snippet. + * + * @param snippet The snippet to insert in this edit. + * @param options The undo/redo behaviour around this edit. By default, undo stops will be created before and after this edit. + * @return A promise that resolves with a value indicating if the snippet could be inserted. + */ + edit(snippet: SnippetString, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + /** * Adds a set of decorations to the text editor. If a set of decorations already exists with * the given [decoration type](#TextEditorDecorationType), they will be replaced. diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 7af57e7eb0e..dbacd611ed6 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -34,7 +34,7 @@ import { IWorkspaceConfigurationValues } from 'vs/workbench/services/configurati import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceSymbol } from 'vs/workbench/parts/search/common/search'; -import { IApplyEditsOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditorsTracker'; +import { IApplyEditsOptions, IUndoStopOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditorsTracker'; import { InternalTreeExplorerNodeContent } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; @@ -138,6 +138,7 @@ export abstract class MainThreadEditorsShape { $tryRevealRange(id: string, range: editorCommon.IRange, revealType: TextEditorRevealType): TPromise { throw ni(); } $trySetSelections(id: string, selections: editorCommon.ISelection[]): TPromise { throw ni(); } $tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise { throw ni(); } + $tryInsertSnippet(id: string, template: string, opts: IUndoStopOptions): TPromise { throw ni(); } } export abstract class MainThreadTreeExplorersShape { diff --git a/src/vs/workbench/api/node/extHostEditors.ts b/src/vs/workbench/api/node/extHostEditors.ts index 7f1b0e57cd5..3a65e8da293 100644 --- a/src/vs/workbench/api/node/extHostEditors.ts +++ b/src/vs/workbench/api/node/extHostEditors.ts @@ -12,7 +12,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import { ExtHostDocuments, ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocuments'; -import { Selection, Range, Position, EndOfLine, TextEditorRevealType, TextEditorSelectionChangeKind, TextEditorLineNumbersStyle } from './extHostTypes'; +import { Selection, Range, Position, EndOfLine, TextEditorRevealType, TextEditorSelectionChangeKind, TextEditorLineNumbersStyle, SnippetString } from './extHostTypes'; import { ISingleEditOperation, TextEditorCursorStyle } from 'vs/editor/common/editorCommon'; import { IResolvedTextEditorConfiguration, ISelectionChangeEvent, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; import * as TypeConverters from './extHostTypeConverters'; @@ -595,10 +595,17 @@ class ExtHostTextEditor implements vscode.TextEditor { // ---- editing - edit(callback: (edit: TextEditorEdit) => void, options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Thenable { - let edit = new TextEditorEdit(this._documentData.document, options); - callback(edit); - return this._applyEdit(edit); + edit(callback: (edit: TextEditorEdit) => void, options: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + edit(snippet: SnippetString, options: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + + edit(callbackOrSnippet: ((edit: TextEditorEdit) => void) | SnippetString, options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Thenable { + if (SnippetString.isSnippetString(callbackOrSnippet)) { + return this._proxy.$tryInsertSnippet(this._id, callbackOrSnippet.value, options); + } else { + let edit = new TextEditorEdit(this._documentData.document, options); + callbackOrSnippet(edit); + return this._applyEdit(edit); + } } _applyEdit(editBuilder: TextEditorEdit): TPromise { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index e7b26461e60..b74137ff36e 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -519,6 +519,16 @@ export class WorkspaceEdit { export class SnippetString { + static isSnippetString(thing: any): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).value === 'string'; + } + private static _escape(value: string): string { return value.replace(/\$|}|\\/g, '\\$&'); } diff --git a/src/vs/workbench/api/node/mainThreadEditors.ts b/src/vs/workbench/api/node/mainThreadEditors.ts index 631c1454e78..7061f854f3a 100644 --- a/src/vs/workbench/api/node/mainThreadEditors.ts +++ b/src/vs/workbench/api/node/mainThreadEditors.ts @@ -14,7 +14,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { Position as EditorPosition } from 'vs/platform/editor/common/editor'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { MainThreadEditorsTracker, TextEditorRevealType, MainThreadTextEditor, IApplyEditsOptions, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; +import { MainThreadEditorsTracker, TextEditorRevealType, MainThreadTextEditor, IApplyEditsOptions, IUndoStopOptions, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { equals as arrayEquals } from 'vs/base/common/arrays'; import { equals as objectEquals } from 'vs/base/common/objects'; @@ -293,6 +293,13 @@ export class MainThreadEditors extends MainThreadEditorsShape { return TPromise.as(this._textEditorsMap[id].applyEdits(modelVersionId, edits, opts)); } + $tryInsertSnippet(id: string, template: string, opts: IUndoStopOptions): TPromise { + if (!this._textEditorsMap[id]) { + return TPromise.wrapError('TextEditor disposed'); + } + return TPromise.as(this._textEditorsMap[id].insertSnippet(template, opts)); + } + $registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void { this._editorTracker.registerTextEditorDecorationType(key, options); } diff --git a/src/vs/workbench/api/node/mainThreadEditorsTracker.ts b/src/vs/workbench/api/node/mainThreadEditorsTracker.ts index f742f730926..127ff1c9373 100644 --- a/src/vs/workbench/api/node/mainThreadEditorsTracker.ts +++ b/src/vs/workbench/api/node/mainThreadEditorsTracker.ts @@ -14,6 +14,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController'; import { EndOfLine, TextEditorLineNumbersStyle } from 'vs/workbench/api/node/extHostTypes'; export interface ITextEditorConfigurationUpdate { @@ -58,9 +59,12 @@ export enum TextEditorRevealType { InCenterIfOutsideViewport = 2 } -export interface IApplyEditsOptions { +export interface IUndoStopOptions { undoStopBefore: boolean; undoStopAfter: boolean; +} + +export interface IApplyEditsOptions extends IUndoStopOptions { setEndOfLine: EndOfLine; } @@ -383,6 +387,28 @@ export class MainThreadTextEditor { console.warn('applyEdits on invisible editor'); return false; } + + insertSnippet(template: string, opts: IUndoStopOptions) { + const snippetController = SnippetController.get(this._codeEditor); + + if (!this._codeEditor) { + return false; + } + + this._codeEditor.focus(); + + if (opts.undoStopBefore) { + this._codeEditor.pushUndoStop(); + } + + snippetController.insertSnippet(template, 0, 0); + + if (opts.undoStopAfter) { + this._codeEditor.pushUndoStop(); + } + + return true; + } } /** diff --git a/src/vs/workbench/api/node/mainThreadExtensionService.ts b/src/vs/workbench/api/node/mainThreadExtensionService.ts index 195282ff12b..722bf0a86f6 100644 --- a/src/vs/workbench/api/node/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/node/mainThreadExtensionService.ts @@ -59,6 +59,8 @@ export class MainProcessExtensionService extends AbstractExtensionService { this._onExtensionDescriptions(disabledExtensions.length ? extensionDescriptions.filter(e => disabledExtensions.indexOf(`${e.publisher}.${e.name}`) === -1) : extensionDescriptions); }); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index cd74b564aaf..ca6a2551315 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -321,6 +321,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.statusbarHeight = isStatusbarHidden ? 0 : this.partLayoutInfo.statusbar.height; this.titlebarHeight = isTitlebarHidden ? 0 : this.partLayoutInfo.titlebar.height / getZoomFactor(); // adjust for zoom prevention + const previousMaxPanelHeight = this.sidebarHeight - MIN_EDITOR_PART_HEIGHT; this.sidebarHeight = this.workbenchSize.height - this.statusbarHeight - this.titlebarHeight; let sidebarSize = new Dimension(sidebarWidth, this.sidebarHeight); @@ -333,6 +334,8 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal const maxPanelHeight = sidebarSize.height - MIN_EDITOR_PART_HEIGHT; if (isPanelHidden) { panelHeight = 0; + } else if (this.panelHeight === previousMaxPanelHeight) { + panelHeight = maxPanelHeight; } else if (this.panelHeight > 0) { panelHeight = Math.min(maxPanelHeight, Math.max(this.partLayoutInfo.panel.minHeight, this.panelHeight)); } else { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 8d35c1bfa8b..bc0d6c91f0f 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -29,6 +29,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/toggleActivityBarVisibility'; +import SCMPreview from 'vs/workbench/parts/scm/browser/scmPreview'; interface IViewletActivity { badge: IBadge; @@ -77,8 +78,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (pinnedViewlets) { // TODO@Ben: Migrate git => scm viewlet + + const map = SCMPreview.enabled + ? (id => id === 'workbench.view.git' ? 'workbench.view.scm' : id) + : (id => id === 'workbench.view.scm' ? 'workbench.view.git' : id); + this.pinnedViewlets = pinnedViewlets - .map(id => id === 'workbench.view.git' ? 'workbench.view.scm' : id) + .map(map) .filter(arrays.uniqueFilter(str => str)); } else { diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index b13eb919ae6..3065d9f8c6e 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -87,6 +87,7 @@ export abstract class CompositePart extends Part { this.activeComposite = null; this.instantiatedComposites = []; this.compositeLoaderPromises = {}; + this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE); } protected openComposite(id: string, focus?: boolean): TPromise { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 4095d49b320..ca9880b768c 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -265,6 +265,16 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen prefix: NAVIGATE_IN_GROUP_ONE_PREFIX, needsEditor: false, description: nls.localize('groupOnePicker', "Show Editors in First Group") + }, + { + prefix: NAVIGATE_IN_GROUP_TWO_PREFIX, + needsEditor: false, + description: nls.localize('groupTwoPicker', "Show Editors in Second Group") + }, + { + prefix: NAVIGATE_IN_GROUP_THREE_PREFIX, + needsEditor: false, + description: nls.localize('groupThreePicker', "Show Editors in Third Group") } ] ) @@ -275,13 +285,7 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen 'vs/workbench/browser/parts/editor/editorPicker', 'GroupTwoPicker', NAVIGATE_IN_GROUP_TWO_PREFIX, - [ - { - prefix: NAVIGATE_IN_GROUP_TWO_PREFIX, - needsEditor: false, - description: nls.localize('groupTwoPicker', "Show Editors in Second Group") - } - ] + [] ) ); @@ -290,13 +294,7 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen 'vs/workbench/browser/parts/editor/editorPicker', 'GroupThreePicker', NAVIGATE_IN_GROUP_THREE_PREFIX, - [ - { - prefix: NAVIGATE_IN_GROUP_THREE_PREFIX, - needsEditor: false, - description: nls.localize('groupThreePicker', "Show Editors in Third Group") - } - ] + [] ) ); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 4e3047ec215..ec2adb866fb 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -907,7 +907,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService } } - public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: EditorOptions }[]): TPromise { + public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: EditorOptions }[], position?: Position): TPromise { const activeReplacements: IEditorReplacement[] = []; const hiddenReplacements: IEditorReplacement[] = []; @@ -919,19 +919,21 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService // For each group this.stacks.groups.forEach(group => { - const index = group.indexOf(editor.toReplace); - if (index >= 0) { - if (editor.options) { - editor.options.index = index; // make sure we respect the index of the editor to replace! - } else { - editor.options = EditorOptions.create({ index }); - } + if (position === void 0 || this.stacks.positionOfGroup(group) === position) { + const index = group.indexOf(editor.toReplace); + if (index >= 0) { + if (editor.options) { + editor.options.index = index; // make sure we respect the index of the editor to replace! + } else { + editor.options = EditorOptions.create({ index }); + } - const replacement = { group, editor: editor.toReplace, replaceWith: editor.replaceWith, options: editor.options }; - if (group.activeEditor.matches(editor.toReplace)) { - activeReplacements.push(replacement); - } else { - hiddenReplacements.push(replacement); + const replacement = { group, editor: editor.toReplace, replaceWith: editor.replaceWith, options: editor.options }; + if (group.activeEditor.matches(editor.toReplace)) { + activeReplacements.push(replacement); + } else { + hiddenReplacements.push(replacement); + } } } }); diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 3aad78cc9bf..d1b2616d085 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -214,25 +214,32 @@ export class TextDiffEditor extends BaseTextEditor { protected getConfigurationOverrides(): IEditorOptions { const options: IDiffEditorOptions = super.getConfigurationOverrides(); + options.readOnly = this.isReadOnly(); + + return options; + } + + protected getAriaLabel(): string { + let ariaLabel: string; + const inputName = this.input && this.input.getName(); + if (this.isReadOnly()) { + ariaLabel = inputName ? nls.localize('readonlyEditorWithInputAriaLabel', "{0}. Readonly text compare editor.", inputName) : nls.localize('readonlyEditorAriaLabel', "Readonly text compare editor."); + } else { + ariaLabel = inputName ? nls.localize('editableEditorWithInputAriaLabel', "{0}. Text file compare editor.", inputName) : nls.localize('editableEditorAriaLabel', "Text file compare editor."); + } + + return ariaLabel; + } + + private isReadOnly(): boolean { const input = this.input; if (input instanceof DiffEditorInput) { const modifiedInput = input.modifiedInput; - const readOnly = modifiedInput instanceof StringEditorInput || modifiedInput instanceof ResourceEditorInput; - options.readOnly = readOnly; - - let ariaLabel: string; - const inputName = input && input.getName(); - if (readOnly) { - ariaLabel = inputName ? nls.localize('readonlyEditorWithInputAriaLabel', "{0}. Readonly text compare editor.", inputName) : nls.localize('readonlyEditorAriaLabel', "Readonly text compare editor."); - } else { - ariaLabel = inputName ? nls.localize('editableEditorWithInputAriaLabel', "{0}. Text file compare editor.", inputName) : nls.localize('editableEditorAriaLabel', "Text file compare editor."); - } - - options.ariaLabel = ariaLabel; + return modifiedInput instanceof StringEditorInput || modifiedInput instanceof ResourceEditorInput; } - return options; + return false; } private isFileBinaryError(error: Error[]): boolean; diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index b69ebd57b3e..ede09948f5e 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -5,6 +5,7 @@ 'use strict'; +import nls = require('vs/nls'); import { TPromise } from 'vs/base/common/winjs.base'; import { Dimension, Builder } from 'vs/base/browser/builder'; import objects = require('vs/base/common/objects'); @@ -97,24 +98,40 @@ export abstract class BaseTextEditor extends BaseEditor { protected computeConfiguration(configuration: IEditorConfiguration): IEditorOptions { // Specific editor options always overwrite user configuration - const editorConfiguration = types.isObject(configuration.editor) ? objects.clone(configuration.editor) : Object.create(null); + const editorConfiguration: IEditorOptions = types.isObject(configuration.editor) ? objects.clone(configuration.editor) : Object.create(null); objects.assign(editorConfiguration, this.getConfigurationOverrides()); + // ARIA label + editorConfiguration.ariaLabel = this.computeAriaLabel(); + return editorConfiguration; } + private computeAriaLabel(): string { + let ariaLabel = this.getAriaLabel(); + + // Apply group information to help identify in which group we are + if (ariaLabel && typeof this.position === 'number') { + ariaLabel = nls.localize('editorLabelWithGroup', "{0} Group {1}.", ariaLabel, this.position + 1); + } + + return ariaLabel; + } + protected getConfigurationOverrides(): IEditorOptions { const overrides = {}; const language = this.getLanguage(); if (language) { objects.assign(overrides, this.configurationService.getConfiguration({ overrideIdentifier: language, section: 'editor' })); } + objects.assign(overrides, { overviewRulerLanes: 3, lineNumbersMinChars: 3, theme: this.themeService.getColorTheme().id, fixedOverflowWidgets: true }); + return overrides; } @@ -151,6 +168,15 @@ export abstract class BaseTextEditor extends BaseEditor { }); } + public changePosition(position: Position): void { + super.changePosition(position); + + // Make sure to update ARIA label if the position of this editor changed + if (this.editorControl) { + this.editorControl.updateOptions({ ariaLabel: this.computeAriaLabel() }); + } + } + protected setEditorVisible(visible: boolean, position: Position = null): void { // Pass on to Editor @@ -241,6 +267,7 @@ export abstract class BaseTextEditor extends BaseEditor { return model.getLanguageIdentifier().language; } } + if (this.input) { const resource = toResource(this.input); if (resource) { @@ -250,9 +277,12 @@ export abstract class BaseTextEditor extends BaseEditor { } } } + return null; } + protected abstract getAriaLabel(): string; + public dispose(): void { // Destroy Editor Control diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index b6323c8980e..539ef2ffe60 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -126,11 +126,14 @@ export class TextResourceEditor extends BaseTextEditor { protected getConfigurationOverrides(): IEditorOptions { const options = super.getConfigurationOverrides(); - const input = this.input; - const isUntitled = input instanceof UntitledEditorInput; - const isReadonly = !isUntitled; // all string editors are readonly except for the untitled one + options.readOnly = !(this.input instanceof UntitledEditorInput); // all resource editors are readonly except for the untitled one; - options.readOnly = isReadonly; + return options; + } + + protected getAriaLabel(): string { + const input = this.input; + const isReadonly = !(this.input instanceof UntitledEditorInput); let ariaLabel: string; const inputName = input && input.getName(); @@ -140,9 +143,7 @@ export class TextResourceEditor extends BaseTextEditor { ariaLabel = inputName ? nls.localize('untitledFileEditorWithInputAriaLabel', "{0}. Untitled file text editor.", inputName) : nls.localize('untitledFileEditorAriaLabel', "Untitled file text editor."); } - options.ariaLabel = ariaLabel; - - return options; + return ariaLabel; } /** diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 2ddf183b385..dc3bd89e95e 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -13,20 +13,13 @@ } .monaco-workbench > .part.panel .title { + border-top: 1px solid rgba(128, 128, 128, 0.35); padding-right: 0px; height: 35px; } -.vs-dark .monaco-workbench > .part.panel .title { - background: #232323; -} - -.vs .monaco-workbench > .part.panel .title { - background: #F6F6F6; -} - .hc-black .monaco-workbench > .part.panel .title { - border-top: 1px solid #6FC3DF; + border-top-color: #6FC3DF; } /** Panel Switcher */ @@ -38,48 +31,50 @@ .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label { text-transform: uppercase; - margin-right: 20px; + margin-right: 33px; font-size: 11px; + padding-bottom: 4px; /* puts the bottom border down */ } .vs .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label { - color: #6f6f6f; + color: #424242; + opacity: 0.75; } .vs-dark .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label { - color: #bbbbbb; + color: #e7e7e7; + opacity: 0.5; } .hc-black .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label { color: #fff; } -.vs-dark .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover .action-label { - color: white; +.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover .action-label, +.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked { + opacity: 1; +} + +.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked { + border-bottom: 1px solid; } .vs .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked { - border-bottom: 2px solid #6f6f6f; + border-bottom-color: #ccceda; } .vs-dark .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked { - color: white; - border-bottom: 2px solid white; -} - -.hc-black .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked { - color: white; - border-bottom: 2px solid white; + border-bottom-color: #404047; } .vs .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:focus, .vs-dark .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:focus { - border-bottom: 2px solid #007ACC; + border-bottom: 1px solid #007ACC; outline: none !important; } .hc-black .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:focus { - border-bottom: 2px solid #f38518; + border-bottom: 1px solid #f38518; outline: none !important; } diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 4f2a20f0373..f8ad40bec26 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -12,6 +12,7 @@ import { IOptions } from 'vs/workbench/common/options'; import * as browser from 'vs/base/browser/browser'; import { domContentLoaded } from 'vs/base/browser/dom'; import errors = require('vs/base/common/errors'); +import comparer = require('vs/base/common/comparers'); import platform = require('vs/base/common/platform'); import paths = require('vs/base/common/paths'); import uri from 'vs/base/common/uri'; @@ -46,11 +47,15 @@ export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { } export function startup(configuration: IWindowConfiguration): TPromise { + // Ensure others can listen to zoom level changes browser.setZoomFactor(webFrame.getZoomFactor()); browser.setZoomLevel(webFrame.getZoomLevel()); browser.setFullscreen(!!configuration.fullscreen); + // Setup Intl + comparer.setFileNameComparer(new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })); + // Shell Options const filesToOpen = configuration.filesToOpen && configuration.filesToOpen.length ? toInputs(configuration.filesToOpen) : null; const filesToCreate = configuration.filesToCreate && configuration.filesToCreate.length ? toInputs(configuration.filesToCreate) : null; diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index f01dcfc984c..5acafaff3bb 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -96,6 +96,7 @@ import { ITextMateService } from 'vs/editor/node/textMate/textMateService'; import { MainProcessTextMateSyntax } from 'vs/editor/electron-browser/textMate/TMSyntax'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { readFontInfo } from 'vs/editor/browser/config/configuration'; +import SCMPreview from 'vs/workbench/parts/scm/browser/scmPreview'; import 'vs/platform/opener/browser/opener.contribution'; /** @@ -331,7 +332,10 @@ export class WorkbenchShell { serviceCollection.set(IThreadService, this.threadService); this.timerService.beforeExtensionLoad = new Date(); - this.extensionService = instantiationService.createInstance(MainProcessExtensionService); + + // TODO@Joao: remove + const disabledExtensions = SCMPreview.enabled ? [] : ['vscode.git']; + this.extensionService = instantiationService.createInstance(MainProcessExtensionService, disabledExtensions); serviceCollection.set(IExtensionService, this.extensionService); extensionHostProcessWorker.start(this.extensionService); this.extensionService.onReady().done(() => { diff --git a/src/vs/workbench/electron-browser/workbench.main.ts b/src/vs/workbench/electron-browser/workbench.main.ts index 5d24296aa63..7540d06d5dc 100644 --- a/src/vs/workbench/electron-browser/workbench.main.ts +++ b/src/vs/workbench/electron-browser/workbench.main.ts @@ -46,10 +46,10 @@ import 'vs/workbench/parts/search/browser/openAnythingHandler'; // can be packag import 'vs/workbench/parts/scm/browser/scm.contribution'; import 'vs/workbench/parts/scm/browser/scmViewlet'; // can be packaged separately -// import 'vs/workbench/parts/git/electron-browser/git.contribution'; -// import 'vs/workbench/parts/git/browser/gitQuickOpen'; -// import 'vs/workbench/parts/git/browser/gitActions.contribution'; -// import 'vs/workbench/parts/git/browser/gitViewlet'; // can be packaged separately +import 'vs/workbench/parts/git/electron-browser/git.contribution'; +import 'vs/workbench/parts/git/browser/gitQuickOpen'; +import 'vs/workbench/parts/git/browser/gitActions.contribution'; +import 'vs/workbench/parts/git/browser/gitViewlet'; // can be packaged separately import 'vs/workbench/parts/debug/electron-browser/debug.contribution'; import 'vs/workbench/parts/debug/electron-browser/repl'; diff --git a/src/vs/workbench/node/extensionHostMain.ts b/src/vs/workbench/node/extensionHostMain.ts index d8ea9818d64..ea41b168ef2 100644 --- a/src/vs/workbench/node/extensionHostMain.ts +++ b/src/vs/workbench/node/extensionHostMain.ts @@ -9,7 +9,7 @@ import nls = require('vs/nls'); import pfs = require('vs/base/node/pfs'); import { TPromise } from 'vs/base/common/winjs.base'; import paths = require('vs/base/common/paths'); -import { IMainProcessExtHostIPC } from 'vs/platform/extensions/common/ipcRemoteCom'; +import { IMainProcessExtHostIPC } from 'vs/platform/extensions/node/ipcRemoteCom'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { ExtHostThreadService } from 'vs/workbench/services/thread/common/extHostThreadService'; import { RemoteTelemetryService } from 'vs/workbench/api/node/extHostTelemetry'; diff --git a/src/vs/workbench/node/extensionHostProcess.ts b/src/vs/workbench/node/extensionHostProcess.ts index 726c7231287..61568a78b5a 100644 --- a/src/vs/workbench/node/extensionHostProcess.ts +++ b/src/vs/workbench/node/extensionHostProcess.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtensionHostMain, exit } from 'vs/workbench/node/extensionHostMain'; -import { create as createIPC, IMainProcessExtHostIPC } from 'vs/platform/extensions/common/ipcRemoteCom'; +import { create as createIPC, IMainProcessExtHostIPC } from 'vs/platform/extensions/node/ipcRemoteCom'; import marshalling = require('vs/base/common/marshalling'); import { createQueuedSender } from 'vs/base/node/processes'; import { IInitData } from 'vs/workbench/api/node/extHost.protocol'; @@ -121,4 +121,4 @@ connectToRenderer().then(renderer => { const extensionHostMain = new ExtensionHostMain(renderer.remoteCom, renderer.initData); onTerminate = () => extensionHostMain.terminate(); return extensionHostMain.start(); -}).done(null, err => console.error(err)); \ No newline at end of file +}).done(null, err => console.error(err)); diff --git a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts index a3181c08f00..87463492734 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts @@ -77,7 +77,7 @@ class RunToCursorAction extends EditorAction { id: 'editor.debug.action.runToCursor', label: nls.localize('runToCursor', "Debug: Run to Cursor"), alias: 'Debug: Run to Cursor', - precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL), + precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, EditorContextKeys.Writable), menuOpts: { group: 'debug', order: 2 diff --git a/src/vs/workbench/parts/debug/browser/media/stop-inverse.svg b/src/vs/workbench/parts/debug/browser/media/stop-inverse.svg index a0e6bcb42d6..97257777312 100644 --- a/src/vs/workbench/parts/debug/browser/media/stop-inverse.svg +++ b/src/vs/workbench/parts/debug/browser/media/stop-inverse.svg @@ -1 +1 @@ - \ No newline at end of file +stop-inverse \ No newline at end of file diff --git a/src/vs/workbench/parts/debug/browser/media/stop.svg b/src/vs/workbench/parts/debug/browser/media/stop.svg index 333812dcdaf..92993bcf5bc 100644 --- a/src/vs/workbench/parts/debug/browser/media/stop.svg +++ b/src/vs/workbench/parts/debug/browser/media/stop.svg @@ -1 +1 @@ - \ No newline at end of file +stop \ No newline at end of file diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 08d757dd580..8a436cc7acc 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -872,8 +872,13 @@ export class Model implements debug.IModel { this.replElements.pop(); } - const newReplElements = typeof output === 'string' ? output.split('\n').map(line => new OutputElement(line, severity)) : [output]; - this.addReplElements(newReplElements); + if (typeof output === 'string') { + this.addReplElements(output.split('\n').map(line => new OutputElement(line, severity))); + } else { + // TODO@Isidor hack, we should introduce a new type which is an output that can fetch children like an expression + (output).severity = severity; + this.addReplElements([output]); + } } this._onDidChangeREPLElements.fire(); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index cd8968328c9..96437ca5b0a 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -323,6 +323,7 @@ export class DebugService implements debug.IDebugService { return; } + const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; if (event.body.category === 'telemetry') { // only log telemetry events from debug adapter if the adapter provided the telemetry key // and the user opted in telemetry @@ -335,11 +336,10 @@ export class DebugService implements debug.IDebugService { children.forEach(child => { // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) child.name = null; - this.model.appendToRepl(child, null); + this.model.appendToRepl(child, outputSeverity); }); }); } else if (typeof event.body.output === 'string') { - const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; this.model.appendToRepl(event.body.output, outputSeverity); } })); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts index 7578afeb9b9..0907f06bf01 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts @@ -293,6 +293,10 @@ export class CallStackController extends BaseDebugController { protected getContext(element: any): any { if (element instanceof StackFrame) { + if (element.source.inMemory) { + return element.source.raw.path || element.source.reference; + } + return element.source.uri.toString(); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index 1f2b1327092..5f23ec77f11 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -191,7 +191,7 @@ export class Repl extends Panel implements IPrivateReplService { if (!e.scrollHeightChanged) { return; } - this.replInputHeight = Math.min(Repl.REPL_INPUT_MAX_HEIGHT, e.scrollHeight, this.dimension.height); + this.replInputHeight = Math.max(Repl.REPL_INPUT_INITIAL_HEIGHT, Math.min(Repl.REPL_INPUT_MAX_HEIGHT, e.scrollHeight, this.dimension.height)); this.layout(this.dimension); })); this.toDispose.push(this.replInput.onDidChangeCursorPosition(e => { diff --git a/src/vs/workbench/parts/emmet/node/editorAccessor.ts b/src/vs/workbench/parts/emmet/node/editorAccessor.ts index aca7276a190..cb2455e2827 100644 --- a/src/vs/workbench/parts/emmet/node/editorAccessor.ts +++ b/src/vs/workbench/parts/emmet/node/editorAccessor.ts @@ -33,7 +33,7 @@ export class EditorAccessor implements emmet.Editor { private _hasMadeEdits: boolean; - private emmetSupportedModes = ['html', 'xhtml', 'css', 'xml', 'xsl', 'haml', 'jade', 'jsx', 'slim', 'scss', 'sass', 'less', 'stylus', 'styl']; + private emmetSupportedModes = ['html', 'css', 'xml', 'xsl', 'haml', 'jade', 'jsx', 'slim', 'scss', 'sass', 'less', 'stylus', 'styl']; constructor(languageIdentifierResolver: ILanguageIdentifierResolver, editor: ICommonCodeEditor, syntaxProfiles: any, excludedLanguages: String[], grammars: IGrammarContributions) { this._languageIdentifierResolver = languageIdentifierResolver; @@ -154,7 +154,7 @@ export class EditorAccessor implements emmet.Editor { // user can overwrite the syntax using the emmet syntaxProfiles setting let profile = this.getSyntaxProfile(syntax); - if (profile) { + if (profile && this.emmetSupportedModes.indexOf(profile) !== -1) { return profile; } diff --git a/src/vs/workbench/parts/emmet/node/emmetActions.ts b/src/vs/workbench/parts/emmet/node/emmetActions.ts index ad08be7bb38..b5cb782dcec 100644 --- a/src/vs/workbench/parts/emmet/node/emmetActions.ts +++ b/src/vs/workbench/parts/emmet/node/emmetActions.ts @@ -104,8 +104,8 @@ class LazyEmmet { private updateEmmetPreferences(configurationService: IConfigurationService, _emmet: typeof emmet): TPromise { let emmetPreferences = configurationService.getConfiguration().emmet; let loadEmmetSettings = () => { - let syntaxProfiles = (Object as any).assign({}, LazyEmmet.syntaxProfilesFromFile, emmetPreferences.syntaxProfiles); - let preferences = (Object as any).assign({}, LazyEmmet.preferencesFromFile, emmetPreferences.preferences); + let syntaxProfiles = { ...LazyEmmet.syntaxProfilesFromFile, ...emmetPreferences.syntaxProfiles }; + let preferences = { ...LazyEmmet.preferencesFromFile, ...emmetPreferences.preferences }; let snippets = LazyEmmet.snippetsFromFile; try { diff --git a/src/vs/workbench/parts/emmet/test/node/editorAccessor.test.ts b/src/vs/workbench/parts/emmet/test/node/editorAccessor.test.ts index 40d024d0bd3..2808b5571f4 100644 --- a/src/vs/workbench/parts/emmet/test/node/editorAccessor.test.ts +++ b/src/vs/workbench/parts/emmet/test/node/editorAccessor.test.ts @@ -65,7 +65,7 @@ suite('Emmet', () => { } // emmet supported languages, null is used as the scopeName since it should not be consulted, they map to to mode to the same syntax name - let emmetSupportedModes = ['html', 'xhtml', 'css', 'xml', 'xsl', 'haml', 'jade', 'jsx', 'slim', 'scss', 'sass', 'less', 'stylus', 'styl']; + let emmetSupportedModes = ['html', 'css', 'xml', 'xsl', 'haml', 'jade', 'jsx', 'slim', 'scss', 'sass', 'less', 'stylus', 'styl']; emmetSupportedModes.forEach(each => { testIsEnabled(each, null); }); @@ -140,7 +140,7 @@ suite('Emmet', () => { } // emmet supported languages, null is used as the scopeName since it should not be consulted, they map to to mode to the same syntax name - let emmetSupportedModes = ['html', 'xhtml', 'css', 'xml', 'xsl', 'haml', 'jade', 'jsx', 'slim', 'scss', 'sass', 'less', 'stylus', 'styl']; + let emmetSupportedModes = ['html', 'css', 'xml', 'xsl', 'haml', 'jade', 'jsx', 'slim', 'scss', 'sass', 'less', 'stylus', 'styl']; emmetSupportedModes.forEach(each => { testSyntax(each, null, each); }); diff --git a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index 6631ce98a28..0c3bece95b8 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -10,7 +10,6 @@ import errors = require('vs/base/common/errors'); import { toErrorMessage } from 'vs/base/common/errorMessage'; import types = require('vs/base/common/types'); import paths = require('vs/base/common/paths'); -import { IEditorOptions } from 'vs/editor/common/editorCommon'; import { Action } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; @@ -207,9 +206,7 @@ export class TextFileEditor extends BaseTextEditor { return true; // in any case we handled it } - protected getConfigurationOverrides(): IEditorOptions { - const options = super.getConfigurationOverrides(); - + protected getAriaLabel(): string { const input = this.input; const inputName = input && input.getName(); @@ -220,9 +217,7 @@ export class TextFileEditor extends BaseTextEditor { ariaLabel = nls.localize('fileEditorAriaLabel', "Text file editor."); } - options.ariaLabel = ariaLabel; - - return options; + return ariaLabel; } public clearInput(): void { diff --git a/src/vs/workbench/parts/git/browser/gitActions.contribution.ts b/src/vs/workbench/parts/git/browser/gitActions.contribution.ts index 62ba2855f7f..6dddf17e7a5 100644 --- a/src/vs/workbench/parts/git/browser/gitActions.contribution.ts +++ b/src/vs/workbench/parts/git/browser/gitActions.contribution.ts @@ -37,6 +37,7 @@ import { import paths = require('vs/base/common/paths'); import URI from 'vs/base/common/uri'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; +import SCMPreview from 'vs/workbench/parts/scm/browser/scmPreview'; function getStatus(gitService: IGitService, contextService: IWorkspaceContextService, input: WorkbenchEditorCommon.IFileEditorInput): IFileStatus { const model = gitService.getModel(); @@ -636,25 +637,27 @@ class GlobalOpenInEditorAction extends OpenFileAction { } } -var actionBarRegistry = platform.Registry.as(abr.Extensions.Actionbar); -actionBarRegistry.registerActionBarContributor(abr.Scope.EDITOR, FileEditorActionContributor); -actionBarRegistry.registerActionBarContributor(abr.Scope.EDITOR, GitEditorActionContributor); -actionBarRegistry.registerActionBarContributor(abr.Scope.EDITOR, GitWorkingTreeDiffEditorActionContributor); +if (!SCMPreview.enabled) { + var actionBarRegistry = platform.Registry.as(abr.Extensions.Actionbar); + actionBarRegistry.registerActionBarContributor(abr.Scope.EDITOR, FileEditorActionContributor); + actionBarRegistry.registerActionBarContributor(abr.Scope.EDITOR, GitEditorActionContributor); + actionBarRegistry.registerActionBarContributor(abr.Scope.EDITOR, GitWorkingTreeDiffEditorActionContributor); -let workbenchActionRegistry = (platform.Registry.as(wbar.Extensions.WorkbenchActions)); + let workbenchActionRegistry = (platform.Registry.as(wbar.Extensions.WorkbenchActions)); -// Register Actions -const category = nls.localize('git', "Git"); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(GlobalOpenChangeAction, GlobalOpenChangeAction.ID, GlobalOpenChangeAction.LABEL), 'Git: Open Change', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(GlobalOpenInEditorAction, GlobalOpenInEditorAction.ID, GlobalOpenInEditorAction.LABEL), 'Git: Open File', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PullAction, PullAction.ID, PullAction.LABEL), 'Git: Pull', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PushAction, PushAction.ID, PushAction.LABEL), 'Git: Push', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PushToRemoteAction, PushToRemoteAction.ID, PushToRemoteAction.LABEL), 'Git: Push to...', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SyncAction, SyncAction.ID, SyncAction.LABEL), 'Git: Sync', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PublishAction, PublishAction.ID, PublishAction.LABEL), 'Git: Publish', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(StartGitBranchAction, StartGitBranchAction.ID, StartGitBranchAction.LABEL), 'Git: Branch', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(StartGitCheckoutAction, StartGitCheckoutAction.ID, StartGitCheckoutAction.LABEL), 'Git: Checkout', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(InputCommitAction, InputCommitAction.ID, InputCommitAction.LABEL), 'Git: Commit', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(UndoLastCommitAction, UndoLastCommitAction.ID, UndoLastCommitAction.LABEL), 'Git: Undo Last Commit', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(WorkbenchStageAction, WorkbenchStageAction.ID, WorkbenchStageAction.LABEL), 'Git: Stage', category); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(WorkbenchUnstageAction, WorkbenchUnstageAction.ID, WorkbenchUnstageAction.LABEL), 'Git: Unstage', category); + // Register Actions + const category = nls.localize('git', "Git"); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(GlobalOpenChangeAction, GlobalOpenChangeAction.ID, GlobalOpenChangeAction.LABEL), 'Git: Open Change', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(GlobalOpenInEditorAction, GlobalOpenInEditorAction.ID, GlobalOpenInEditorAction.LABEL), 'Git: Open File', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PullAction, PullAction.ID, PullAction.LABEL), 'Git: Pull', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PushAction, PushAction.ID, PushAction.LABEL), 'Git: Push', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PushToRemoteAction, PushToRemoteAction.ID, PushToRemoteAction.LABEL), 'Git: Push to...', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SyncAction, SyncAction.ID, SyncAction.LABEL), 'Git: Sync', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(PublishAction, PublishAction.ID, PublishAction.LABEL), 'Git: Publish', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(StartGitBranchAction, StartGitBranchAction.ID, StartGitBranchAction.LABEL), 'Git: Branch', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(StartGitCheckoutAction, StartGitCheckoutAction.ID, StartGitCheckoutAction.LABEL), 'Git: Checkout', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(InputCommitAction, InputCommitAction.ID, InputCommitAction.LABEL), 'Git: Commit', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(UndoLastCommitAction, UndoLastCommitAction.ID, UndoLastCommitAction.LABEL), 'Git: Undo Last Commit', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(WorkbenchStageAction, WorkbenchStageAction.ID, WorkbenchStageAction.LABEL), 'Git: Stage', category); + workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(WorkbenchUnstageAction, WorkbenchUnstageAction.ID, WorkbenchUnstageAction.LABEL), 'Git: Unstage', category); +} diff --git a/src/vs/workbench/parts/git/browser/gitEditorContributions.ts b/src/vs/workbench/parts/git/browser/gitEditorContributions.ts index 21dcfbb404c..53478c47f05 100644 --- a/src/vs/workbench/parts/git/browser/gitEditorContributions.ts +++ b/src/vs/workbench/parts/git/browser/gitEditorContributions.ts @@ -11,6 +11,8 @@ import { IGitService, ModelEvents, StatusType } from 'vs/workbench/parts/git/com import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Disposable } from 'vs/base/common/lifecycle'; import { Delayer } from 'vs/base/common/async'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import SCMPreview from 'vs/workbench/parts/scm/browser/scmPreview'; const pattern = /^<<<<<<<|^=======|^>>>>>>>/; @@ -76,7 +78,6 @@ class MergeDecoratorBoundToModel extends Disposable { } } -@editorContribution export class MergeDecorator extends Disposable implements IEditorContribution { static ID = 'vs.git.editor.merge.decorator'; @@ -146,3 +147,35 @@ export class MergeDecorator extends Disposable implements IEditorContribution { super.dispose(); } } + +// TODO@Joao: remove +@editorContribution +export class MergeDecoratorWrapper extends Disposable implements IEditorContribution { + + static ID = 'vs.git.editor.merge.decoratorwrapper'; + private decorator: MergeDecorator; + + constructor( + private editor: ICodeEditor, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(); + + if (SCMPreview.enabled) { + return; + } + + this.decorator = instantiationService.createInstance(MergeDecorator, editor); + } + + getId(): string { + return MergeDecoratorWrapper.ID; + } + + dispose(): void { + if (this.decorator) { + this.decorator.dispose(); + this.decorator = null; + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/git/browser/gitScm.ts b/src/vs/workbench/parts/git/browser/gitScm.ts new file mode 100644 index 00000000000..ff319530023 --- /dev/null +++ b/src/vs/workbench/parts/git/browser/gitScm.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import Event, { Emitter } from 'vs/base/common/event'; +import { dispose } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { IModel } from 'vs/editor/common/editorCommon'; +import { ISCMService, ISCMProvider, ISCMResource, ISCMResourceGroup } from 'vs/workbench/services/scm/common/scm'; +import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { Throttler } from 'vs/base/common/async'; +import * as paths from 'vs/base/common/paths'; +import { IGitService, StatusType, ServiceEvents, ServiceOperations, ServiceState } from 'vs/workbench/parts/git/common/git'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; + +// TODO@Joao: remove +export class GitSCMProvider implements IWorkbenchContribution, ISCMProvider, ITextModelContentProvider { + + get id() { return 'git-internal'; } + get label() { return 'Git'; } + get resources() { return []; } + + private _onDidChange = new Emitter(); + get onDidChange(): Event { + return this._onDidChange.event; + } + + constructor( + @ITextModelResolverService textModelResolverService: ITextModelResolverService, + @IModelService private modelService: IModelService, + @IGitService private gitService: IGitService, + @ISCMService scmService: ISCMService + ) { + scmService.registerSCMProvider(this); + textModelResolverService.registerTextModelContentProvider('git', this); + } + + getId(): string { + return 'git.contentprovider'; + } + + commit(message: string): TPromise { + return TPromise.wrapError('not implemented'); + } + + open(uri: ISCMResource): TPromise { + return TPromise.wrapError('not implemented'); + } + + drag(from: ISCMResource, to: ISCMResourceGroup): TPromise { + return TPromise.wrapError('not implemented'); + } + + getOriginalResource(uri: URI): TPromise { + if (uri.scheme !== 'file') { + return TPromise.as(null); + } + + return TPromise.as(uri.with({ scheme: 'git' })); + } + + provideTextContent(uri: URI): TPromise { + const model = this.modelService.createModel('', null, uri); + const throttler = new Throttler(); + + const setModelContents = contents => { + if (model.isDisposed()) { + return; + } + + model.setValue(contents || ''); + }; + + const updateModel = () => { + const gitModel = this.gitService.getModel(); + const root = gitModel.getRepositoryRoot(); + + if (!root) { + return TPromise.as(null); + } + + const path = uri.fsPath; + const relativePath = paths.relative(root, path).replace(/\\/g, '/'); + + if (/^\.\./.test(relativePath)) { + return TPromise.as(null); + } + + const treeish = gitModel.getStatus().find(relativePath, StatusType.INDEX) ? '~' : 'HEAD'; + + return this.gitService.buffer(path, treeish).then(setModelContents); + }; + + const triggerModelUpdate = () => { + if (this.gitService.getState() !== ServiceState.OK) { + return; + } + + throttler.queue(updateModel); + }; + + const disposables = [ + this.gitService.addListener2(ServiceEvents.STATE_CHANGED, triggerModelUpdate), + this.gitService.addListener2(ServiceEvents.OPERATION_END, e => { + if (e.operation.id !== ServiceOperations.BACKGROUND_FETCH) { + triggerModelUpdate(); + } + }) + ]; + + model.onWillDispose(() => dispose(disposables)); + triggerModelUpdate(); + + return TPromise.as(model); + } + + dispose(): void { + + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts b/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts index 73432ad2648..4d8370c5d42 100644 --- a/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts +++ b/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts @@ -28,6 +28,7 @@ import { IMessageService } from 'vs/platform/message/common/message'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { GitSCMProvider } from './gitScm'; import IGitService = git.IGitService; @@ -164,6 +165,11 @@ export function registerContributions(): void { StatusUpdater ); + // Register GitSCMProvider + (platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution( + GitSCMProvider + ); + // Register Quick Open for git (platform.Registry.as(quickopen.Extensions.Quickopen)).registerQuickOpenHandler( new quickopen.QuickOpenHandlerDescriptor( diff --git a/src/vs/workbench/parts/git/electron-browser/git.contribution.ts b/src/vs/workbench/parts/git/electron-browser/git.contribution.ts index c4490b40a98..f85747bf668 100644 --- a/src/vs/workbench/parts/git/electron-browser/git.contribution.ts +++ b/src/vs/workbench/parts/git/electron-browser/git.contribution.ts @@ -12,13 +12,16 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/platform'; import { CloneAction } from './gitActions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actionRegistry'; +import SCMPreview from 'vs/workbench/parts/scm/browser/scmPreview'; -registerContributions(); +if (!SCMPreview.enabled) { + registerContributions(); -// Register Service -registerSingleton(IGitService, ElectronGitService); + // Register Service + registerSingleton(IGitService, ElectronGitService); -const category = localize('git', "Git"); + const category = localize('git', "Git"); -Registry.as(WorkbenchActionExtensions.WorkbenchActions) - .registerWorkbenchAction(new SyncActionDescriptor(CloneAction, CloneAction.ID, CloneAction.LABEL), 'Git: Clone', category); + Registry.as(WorkbenchActionExtensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(CloneAction, CloneAction.ID, CloneAction.LABEL), 'Git: Clone', category); +} diff --git a/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts b/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts index d058b874964..12dd118f7c4 100644 --- a/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts +++ b/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts @@ -37,7 +37,7 @@ export function registerContributions(): void { Constants.MARKERS_PANEL_ID, Messages.MARKERS_PANEL_TITLE_PROBLEMS, 'markersPanel', - 20, + 10, ToggleMarkersPanelAction.ID )); diff --git a/src/vs/workbench/parts/output/browser/output.contribution.ts b/src/vs/workbench/parts/output/browser/output.contribution.ts index a02939d7230..44c12b21d80 100644 --- a/src/vs/workbench/parts/output/browser/output.contribution.ts +++ b/src/vs/workbench/parts/output/browser/output.contribution.ts @@ -15,9 +15,8 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry'; import { OutputService } from 'vs/workbench/parts/output/browser/outputServices'; import { ToggleOutputAction, ClearOutputAction } from 'vs/workbench/parts/output/browser/outputActions'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_PANEL_ID, IOutputService } from 'vs/workbench/parts/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_PANEL_ID, IOutputService, CONTEXT_IN_OUTPUT } from 'vs/workbench/parts/output/common/output'; import { PanelRegistry, Extensions, PanelDescriptor } from 'vs/workbench/browser/panel'; -import { EditorContextKeys } from 'vs/editor/common/editorCommon'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -39,7 +38,7 @@ Registry.as(Extensions.Panels).registerPanel(new PanelDescriptor( OUTPUT_PANEL_ID, nls.localize('output', "Output"), 'output', - 10, + 20, ToggleOutputAction.ID )); @@ -125,7 +124,7 @@ registerAction({ title: nls.localize('clearOutput.label', "Clear Output"), menu: { menuId: MenuId.EditorContext, - when: EditorContextKeys.LanguageId.isEqualTo(OUTPUT_MODE_ID) + when: CONTEXT_IN_OUTPUT }, handler(accessor) { accessor.get(IOutputService).getActiveChannel().clear(); diff --git a/src/vs/workbench/parts/output/browser/outputPanel.ts b/src/vs/workbench/parts/output/browser/outputPanel.ts index ae266298cd0..4e14292ff6a 100644 --- a/src/vs/workbench/parts/output/browser/outputPanel.ts +++ b/src/vs/workbench/parts/output/browser/outputPanel.ts @@ -86,12 +86,15 @@ export class OutputPanel extends TextResourceEditor { options.scrollBeyondLastLine = false; options.renderLineHighlight = 'none'; - const channel = this.outputService.getActiveChannel(); - options.ariaLabel = channel ? nls.localize('outputPanelWithInputAriaLabel', "{0}, Output panel", channel.label) : nls.localize('outputPanelAriaLabel', "Output panel"); - return options; } + protected getAriaLabel(): string { + const channel = this.outputService.getActiveChannel(); + + return channel ? nls.localize('outputPanelWithInputAriaLabel', "{0}, Output panel", channel.label) : nls.localize('outputPanelAriaLabel', "Output panel"); + } + public setInput(input: EditorInput, options?: EditorOptions): TPromise { return super.setInput(input, options).then(() => this.revealLastLine()); } diff --git a/src/vs/workbench/parts/preferences/browser/media/preferences.css b/src/vs/workbench/parts/preferences/browser/media/preferences.css index 234f5b5cc96..c61f664d768 100644 --- a/src/vs/workbench/parts/preferences/browser/media/preferences.css +++ b/src/vs/workbench/parts/preferences/browser/media/preferences.css @@ -25,7 +25,6 @@ .settings-tabs-widget > .settings-tab { text-transform: uppercase; font-size: 12px; - font-weight: bold; margin-left: 20px; padding-top: 6px; cursor: pointer; @@ -40,6 +39,47 @@ cursor: default; } +.vs .settings-tabs-widget > .settings-tab { + color: #6f6f6f; +} + +.vs-dark .settings-tabs-widget > .settings-tab { + color: #bbbbbb; +} + +.hc-black .settings-tabs-widget > .settings-tab { + color: #fff; +} + +.vs-dark .settings-tabs-widget > .settings-tab:not(.disabled):hover { + color: white; +} + +.vs .settings-tabs-widget > .settings-tab.active { + border-bottom: 1px solid #6f6f6f; +} + +.vs-dark .settings-tabs-widget > .settings-tab.active { + color: white; + border-bottom: 1px solid white; +} + +.hc-black .settings-tabs-widget > .settings-tab.active { + color: white; + border-bottom: 1px solid white; +} + +.vs .settings-tabs-widget > .settings-tab:focus, +.vs-dark .settings-tabs-widget > .settings-tab:focus { + border-bottom: 1px solid #007ACC; + outline: none !important; +} + +.hc-black .settings-tabs-widget > .settings-tab:focus { + border-bottom: 1px solid #f38518; + outline: none !important; +} + .preferences-header > .settings-header-widget { flex: 1; display: flex; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 0323e7f7cd2..85ac2ed1d43 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -152,6 +152,11 @@ export class PreferencesEditor extends BaseEditor { this.searchWidget.focus(); } + public clearInput(): void { + this.sideBySidePreferencesWidget.clearInput(); + super.clearInput(); + } + private updateInput(oldInput: PreferencesEditorInput, newInput: PreferencesEditorInput, options?: EditorOptions): TPromise { const editablePreferencesUri = toResource(newInput.master); this.settingsTabsWidget.show(editablePreferencesUri.toString() === this.preferencesService.userSettingsResource.toString() ? ConfigurationTarget.USER : ConfigurationTarget.WORKSPACE); @@ -169,6 +174,10 @@ export class PreferencesEditor extends BaseEditor { } private switchSettings(): void { + // Focus the editor if this editor is not active editor + if (this.editorService.getActiveEditor() !== this) { + this.focus(); + } const promise = this.input.isDirty() ? this.input.save() : TPromise.as(true); promise.done(value => this.preferencesService.switchSettings()); } @@ -273,6 +282,7 @@ export class SideBySidePreferencesWidget extends Widget { this.defaultPreferencesEditorContainer.style.position = 'absolute'; this.defaultPreferencesEditor = this.instantiationService.createInstance(DefaultPreferencesEditor); this.defaultPreferencesEditor.create(new Builder(this.defaultPreferencesEditorContainer)); + this.defaultPreferencesEditor.setVisible(true); this.editablePreferencesEditorContainer = DOM.append(parentElement, DOM.$('.editable-preferences-editor-container')); this.editablePreferencesEditorContainer.style.position = 'absolute'; @@ -301,6 +311,12 @@ export class SideBySidePreferencesWidget extends Widget { return this.defaultPreferencesEditor; } + public clearInput(): void { + if (this.editablePreferencesEditor) { + this.editablePreferencesEditor.clearInput(); + } + } + private getOrCreateEditablePreferencesEditor(editorInput: EditorInput): TPromise { if (this.editablePreferencesEditor) { return TPromise.as(this.editablePreferencesEditor); @@ -413,6 +429,10 @@ export class DefaultPreferencesEditor extends BaseTextEditor { this.getControl().setModel(null); super.clearInput(); } + + protected getAriaLabel(): string { + return nls.localize('preferencesAriaLabel', "Default preferences. Readonly text editor."); + } } class DefaultPreferencesCodeEditor extends CodeEditor { diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index 5cbe4b633d0..02773a2201b 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -142,7 +142,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic } switchSettings(): TPromise { - const activeEditorInput = this.editorService.getActiveEditorInput(); + const activeEditor = this.editorService.getActiveEditor(); + const activeEditorInput = activeEditor.input; if (activeEditorInput instanceof PreferencesEditorInput) { const fromTarget = this.getSettingsConfigurationTarget(activeEditorInput); const toTarget = ConfigurationTarget.USER === fromTarget ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; @@ -152,7 +153,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.replaceEditors([{ toReplace: this.lastOpenedSettingsInput, replaceWith - }]).then(() => { + }], activeEditor.position).then(() => { this.lastOpenedSettingsInput = replaceWith; }); }); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 4c5e4e1b579..731676c4fbf 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -58,7 +58,7 @@ export class SettingsGroupTitleWidget extends Widget implements IViewZone { this._domNode = DOM.$('.settings-group-title-widget'); this.titleContainer = DOM.append(this._domNode, DOM.$('.title-container')); - this.titleContainer.tabIndex = 1; + this.titleContainer.tabIndex = 0; this.onclick(this.titleContainer, () => this.toggle()); this.onkeydown(this.titleContainer, (e) => this.onKeyDown(e)); const focusTracker = this._register(DOM.trackFocus(this.titleContainer)); @@ -193,15 +193,19 @@ export class SettingsTabsWidget extends Widget { private create(parent: HTMLElement): void { const settingsTabsWidget = DOM.append(parent, DOM.$('.settings-tabs-widget')); this.userSettingsTab = DOM.append(settingsTabsWidget, DOM.$('.settings-tab')); - DOM.append(this.userSettingsTab, DOM.$('')).textContent = localize('userSettings', "User Settings"); + this.userSettingsTab.tabIndex = 0; + this.userSettingsTab.textContent = localize('userSettings', "User Settings"); this.onclick(this.userSettingsTab, () => this.onClick(this.userSettingsTab)); + this.onkeyup(this.userSettingsTab, (e) => this.onkeyUp(e, this.userSettingsTab)); this.workspaceSettingsTab = DOM.append(settingsTabsWidget, DOM.$('.settings-tab')); - DOM.append(this.workspaceSettingsTab, DOM.$('')).textContent = localize('workspaceSettings', "Workspace Settings"); + this.workspaceSettingsTab.tabIndex = 0; + this.workspaceSettingsTab.textContent = localize('workspaceSettings', "Workspace Settings"); if (!this.contextService.hasWorkspace()) { DOM.addClass(this.workspaceSettingsTab, 'disabled'); } else { this.onclick(this.workspaceSettingsTab, () => this.onClick(this.workspaceSettingsTab)); + this.onkeyup(this.workspaceSettingsTab, (e) => this.onkeyUp(e, this.workspaceSettingsTab)); } } @@ -210,6 +214,12 @@ export class SettingsTabsWidget extends Widget { DOM.toggleClass(this.workspaceSettingsTab, 'active', ConfigurationTarget.WORKSPACE === configurationTarget); } + private onkeyUp(keyboardEvent: IKeyboardEvent, element: HTMLElement): void { + if (keyboardEvent.keyCode === KeyCode.Enter || keyboardEvent.keyCode === KeyCode.Space) { + this.onClick(element); + } + } + private onClick(element: HTMLElement): void { if (!DOM.hasClass(element, 'active')) { this._onSwitch.fire(); diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index 89eb8583527..24e61fe659c 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -123,19 +123,17 @@ export abstract class AbstractSettingsModel extends Disposable { keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } - if (schema.type === 'string' || schema.enum) { - const valueMatches = matchesContiguousSubString(word, setting.value); - if (valueMatches) { - valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); - } else if (schema.enum && schema.enum.some(enumValue => !!matchesContiguousSubString(word, enumValue))) { - valueMatchingWords.set(word, []); - } + const valueMatches = typeof setting.value === 'string' ? matchesContiguousSubString(word, setting.value) : null; + if (valueMatches) { + valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); + } else if (schema.enum && schema.enum.some(enumValue => typeof enumValue === 'string' && !!matchesContiguousSubString(word, enumValue))) { + valueMatchingWords.set(word, []); } } const descriptionRanges: IRange[] = []; for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { - const matches = or(matchesContiguousSubString)(searchString, setting.description[lineIndex]) || []; + const matches = or(matchesContiguousSubString)(searchString, setting.description[lineIndex] || '') || []; descriptionRanges.push(...matches.map(match => this.toDescriptionRange(setting, match, lineIndex))); } if (descriptionRanges.length === 0) { @@ -146,7 +144,7 @@ export abstract class AbstractSettingsModel extends Disposable { const keyRanges: IRange[] = keyMatches ? keyMatches.map(match => this.toKeyRange(setting, match)) : this.getRangesForWords(words, keyMatchingWords, [descriptionMatchingWords, valueMatchingWords]); let valueRanges: IRange[] = []; - if (typeof setting.value === 'string') { + if (setting.value && typeof setting.value === 'string') { const valueMatches = or(matchesPrefix, matchesContiguousSubString)(searchString, setting.value); valueRanges = valueMatches ? valueMatches.map(match => this.toValueRange(setting, match)) : this.getRangesForWords(words, valueMatchingWords, [keyMatchingWords, descriptionMatchingWords]); } diff --git a/src/vs/workbench/parts/quickopen/browser/helpHandler.ts b/src/vs/workbench/parts/quickopen/browser/helpHandler.ts index 539f5cf8c26..afe1a07de19 100644 --- a/src/vs/workbench/parts/quickopen/browser/helpHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/helpHandler.ts @@ -68,39 +68,42 @@ class HelpEntry extends QuickOpenEntryItem { public render(tree: ITree, container: HTMLElement, previousCleanupFn: IElementCallback): IElementCallback { let builder = $(container); - builder.addClass('quick-open-entry'); - // Support border - if (this.showBorder()) { - $(container).addClass('results-group-separator'); - } else { - $(container).removeClass('results-group-separator'); - } + builder.div({ class: 'quick-open-entry' }, builder => { + // Support border + if (this.showBorder()) { + $(container).addClass('results-group-separator'); + } else { + $(container).removeClass('results-group-separator'); + } - // Add a container for the group - if (this.getGroupLabel()) { - $(container).div((div: Builder) => { - div.addClass('results-group'); - div.attr({ - text: this.getGroupLabel() + // Add a container for the group + if (this.getGroupLabel()) { + $(container).div((div: Builder) => { + div.addClass('results-group'); + div.attr({ + text: this.getGroupLabel() + }); + }); + } + + builder.div({ class: 'row' }, builder => { + // Prefix + let label = builder.clone().div({ + text: this.prefix, + 'class': 'quick-open-help-entry-label' + }); + + if (!this.prefix) { + label.text('\u2026'); + } + + // Description + builder.span({ + text: this.description, + 'class': 'quick-open-entry-description' }); }); - } - - // Prefix - let label = builder.clone().div({ - text: this.prefix, - 'class': 'quick-open-help-entry-label' - }); - - if (!this.prefix) { - label.text('\u2026'); - } - - // Description - builder.span({ - text: this.description, - 'class': 'quick-open-entry-description' }); return null; diff --git a/src/vs/workbench/parts/scm/browser/scm.contribution.ts b/src/vs/workbench/parts/scm/browser/scm.contribution.ts index 9e8ebb7ed17..959d1d1dfbe 100644 --- a/src/vs/workbench/parts/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/parts/scm/browser/scm.contribution.ts @@ -6,35 +6,22 @@ 'use strict'; import { localize } from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Action } from 'vs/base/common/actions'; import { Registry } from 'vs/platform/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffDecorator } from './dirtydiffDecorator'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ToggleViewletAction } from 'vs/workbench/browser/viewlet'; import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actionRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { ISCMService } from 'vs/workbench/services/scm/common/scm'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StatusUpdater } from './scmActivity'; - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(DirtyDiffDecorator); - -const viewletDescriptor = new ViewletDescriptor( - 'vs/workbench/parts/scm/browser/scmViewlet', - 'SCMViewlet', - VIEWLET_ID, - localize('scm', "SCM"), - 'scm', - 36 -); - -Registry.as(ViewletExtensions.Viewlets) - .registerViewlet(viewletDescriptor); - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(StatusUpdater); +import SCMPreview, { DisableSCMPreviewAction, EnableSCMPreviewAction } from 'vs/workbench/parts/scm/browser/scmPreview'; class OpenSCMViewletAction extends ToggleViewletAction { @@ -46,14 +33,69 @@ class OpenSCMViewletAction extends ToggleViewletAction { } } -// Register Action to Open Viewlet -Registry.as(WorkbenchActionExtensions.WorkbenchActions).registerWorkbenchAction( - new SyncActionDescriptor(OpenSCMViewletAction, VIEWLET_ID, localize('toggleSCMViewlet', "Show SCM"), { - primary: null, - win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G } - }), - 'View: Show SCM', - localize('view', "View") -); \ No newline at end of file +// TODO@Joao +export class SwitchProvider extends Action { + + static readonly ID = 'scm.switch'; + static readonly LABEL = 'Switch SCM Provider'; + + constructor( + id = SwitchProvider.ID, + label = SwitchProvider.LABEL, + @ISCMService private scmService: ISCMService, + @IQuickOpenService private quickOpenService: IQuickOpenService + ) { + super('scm.switchprovider', 'Switch SCM Provider', '', true); + } + + run(): TPromise { + const picks = this.scmService.providers.map(provider => ({ + id: provider.id, + label: provider.label, + run: () => this.scmService.activeProvider = provider + })); + + return this.quickOpenService.pick(picks); + } +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(DirtyDiffDecorator); + +if (SCMPreview.enabled) { + const viewletDescriptor = new ViewletDescriptor( + 'vs/workbench/parts/scm/browser/scmViewlet', + 'SCMViewlet', + VIEWLET_ID, + localize('scm', "SCM"), + 'scm', + 36 + ); + + Registry.as(ViewletExtensions.Viewlets) + .registerViewlet(viewletDescriptor); + + Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(StatusUpdater); + + // Register Action to Open Viewlet + Registry.as(WorkbenchActionExtensions.WorkbenchActions).registerWorkbenchAction( + new SyncActionDescriptor(OpenSCMViewletAction, VIEWLET_ID, localize('toggleSCMViewlet', "Show SCM"), { + primary: null, + win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G } + }), + 'View: Show SCM', + localize('view', "View") + ); + + Registry.as(WorkbenchActionExtensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(SwitchProvider, SwitchProvider.ID, SwitchProvider.LABEL), 'SCM: Switch Provider', 'SCM'); + + Registry.as(WorkbenchActionExtensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(DisableSCMPreviewAction, DisableSCMPreviewAction.ID, DisableSCMPreviewAction.LABEL), 'SCM: Disable Preview SCM', 'SCM'); +} else { + Registry.as(WorkbenchActionExtensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(EnableSCMPreviewAction, EnableSCMPreviewAction.ID, EnableSCMPreviewAction.LABEL), 'SCM: Enable Preview SCM', 'SCM'); +} diff --git a/src/vs/workbench/parts/scm/browser/scmPreview.ts b/src/vs/workbench/parts/scm/browser/scmPreview.ts new file mode 100644 index 00000000000..b7a11093b37 --- /dev/null +++ b/src/vs/workbench/parts/scm/browser/scmPreview.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Action } from 'vs/base/common/actions'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IMessageService } from 'vs/platform/message/common/message'; + +export default class SCMPreview { + + private static readonly _enabled = window.localStorage.getItem('enablePreviewSCM') === 'true'; + + static get enabled(): boolean { + return this._enabled; + } + + static set enabled(enabled: boolean) { + window.localStorage.setItem('enablePreviewSCM', enabled ? 'true' : 'false'); + } +} + +export class EnableSCMPreviewAction extends Action { + + static ID = 'enablescmpreview'; + static LABEL = 'Enable Preview SCM'; + + constructor( + id = EnableSCMPreviewAction.ID, + label = EnableSCMPreviewAction.LABEL, + @IWindowService private windowService: IWindowService, + @IMessageService private messageService: IMessageService, + ) { + super(EnableSCMPreviewAction.ID, EnableSCMPreviewAction.LABEL, '', true); + } + + run(): TPromise { + const message = 'This will reload this window, do you want to continue?'; + const result = this.messageService.confirm({ message }); + + if (!result) { + return; + } + + SCMPreview.enabled = true; + return this.windowService.reloadWindow(); + } +} + +export class DisableSCMPreviewAction extends Action { + + static ID = 'disablescmpreview'; + static LABEL = 'Disable Preview SCM'; + + constructor( + id = DisableSCMPreviewAction.ID, + label = DisableSCMPreviewAction.LABEL, + @IWindowService private windowService: IWindowService, + @IMessageService private messageService: IMessageService, + ) { + super(DisableSCMPreviewAction.ID, DisableSCMPreviewAction.LABEL, '', true); + } + + run(): TPromise { + const message = 'This will reload this window, do you want to continue?'; + const result = this.messageService.confirm({ message }); + + if (!result) { + return; + } + + SCMPreview.enabled = false; + return this.windowService.reloadWindow(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/browser/scmViewlet.ts b/src/vs/workbench/parts/scm/browser/scmViewlet.ts index 059a0e30d2d..1008d1152b7 100644 --- a/src/vs/workbench/parts/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/browser/scmViewlet.ts @@ -39,22 +39,6 @@ import { ActionBar, IActionItemProvider } from 'vs/base/browser/ui/actionbar/act import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; import { isDarkTheme } from 'vs/platform/theme/common/themes'; -function isSCMResource(element: ISCMResource | ISCMResourceGroup): element is ISCMResource { - return !!(element as ISCMResource).uri; -} - -function equalsSCMResource(one: ISCMResource, other: ISCMResource): boolean { - if (one.resourceGroupId !== other.resourceGroupId) { - return false; - } - - if (one.uri.toString() !== other.uri.toString()) { - return false; - } - - return true; -} - interface SearchInputEvent extends Event { target: HTMLInputElement; immediate?: boolean; @@ -157,7 +141,7 @@ class Delegate implements IDelegate { getHeight() { return 22; } getTemplateId(element: ISCMResourceGroup | ISCMResource) { - return isSCMResource(element) ? ResourceRenderer.TEMPLATE_ID : ResourceGroupRenderer.TEMPLATE_ID; + return (element as ISCMResource).uri ? ResourceRenderer.TEMPLATE_ID : ResourceGroupRenderer.TEMPLATE_ID; } } @@ -218,8 +202,6 @@ export class SCMViewlet extends Viewlet { private list: List; private menus: SCMMenus; private providerChangeDisposable: IDisposable = EmptyDisposable; - private currentFocus: (ISCMResource | ISCMResourceGroup)[] = []; - private currentSelection: (ISCMResource | ISCMResourceGroup)[] = []; private disposables: IDisposable[] = []; constructor( @@ -285,12 +267,9 @@ export class SCMViewlet extends Viewlet { this.instantiationService.createInstance(ResourceRenderer, this.menus, actionItemProvider) ]); - this.list.onSelectionChange(e => this.currentSelection = e.elements, null, this.disposables); - this.list.onFocusChange(e => this.currentFocus = e.elements, null, this.disposables); - chain(this.list.onSelectionChange) .map(e => e.elements[0]) - .filter(e => !!e && isSCMResource(e)) + .filter(e => !!e && !!(e as ISCMResource).uri) .on(this.open, this, this.disposables); this.list.onContextMenu(this.onListContextMenu, this, this.disposables); @@ -311,34 +290,11 @@ export class SCMViewlet extends Viewlet { return; } + const elements = provider.resources .reduce<(ISCMResourceGroup | ISCMResource)[]>((r, g) => [...r, g, ...g.resources], []); - // TODO@Joao: move this behaviour down to the List - const previousSelection = this.currentSelection.filter(e => isSCMResource(e)) as ISCMResource[]; - const previousFocus = this.currentFocus.filter(e => isSCMResource(e)) as ISCMResource[]; - const selection: number[] = []; - const focus: number[] = []; - - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - - if (!isSCMResource(element)) { - continue; - } - - if (previousSelection.some(s => equalsSCMResource(s, element))) { - selection.push(i); - } - - if (previousFocus.some(s => equalsSCMResource(s, element))) { - focus.push(i); - } - } - this.list.splice(0, this.list.length, elements); - this.list.setSelection(selection); - this.list.setFocus(focus); } layout(dimension: Dimension = this.cachedDimension): void { @@ -395,10 +351,12 @@ export class SCMViewlet extends Viewlet { const element = e.element; let actions: IAction[]; - if (isSCMResource(element)) { - actions = this.menus.getResourceContextActions(element); + if ((element as ISCMResource).uri) { + const resource = element as ISCMResource; + actions = this.menus.getResourceContextActions(resource); } else { - actions = this.menus.getResourceGroupContextActions(element); + const resourceGroup = element as ISCMResourceGroup; + actions = this.menus.getResourceGroupContextActions(resourceGroup); } this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/parts/tasks/common/taskSystem.ts b/src/vs/workbench/parts/tasks/common/taskSystem.ts index 186e190fbe8..0cf6b3c9fdc 100644 --- a/src/vs/workbench/parts/tasks/common/taskSystem.ts +++ b/src/vs/workbench/parts/tasks/common/taskSystem.ts @@ -71,6 +71,52 @@ export namespace ShowOutput { } } +export interface CommandOptions { + /** + * The current working directory of the executed program or shell. + * If omitted VSCode's current workspace root is used. + */ + cwd?: string; + + /** + * The environment of the executed program or shell. If omitted + * the parent process' environment is used. + */ + env?: { [key: string]: string; }; +} + +export interface CommandConfiguration { + /** + * The command to execute + */ + name?: string; + + /** + * Whether the command is a shell command or not + */ + isShellCommand?: boolean; + + /** + * Additional command options. + */ + options?: CommandOptions; + + /** + * Command arguments. + */ + args?: string[]; + + /** + * The task selector if needed. + */ + taskSelector?: string; + + /** + * Controls whether the executed command is printed to the output windows as well. + */ + echo?: boolean; +} + /** * A task description */ @@ -86,6 +132,11 @@ export interface TaskDescription { */ name: string; + /** + * The command configuration + */ + command: CommandConfiguration; + /** * Suppresses the task name when calling the task using the task runner. */ @@ -113,56 +164,25 @@ export interface TaskDescription { */ showOutput: ShowOutput; - /** - * Controls whether the executed command is printed to the output windows as well. - */ - echoCommand?: boolean; - /** * The problem watchers to use for this task */ problemMatchers?: ProblemMatcher[]; } -export interface CommandOptions { - /** - * The current working directory of the executed program or shell. - * If omitted VSCode's current workspace root is used. - */ - cwd?: string; - - /** - * The environment of the executed program or shell. If omitted - * the parent process' environment is used. - */ - env?: { [key: string]: string; }; -} - - /** * Describs the settings of a task runner */ -export interface BaseTaskRunnerConfiguration { +export interface TaskRunnerConfiguration { + /** + * The inferred build tasks + */ + buildTasks: string[]; /** - * The command to execute + * The inferred test tasks; */ - command?: string; - - /** - * Whether the task is a shell command or not - */ - isShellCommand?: boolean; - - /** - * Additional command options - */ - options?: CommandOptions; - - /** - * General args - */ - args?: string[]; + testTasks: string[]; /** * The configured tasks @@ -170,17 +190,6 @@ export interface BaseTaskRunnerConfiguration { tasks?: { [id: string]: TaskDescription; }; } -/** - * Describs the settings of a task runner - */ -export interface TaskRunnerConfiguration extends BaseTaskRunnerConfiguration { - - /** - * The command to execute. Not optional. - */ - command: string; -} - export interface ITaskSummary { /** * Exit code of the process. diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index 282718bc334..6aacaa490b3 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -669,6 +669,10 @@ class TaskService extends EventEmitter implements ITaskService { lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown())); } + public log(value: string): void { + this.outputChannel.append(value + '\n'); + } + private disposeTaskSystemListeners(): void { this.taskSystemListeners = dispose(this.taskSystemListeners); } @@ -744,11 +748,15 @@ class TaskService extends EventEmitter implements ITaskService { throw new TaskError(Severity.Info, nls.localize('TaskSystem.noConfiguration', 'No task runner configured.'), TaskErrors.NotConfigured); } let result: ITaskSystem = null; + let parseResult = FileConfig.parse(config, this); + if (parseResult.validationStatus.isFatal()) { + throw new TaskError(Severity.Error, nls.localize('TaskSystem.fatalError', 'The provided task configuration has validation errors. See tasks output log for details.'), TaskErrors.ConfigValidationError); + } if (this.isRunnerConfig(config)) { - result = new ProcessRunnerSystem(config, this.markerService, this.modelService, this.telemetryService, this.outputService, this.configurationResolverService, TaskService.OutputChannelId, clearOutput); + result = new ProcessRunnerSystem(parseResult.configuration, this.markerService, this.modelService, this.telemetryService, this.outputService, this.configurationResolverService, TaskService.OutputChannelId, clearOutput); } else if (this.isTerminalConfig(config)) { result = new TerminalTaskSystem( - config, + parseResult.configuration, this.terminalService, this.outputService, this.markerService, this.modelService, this.configurationResolverService, this.telemetryService, TaskService.OutputChannelId @@ -1041,6 +1049,26 @@ let schema: IJSONSchema = 'type': 'string', 'enum': ['always', 'silent', 'never'] }, + 'options': { + 'type': 'object', + 'description': nls.localize('JsonSchema.options', 'Additional command options'), + 'properties': { + 'cwd': { + 'type': 'string', + 'description': nls.localize('JsonSchema.options.cwd', 'The current working directory of the executed program or script. If omitted Code\'s current workspace root is used.') + }, + 'env': { + 'type': 'object', + 'additionalProperties': { + 'type': 'string' + }, + 'description': nls.localize('JsonSchema.options.env', 'The environment of the executed program or shell. If omitted the parent process\' environment is used.') + } + }, + 'additionalProperties': { + 'type': ['string', 'array', 'object'] + } + }, 'patternType': { 'anyOf': [ { @@ -1256,24 +1284,7 @@ let schema: IJSONSchema = } }, 'options': { - 'type': 'object', - 'description': nls.localize('JsonSchema.options', 'Additional command options'), - 'properties': { - 'cwd': { - 'type': 'string', - 'description': nls.localize('JsonSchema.options.cwd', 'The current working directory of the executed program or script. If omitted Code\'s current workspace root is used.') - }, - 'env': { - 'type': 'object', - 'additionalProperties': { - 'type': 'string' - }, - 'description': nls.localize('JsonSchema.options.env', 'The environment of the executed program or shell. If omitted the parent process\' environment is used.') - } - }, - 'additionalProperties': { - 'type': ['string', 'array', 'object'] - } + '$ref': '#/definitions/options' }, 'showOutput': { '$ref': '#/definitions/showOutputType', @@ -1326,13 +1337,25 @@ let schema: IJSONSchema = 'type': 'string', 'description': nls.localize('JsonSchema.tasks.taskName', "The task's name") }, + 'command': { + 'type': 'string', + 'description': nls.localize('JsonSchema.command', 'The command to be executed. Can be an external program or a shell command.') + }, + 'isShellCommand': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('JsonSchema.shell', 'Specifies whether the command is a shell command or an external program. Defaults to false if omitted.') + }, 'args': { 'type': 'array', - 'description': nls.localize('JsonSchema.tasks.args', 'Additional arguments passed to the command when this task is invoked.'), + 'description': nls.localize('JsonSchema.tasks.args', 'Arguments passed to the command when this task is invoked.'), 'items': { 'type': 'string' } }, + 'options': { + '$ref': '#/definitions/options' + }, 'suppressTaskName': { 'type': 'boolean', 'description': nls.localize('JsonSchema.tasks.suppressTaskName', 'Controls whether the task name is added as an argument to the command. If omitted the globally defined value is used.'), diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index 703a4420c2d..d84062537a4 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -22,7 +22,6 @@ import { TerminateResponse } from 'vs/base/common/processes'; import * as TPath from 'vs/base/common/paths'; import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { ValidationStatus } from 'vs/base/common/parsers'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -33,7 +32,6 @@ import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-brows import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEvents } from 'vs/workbench/parts/tasks/common/problemCollectors'; import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskRunnerConfiguration, TaskDescription, ShowOutput, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType, CommandOptions } from 'vs/workbench/parts/tasks/common/taskSystem'; -import * as FileConfig from '../node/processRunnerConfiguration'; interface TerminalData { terminal: ITerminalInstance; @@ -87,15 +85,10 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { public static TelemetryEventName: string = 'taskService'; - private validationStatus: ValidationStatus; - private buildTaskIdentifier: string; - private testTaskIdentifier: string; - private configuration: TaskRunnerConfiguration; - private outputChannel: IOutputChannel; private activeTasks: IStringDictionary; - constructor(private fileConfig: FileConfig.ExternalTaskRunnerConfiguration, private terminalService: ITerminalService, private outputService: IOutputService, + constructor(private configuration: TaskRunnerConfiguration, private terminalService: ITerminalService, private outputService: IOutputService, private markerService: IMarkerService, private modelService: IModelService, private configurationResolverService: IConfigurationResolverService, private telemetryService: ITelemetryService, outputChannelId: string) { super(); @@ -103,23 +96,13 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { this.outputChannel = this.outputService.getChannel(outputChannelId); this.clearOutput(); this.activeTasks = Object.create(null); - - let parseResult = FileConfig.parse(fileConfig, this); - this.validationStatus = parseResult.validationStatus; - this.configuration = parseResult.configuration; - this.buildTaskIdentifier = parseResult.defaultBuildTaskIdentifier; - this.testTaskIdentifier = parseResult.defaultTestTaskIdentifier; - - if (!this.validationStatus.isOK()) { - this.showOutput(); - } } public log(value: string): void { this.outputChannel.append(value + '\n'); } - private showOutput(): void { + protected showOutput(): void { this.outputChannel.show(true); } @@ -128,10 +111,10 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { } public build(): ITaskExecuteResult { - if (!this.buildTaskIdentifier) { + if (this.configuration.buildTasks.length === 0) { throw new TaskError(Severity.Info, nls.localize('TerminalTaskSystem.noBuildTask', 'No build task defined in tasks.json'), TaskErrors.NoBuildTask); } - return this.run(this.buildTaskIdentifier, Triggers.shortcut); + return this.run(this.configuration.buildTasks[0], Triggers.shortcut); } public rebuild(): ITaskExecuteResult { @@ -143,10 +126,10 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { } public runTest(): ITaskExecuteResult { - if (!this.testTaskIdentifier) { + if (this.configuration.testTasks.length === 0) { throw new TaskError(Severity.Info, nls.localize('TerminalTaskSystem.noTestTask', 'No test task defined in tasks.json'), TaskErrors.NoTestTask); } - return this.run(this.testTaskIdentifier, Triggers.shortcut); + return this.run(this.configuration.testTasks[0], Triggers.shortcut); } public run(taskIdentifier: string, trigger: string = Triggers.command): ITaskExecuteResult { @@ -257,6 +240,7 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { }); }); terminal.onExit((exitCode) => { + delete this.activeTasks[task.id]; watchingProblemMatcher.dispose(); toUnbind = dispose(toUnbind); toUnbind = null; @@ -283,12 +267,11 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { }); }); terminal.onExit((exitCode) => { + delete this.activeTasks[task.id]; startStopProblemMatcher.processLine(decoder.end()); startStopProblemMatcher.done(); startStopProblemMatcher.dispose(); this.emit(TaskSystemEvents.Inactive, event); - delete this.activeTasks[task.id]; - terminal.reuseTerminal(); resolve({ exitCode }); }); this.terminalService.setActiveInstance(terminal); @@ -307,11 +290,11 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { } private createTerminal(task: TaskDescription): ITerminalInstance { - let options = this.resolveOptions(this.configuration.options); + let options = this.resolveOptions(task.command.options); let { command, args } = this.resolveCommandAndArgs(task); let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', task.name); let waitOnExit = task.showOutput !== ShowOutput.Never || !task.isBackground; - if (this.configuration.isShellCommand) { + if (task.command.isShellCommand) { if (Platform.isWindows && ((options.cwd && TPath.isUNC(options.cwd)) || (!options.cwd && TPath.isUNC(process.cwd())))) { throw new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive.'), TaskErrors.UnknownError); } @@ -334,7 +317,12 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { let toAdd: string[] = []; let commandLine: string; if (Platform.isWindows) { - toAdd.push('/d', '/c'); + let basename = path.basename(shellConfig.executable).toLowerCase(); + if (basename === 'powershell.exe') { + toAdd.push('-Command'); + } else { + toAdd.push('/d', '/c'); + } let quotedCommand: boolean = false; let quotedArg: boolean = false; let quoted = this.ensureDoubleQuotes(command); @@ -382,7 +370,7 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { let cwd = options && options.cwd ? options.cwd : process.cwd(); // On Windows executed process must be described absolute. Since we allowed command without an // absolute path (e.g. "command": "node") we need to find the executable in the CWD or PATH. - let executable = Platform.isWindows && !this.configuration.isShellCommand ? this.findExecutable(command, cwd) : command; + let executable = Platform.isWindows && !task.command.isShellCommand ? this.findExecutable(command, cwd) : command; const shellLaunchConfig: IShellLaunchConfig = { name: terminalName, executable: executable, @@ -394,11 +382,12 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { } private resolveCommandAndArgs(task: TaskDescription): { command: string, args: string[] } { - let args: string[] = this.configuration.args ? this.configuration.args.slice() : []; + // First we need to use the command args: + let args: string[] = task.command.args ? task.command.args.slice() : []; // We need to first pass the task name if (!task.suppressTaskName) { - if (this.fileConfig.taskSelector) { - args.push(this.fileConfig.taskSelector + task.name); + if (task.command.taskSelector) { + args.push(task.command.taskSelector + task.name); } else { args.push(task.name); } @@ -408,7 +397,7 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { args = args.concat(task.args); } args = this.resolveVariables(args); - let command: string = this.resolveVariable(this.configuration.command); + let command: string = this.resolveVariable(task.command.name); return { command, args }; } diff --git a/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts b/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts index 6634b8455fe..d3c3852640a 100644 --- a/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts @@ -28,12 +28,6 @@ export class ProblemHandling { public static clean: string = 'cleanMatcherMatchers'; } -export namespace ShowOutput { - // let always: string = 'always'; - // let silent: string = 'silent'; - // let never: string = 'never'; -} - /** * The description of a task. */ @@ -45,16 +39,41 @@ export interface TaskDescription { taskName: string; /** - * Additional arguments passed to the command when this task is - * executed. + * The command to be executed. Can be an external program or a shell + * command. + */ + command?: string; + + /** + * Specifies whether the command is a shell command and therefore must + * be executed in a shell interpreter (e.g. cmd.exe, bash, ...). + * + * Defaults to false if omitted. + */ + isShellCommand?: boolean; + + /** + * The command options used when the command is executed. Can be omitted. + */ + options?: ProcessConfig.CommandOptions; + + /** + * The arguments passed to the command or additional arguments passed to the + * command when using a global command. */ args?: string[]; /** + * @deprecated Use `isBackground` instead. * Whether the executed command is kept alive and is watching the file system. */ isWatching?: boolean; + /** + * Whether the executed command is kept alive and runs in the background. + */ + isBackground?: boolean; + /** * Whether the task should prompt on close for confirmation if running. */ @@ -160,12 +179,19 @@ export interface BaseTaskRunnerConfiguration extends TaskSystem.TaskConfiguratio problemMatcher?: ProblemMatcherConfig.ProblemMatcherType; /** + * @deprecated Use `isBackground` instead. + * * Specifies whether a global command is a watching the filesystem. A task.json - * file can iether contains a global isWatching property or a tasks property + * file can either contain a global isWatching property or a tasks property * but not both. */ isWatching?: boolean; + /** + * Specifies whether a global command is a background task. + */ + isBackground?: boolean; + /** * Whether the task should prompt on close for confirmation if running. */ @@ -217,363 +243,333 @@ enum ProblemMatcherKind { Array } -interface Globals { - command?: string; - isShellCommand?: boolean; - taskSelector?: string; - suppressTaskName?: boolean; - showOutput?: TaskSystem.ShowOutput; - echoCommand?: boolean; +const EMPTY_ARRAY: any[] = []; +Object.freeze(EMPTY_ARRAY); + +function mergeProperty(target: T, source: T, key: K) { + if (source[key] !== void 0) { + target[key] = source[key]; + } +} + +function fillProperty(target: T, source: T, key: K) { + if (target[key] === void 0 && source[key] !== void 0) { + target[key] = source[key]; + } } interface ParseContext { - isMain: boolean; - globals: Globals; -} - -export interface ParseResult { + logger: ILogger; validationStatus: ValidationStatus; - configuration: TaskSystem.TaskRunnerConfiguration; - defaultBuildTaskIdentifier: string; - defaultTestTaskIdentifier: string; + namedProblemMatchers: IStringDictionary; } -export interface ILogger { - log(value: string): void; -} - -class ConfigurationParser { - - private validationStatus: ValidationStatus; - private defaultBuildTaskIdentifier: string; - private defaultTestTaskIdentifier: string; - - private logger: ILogger; - private namedProblemMatchers: IStringDictionary; - - constructor(logger: ILogger) { - this.logger = logger; - this.validationStatus = new ValidationStatus(); - this.namedProblemMatchers = Object.create(null); - } - - private log(value: string): void { - this.logger.log(value); - } - - public run(fileConfig: ExternalTaskRunnerConfiguration): ParseResult { - return { - validationStatus: this.validationStatus, - configuration: this.createTaskRunnerConfiguration(fileConfig), - defaultBuildTaskIdentifier: this.defaultBuildTaskIdentifier, - defaultTestTaskIdentifier: this.defaultTestTaskIdentifier - }; - } - - private createTaskRunnerConfiguration(fileConfig: ExternalTaskRunnerConfiguration): TaskSystem.TaskRunnerConfiguration { - let globals = this.createGlobals(fileConfig); - let result = this.createBaseTaskRunnerConfiguration(fileConfig, { isMain: true, globals: globals }); - if (!this.validationStatus.isFatal()) { - let osConfig: TaskSystem.BaseTaskRunnerConfiguration = null; - let osContext: ParseContext = { isMain: false, globals: globals }; - if (fileConfig.windows && Platform.platform === Platform.Platform.Windows) { - osConfig = this.createBaseTaskRunnerConfiguration(fileConfig.windows, osContext); - } else if (fileConfig.osx && Platform.platform === Platform.Platform.Mac) { - osConfig = this.createBaseTaskRunnerConfiguration(fileConfig.osx, osContext); - } else if (fileConfig.linux && Platform.platform === Platform.Platform.Linux) { - osConfig = this.createBaseTaskRunnerConfiguration(fileConfig.linux, osContext); - } - if (!this.validationStatus.isFatal()) { - if (osConfig) { - this.mergeTaskRunnerConigurations(result, osConfig); - } - if (Types.isUndefined(result.options.cwd)) { - result.options.cwd = '${workspaceRoot}'; - } - } - } - if (!result.command) { - this.validationStatus.state = ValidationState.Fatal; - this.log(nls.localize('ConfigurationParser.noCommand', 'Error: no valid command name provided.')); - return null; - } - return result; - } - - private createGlobals(fileConfig: ExternalTaskRunnerConfiguration): Globals { - let result = this.parseGlobals(fileConfig); - let osGlobals: Globals = null; - if (fileConfig.windows && Platform.platform === Platform.Platform.Windows) { - osGlobals = this.parseGlobals(fileConfig.windows); - } else if (fileConfig.osx && Platform.platform === Platform.Platform.Mac) { - osGlobals = this.parseGlobals(fileConfig.osx); - } else if (fileConfig.linux && Platform.platform === Platform.Platform.Linux) { - osGlobals = this.parseGlobals(fileConfig.linux); - } - if (osGlobals) { - Objects.mixin(result, osGlobals, true); - } - if (Types.isUndefined(result.isShellCommand)) { - result.isShellCommand = false; - } - if (Types.isUndefined(result.showOutput)) { - result.showOutput = TaskSystem.ShowOutput.Always; - } - if (Types.isUndefined(result.echoCommand)) { - result.echoCommand = false; - } - if (Types.isUndefined(result.suppressTaskName)) { - result.suppressTaskName = false; - } - return result; - } - - private parseGlobals(fileConfig: BaseTaskRunnerConfiguration): Globals { - let result: Globals = {}; - if (Types.isString(fileConfig.command)) { - result.command = fileConfig.command; - } - if (Types.isBoolean(fileConfig.isShellCommand)) { - result.isShellCommand = fileConfig.isShellCommand; - } - if (Types.isString(fileConfig.showOutput)) { - result.showOutput = TaskSystem.ShowOutput.fromString(fileConfig.showOutput); - } - if (!Types.isUndefined(fileConfig.echoCommand)) { - result.echoCommand = !!fileConfig.echoCommand; - } - if (!Types.isUndefined(fileConfig.suppressTaskName)) { - result.suppressTaskName = !!fileConfig.suppressTaskName; - } - if (Types.isString(fileConfig.taskSelector)) { - result.taskSelector = fileConfig.taskSelector; - } - return result; - } - - private mergeTaskRunnerConigurations(result: TaskSystem.BaseTaskRunnerConfiguration, osConfig: TaskSystem.BaseTaskRunnerConfiguration): void { - if (osConfig.command) { - result.command = osConfig.command; - } - if (osConfig.args) { - result.args = result.args ? result.args.concat(osConfig.args) : osConfig.args; - } - if (!Types.isUndefined(osConfig.isShellCommand)) { - result.isShellCommand = osConfig.isShellCommand; - } - if (osConfig.options) { - if (Types.isString(osConfig.options.cwd)) { - result.options.cwd = osConfig.options.cwd; - } - if (osConfig.options.env) { - let osEnv = osConfig.options.env; - let env = result.options.env; - if (env) { - Object.keys(osEnv).forEach((key) => { - env[key] = osEnv[key]; - }); - } else { - result.options.env = osEnv; - } - } - } - if (osConfig.tasks) { - let taskNames: IStringDictionary = Object.create(null); - Object.keys(result.tasks).forEach(key => { - let task = result.tasks[key]; - taskNames[task.name] = task.id; - }); - - let osTaskNames: IStringDictionary = Object.create(null); - Object.keys(osConfig.tasks).forEach(key => { - let task = osConfig.tasks[key]; - osTaskNames[task.name] = task.id; - }); - - Object.keys(osTaskNames).forEach(taskName => { - let id = taskNames[taskName]; - let osId = osTaskNames[taskName]; - // Same name exists globally - if (id) { - delete result.tasks[id]; - } - result.tasks[osId] = osConfig.tasks[osId]; - }); - } - } - - private createBaseTaskRunnerConfiguration(fileConfig: BaseTaskRunnerConfiguration, context: ParseContext): TaskSystem.BaseTaskRunnerConfiguration { - let globals = context.globals; - let result: TaskSystem.BaseTaskRunnerConfiguration = { - isShellCommand: globals.isShellCommand, - args: [], - }; - if (Types.isString(fileConfig.command)) { - result.command = fileConfig.command; - } - if (!Types.isUndefined(fileConfig.isShellCommand)) { - result.isShellCommand = fileConfig.isShellCommand; - } - let argsIsValid: boolean = Types.isUndefined(fileConfig.args); - if (Types.isStringArray(fileConfig.args)) { - argsIsValid = true; - result.args = fileConfig.args.slice(); - } - if (!argsIsValid) { - this.validationStatus.state = ValidationState.Fatal; - this.log(nls.localize('ConfigurationParser.noargs', 'Error: command arguments must be an array of strings. Provided value is:\n{0}', fileConfig.args ? JSON.stringify(fileConfig.args, null, 4) : 'undefined')); - } - result.options = this.createCommandOptions(fileConfig.options); - if (context.isMain) { - this.namedProblemMatchers = this.createNamedProblemMatchers(fileConfig); - } - let hasGlobalMatcher = !Types.isUndefined(fileConfig.problemMatcher); - let hasTasks = Types.isArray(fileConfig.tasks); - if (hasTasks) { - result.tasks = this.createTasks(fileConfig.tasks, context); - if (hasGlobalMatcher) { - this.validationStatus.state = ValidationState.Warning; - this.log(nls.localize('ConfigurationParser.globalMatcher', 'Warning: global matchers and tasks can\'t be mixed. Ignoring global matchers.')); - } - } else if (context.isMain) { - let isWatching: boolean = false; - if (!Types.isUndefined(fileConfig.isWatching)) { - isWatching = !!fileConfig.isWatching; - } - let promptOnClose: boolean = true; - if (!Types.isUndefined(fileConfig.promptOnClose)) { - promptOnClose = !!fileConfig.promptOnClose; - } else { - promptOnClose = !isWatching; - } - let task: TaskSystem.TaskDescription = { - id: UUID.generateUuid(), - name: globals.command, - showOutput: globals.showOutput, - suppressTaskName: true, - isBackground: isWatching, - promptOnClose: promptOnClose, - echoCommand: globals.echoCommand, - }; - if (hasGlobalMatcher) { - let problemMatchers = this.createProblemMatchers(fileConfig.problemMatcher); - task.problemMatchers = problemMatchers; - } else { - task.problemMatchers = []; - } - // ToDo@dirkb: this is very special for the tsc watch mode. We should find - // a exensible solution for this. - for (let matcher of task.problemMatchers) { - if ((matcher).tscWatch) { - (task).tscWatch = true; - break; - } - } - this.defaultBuildTaskIdentifier = task.id; - result.tasks = Object.create(null); - result.tasks[task.id] = task; - } - return result; - } - - private createCommandOptions(fileOptions: ProcessConfig.CommandOptions): TaskSystem.CommandOptions { +namespace CommandOptions { + export function from(this: void, options: ProcessConfig.CommandOptions, context: ParseContext): TaskSystem.CommandOptions { let result: TaskSystem.CommandOptions = {}; - if (fileOptions) { - if (!Types.isUndefined(fileOptions.cwd)) { - if (Types.isString(fileOptions.cwd)) { - result.cwd = fileOptions.cwd; - } else { - this.validationStatus.state = ValidationState.Warning; - this.log(nls.localize('ConfigurationParser.invalidCWD', 'Warning: options.cwd must be of type string. Ignoring value {0}\n', fileOptions.cwd)); - } - } - if (!Types.isUndefined(fileOptions.env)) { - result.env = Objects.clone(fileOptions.env); + if (options.cwd !== void 0) { + if (Types.isString(options.cwd)) { + result.cwd = options.cwd; + } else { + context.validationStatus.state = ValidationState.Warning; + context.logger.log(nls.localize('ConfigurationParser.invalidCWD', 'Warning: options.cwd must be of type string. Ignoring value {0}\n', options.cwd)); } } - return result; + if (options.env !== void 0) { + result.env = Objects.clone(options.env); + } + return isEmpty(result) ? undefined : result; } - private createNamedProblemMatchers(fileConfig: BaseTaskRunnerConfiguration): IStringDictionary { + export function isEmpty(value: TaskSystem.CommandOptions): boolean { + return !value || value.cwd === void 0 && value.env === void 0; + } + + export function merge(target: TaskSystem.CommandOptions, source: TaskSystem.CommandOptions): TaskSystem.CommandOptions { + if (isEmpty(source)) { + return target; + } + if (isEmpty(target)) { + return source; + } + mergeProperty(target, source, 'cwd'); + if (target.env === void 0) { + target.env = source.env; + } else if (source.env !== void 0) { + let env: { [key: string]: string; } = Object.create(null); + Object.keys(target.env).forEach(key => env[key] = target.env[key]); + Object.keys(source.env).forEach(key => env[key = source.env[key]]); + target.env = env; + } + return target; + } + + export function fillDefaults(value: TaskSystem.CommandOptions): TaskSystem.CommandOptions { + if (value && Object.isFrozen(value)) { + return value; + } + if (value === void 0) { + value = {}; + } + if (value.cwd === void 0) { + value.cwd = '${workspaceRoot}'; + } + return value; + } + + export function freeze(value: TaskSystem.CommandOptions): void { + Object.freeze(value); + if (value.env) { + Object.freeze(value.env); + } + } +} + +namespace CommandConfiguration { + interface CommandConfiguationShape { + command?: string; + isShellCommand?: boolean; + args?: string[]; + options?: ProcessConfig.CommandOptions; + echoCommand?: boolean; + taskSelector?: string; + } + + export function from(this: void, config: CommandConfiguationShape, context: ParseContext): TaskSystem.CommandConfiguration { + let result: TaskSystem.CommandConfiguration = {}; + if (Types.isString(config.command)) { + result.name = config.command; + } + if (Types.isBoolean(config.isShellCommand)) { + result.isShellCommand = config.isShellCommand; + } + if (config.args !== void 0) { + if (Types.isStringArray(config.args)) { + result.args = config.args.slice(0); + } else { + context.validationStatus.state = ValidationState.Fatal; + context.logger.log(nls.localize('ConfigurationParser.noargs', 'Error: command arguments must be an array of strings. Provided value is:\n{0}', config.args ? JSON.stringify(config.args, undefined, 4) : 'undefined')); + } + } + if (config.options !== void 0) { + result.options = CommandOptions.from(config.options, context); + } + if (Types.isBoolean(config.echoCommand)) { + result.echo = config.echoCommand; + } + if (Types.isString(config.taskSelector)) { + result.taskSelector = config.taskSelector; + } + return isEmpty(result) ? undefined : result; + } + + export function isEmpty(value: TaskSystem.CommandConfiguration): boolean { + return !value || value.name === void 0 && value.isShellCommand === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options) && value.echo === void 0; + } + + export function onlyEcho(value: TaskSystem.CommandConfiguration): boolean { + return value && value.echo !== void 0 && value.name === void 0 && value.isShellCommand === void 0 && value.args === void 0 && CommandOptions.isEmpty(value.options); + } + + export function merge(target: TaskSystem.CommandConfiguration, source: TaskSystem.CommandConfiguration): TaskSystem.CommandConfiguration { + if (isEmpty(source)) { + return target; + } + if (isEmpty(target)) { + return source; + } + mergeProperty(target, source, 'name'); + mergeProperty(target, source, 'isShellCommand'); + mergeProperty(target, source, 'echo'); + mergeProperty(target, source, 'taskSelector'); + if (source.args !== void 0) { + if (target.args === void 0) { + target.args = source.args; + } else { + target.args = target.args.concat(source.args); + } + } + target.options = CommandOptions.merge(target.options, source.options); + return target; + } + + export function fillDefaults(value: TaskSystem.CommandConfiguration): void { + if (!value || Object.isFrozen(value)) { + return; + } + if (value.name !== void 0 && value.isShellCommand === void 0) { + value.isShellCommand = false; + } + if (value.echo === void 0) { + value.echo = false; + } + if (value.args === void 0) { + value.args = EMPTY_ARRAY; + } + if (!isEmpty(value)) { + value.options = CommandOptions.fillDefaults(value.options); + } + } + + export function freeze(value: TaskSystem.CommandConfiguration): void { + Object.freeze(value); + if (value.args) { + Object.freeze(value.args); + } + if (value.options) { + CommandOptions.freeze(value.options); + } + } +} + +namespace ProblemMatcherConverter { + + export function namedFrom(this: void, declares: ProblemMatcherConfig.NamedProblemMatcher[], context: ParseContext): IStringDictionary { let result: IStringDictionary = Object.create(null); - if (!Types.isArray(fileConfig.declares)) { + + if (!Types.isArray(declares)) { return result; } - let values = fileConfig.declares; - (values).forEach((value) => { - let namedProblemMatcher = this.createNamedProblemMatcher(value); - if (namedProblemMatcher) { + (declares).forEach((value) => { + let namedProblemMatcher = (new ProblemMatcherParser(ProblemMatcherRegistry, context.logger, context.validationStatus)).parse(value); + if (isNamedProblemMatcher(namedProblemMatcher)) { result[namedProblemMatcher.name] = namedProblemMatcher; + } else { + context.validationStatus.state = ValidationState.Error; + context.logger.log(nls.localize('ConfigurationParser.noName', 'Error: Problem Matcher in declare scope must have a name:\n{0}\n', JSON.stringify(value, undefined, 4))); } }); return result; } - private createNamedProblemMatcher(value: ProblemMatcherConfig.NamedProblemMatcher): NamedProblemMatcher { - let result = (new ProblemMatcherParser(ProblemMatcherRegistry, this.logger, this.validationStatus)).parse(value); - if (isNamedProblemMatcher(result)) { + export function from(this: void, config: ProblemMatcherConfig.ProblemMatcherType, context: ParseContext): ProblemMatcher[] { + let result: ProblemMatcher[] = []; + if (config === void 0) { return result; + } + let kind = getProblemMatcherKind(config); + if (kind === ProblemMatcherKind.Unknown) { + context.validationStatus.state = ValidationState.Warning; + context.logger.log(nls.localize( + 'ConfigurationParser.unknownMatcherKind', + 'Warning: the defined problem matcher is unknown. Supported types are string | ProblemMatcher | (string | ProblemMatcher)[].\n{0}\n', + JSON.stringify(config, null, 4))); + return result; + } else if (kind === ProblemMatcherKind.String || kind === ProblemMatcherKind.ProblemMatcher) { + let matcher = resolveProblemMatcher(config, context); + if (matcher) { + result.push(matcher); + } + } else if (kind === ProblemMatcherKind.Array) { + let problemMatchers = <(string | ProblemMatcherConfig.ProblemMatcher)[]>config; + problemMatchers.forEach(problemMatcher => { + let matcher = resolveProblemMatcher(problemMatcher, context); + if (matcher) { + result.push(matcher); + } + }); + } + return result; + } + + function getProblemMatcherKind(this: void, value: ProblemMatcherConfig.ProblemMatcherType): ProblemMatcherKind { + if (Types.isString(value)) { + return ProblemMatcherKind.String; + } else if (Types.isArray(value)) { + return ProblemMatcherKind.Array; + } else if (!Types.isUndefined(value)) { + return ProblemMatcherKind.ProblemMatcher; } else { - this.validationStatus.state = ValidationState.Error; - this.log(nls.localize('ConfigurationParser.noName', 'Error: Problem Matcher in declare scope must have a name:\n{0}\n', JSON.stringify(value, null, 4))); - return null; + return ProblemMatcherKind.Unknown; } } - private createTasks(tasks: TaskDescription[], context: ParseContext): IStringDictionary { + function resolveProblemMatcher(this: void, value: string | ProblemMatcherConfig.ProblemMatcher, context: ParseContext): ProblemMatcher { + if (Types.isString(value)) { + let variableName = value; + if (variableName.length > 1 && variableName[0] === '$') { + variableName = variableName.substring(1); + let global = ProblemMatcherRegistry.get(variableName); + if (global) { + return Objects.clone(global); + } + let localProblemMatcher = context.namedProblemMatchers[variableName]; + if (localProblemMatcher) { + localProblemMatcher = Objects.clone(localProblemMatcher); + // remove the name + delete localProblemMatcher.name; + return localProblemMatcher; + } + } + context.validationStatus.state = ValidationState.Error; + context.logger.log(nls.localize('ConfigurationParser.invalidVaraibleReference', 'Error: Invalid problemMatcher reference: {0}\n', value)); + return undefined; + } else { + let json = value; + return new ProblemMatcherParser(ProblemMatcherRegistry, context.logger, context.validationStatus).parse(json); + } + } +} + +namespace TaskDescription { + + export interface TaskConfiguration { + tasks: IStringDictionary; + buildTask?: string; + testTask?: string; + } + + export function from(this: void, tasks: TaskDescription[], globals: Globals, context: ParseContext): TaskConfiguration { let result: IStringDictionary = Object.create(null); if (!tasks) { - return result; + return { tasks: result }; } let defaultBuildTask: { id: string; exact: number; } = { id: null, exact: -1 }; let defaultTestTask: { id: string; exact: number; } = { id: null, exact: -1 }; tasks.forEach((externalTask) => { let taskName = externalTask.taskName; if (!taskName) { - this.validationStatus.state = ValidationState.Fatal; - this.log(nls.localize('ConfigurationParser.noTaskName', 'Error: tasks must provide a taskName property. The task will be ignored.\n{0}\n', JSON.stringify(externalTask, null, 4))); + context.validationStatus.state = ValidationState.Fatal; + context.logger.log(nls.localize('ConfigurationParser.noTaskName', 'Error: tasks must provide a taskName property. The task will be ignored.\n{0}\n', JSON.stringify(externalTask, null, 4))); return; } - let problemMatchers = this.createProblemMatchers(externalTask.problemMatcher); - let task: TaskSystem.TaskDescription = { id: UUID.generateUuid(), name: taskName, showOutput: context.globals.showOutput }; - if (Types.isStringArray(externalTask.args)) { + let problemMatchers = ProblemMatcherConverter.from(externalTask.problemMatcher, context); + let command: TaskSystem.CommandConfiguration = externalTask.command !== void 0 + ? CommandConfiguration.from(externalTask, context) + : externalTask.echoCommand !== void 0 ? { echo: !!externalTask.echoCommand } : undefined; + let task: TaskSystem.TaskDescription = { + id: UUID.generateUuid(), + name: taskName, + command, + showOutput: undefined + }; + if (externalTask.command === void 0 && Types.isStringArray(externalTask.args)) { task.args = externalTask.args.slice(); } - task.isBackground = false; - if (!Types.isUndefined(externalTask.isWatching)) { + if (externalTask.isWatching !== void 0) { task.isBackground = !!externalTask.isWatching; } - task.promptOnClose = true; - if (!Types.isUndefined(externalTask.promptOnClose)) { + if (externalTask.isBackground !== void 0) { + task.isBackground = !!externalTask.isBackground; + } + if (externalTask.promptOnClose !== void 0) { task.promptOnClose = !!externalTask.promptOnClose; - } else { - task.promptOnClose = !task.isBackground; } if (Types.isString(externalTask.showOutput)) { task.showOutput = TaskSystem.ShowOutput.fromString(externalTask.showOutput); } - if (Types.isUndefined(task.showOutput)) { - task.showOutput = context.globals.showOutput; - } - task.echoCommand = context.globals.echoCommand; - if (!Types.isUndefined(externalTask.echoCommand)) { - task.echoCommand = !!externalTask.echoCommand; - } - task.suppressTaskName = context.globals.suppressTaskName; - if (!Types.isUndefined(externalTask.suppressTaskName)) { + if (externalTask.suppressTaskName !== void 0) { task.suppressTaskName = !!externalTask.suppressTaskName; + } else if (externalTask.command !== void 0) { + // if the task has its own command then we suppress the + // task name by default. + task.suppressTaskName = true; } if (problemMatchers) { task.problemMatchers = problemMatchers; } - // ToDo@dirkb: this is very special for the tsc watch mode. We should find - // a exensible solution for this. - for (let matcher of task.problemMatchers) { - if ((matcher).tscWatch) { - (task).tscWatch = true; - break; - } - } + mergeGlobals(task, globals); + fillDefaults(task); result[task.id] = task; if (!Types.isUndefined(externalTask.isBuildCommand) && externalTask.isBuildCommand && defaultBuildTask.exact < 2) { defaultBuildTask.id = task.id; @@ -590,84 +586,260 @@ class ConfigurationParser { defaultTestTask.exact = 1; } }); + let buildTask: string; if (defaultBuildTask.exact > 0) { - this.defaultBuildTaskIdentifier = defaultBuildTask.id; + buildTask = defaultBuildTask.id; } + let testTask: string; if (defaultTestTask.exact > 0) { - this.defaultTestTaskIdentifier = defaultTestTask.id; + testTask = defaultTestTask.id; } - return result; + return { tasks: result, buildTask, testTask }; } - private createProblemMatchers(problemMatcher: ProblemMatcherConfig.ProblemMatcherType): ProblemMatcher[] { - let result: ProblemMatcher[] = []; - if (Types.isUndefined(problemMatcher)) { - return result; - } - let kind = this.getProblemMatcherKind(problemMatcher); - if (kind === ProblemMatcherKind.Unknown) { - this.validationStatus.state = ValidationState.Warning; - this.log(nls.localize( - 'ConfigurationParser.unknownMatcherKind', - 'Warning: the defined problem matcher is unknown. Supported types are string | ProblemMatcher | (string | ProblemMatcher)[].\n{0}\n', - JSON.stringify(problemMatcher, null, 4))); - return result; - } else if (kind === ProblemMatcherKind.String || kind === ProblemMatcherKind.ProblemMatcher) { - let matcher = this.resolveProblemMatcher(problemMatcher); - if (matcher) { - result.push(matcher); - } - } else if (kind === ProblemMatcherKind.Array) { - let problemMatchers = <(string | ProblemMatcherConfig.ProblemMatcher)[]>problemMatcher; - problemMatchers.forEach(problemMatcher => { - let matcher = this.resolveProblemMatcher(problemMatcher); - if (matcher) { - result.push(matcher); + export function merge(target: TaskConfiguration, source: TaskConfiguration): TaskConfiguration { + if (source.tasks) { + // Tasks are keyed by ID but we need to merge by name + let targetNames: IStringDictionary = Object.create(null); + Object.keys(target.tasks).forEach(key => { + let task = target.tasks[key]; + targetNames[task.name] = task.id; + }); + + let sourceNames: IStringDictionary = Object.create(null); + Object.keys(source.tasks).forEach(key => { + let task = source.tasks[key]; + sourceNames[task.name] = task.id; + }); + + Object.keys(sourceNames).forEach(taskName => { + let targetId = targetNames[taskName]; + let sourceId = sourceNames[taskName]; + // Same name exists globally + if (targetId) { + delete target.tasks[targetId]; } + target.tasks[sourceId] = source.tasks[sourceId]; }); } + fillProperty(target, source, 'buildTask'); + fillProperty(target, source, 'testTask'); + return target; + } + + export function mergeGlobals(task: TaskSystem.TaskDescription, globals: Globals): void { + if (CommandConfiguration.isEmpty(task.command) && !CommandConfiguration.isEmpty(globals.command)) { + task.command = globals.command; + } + if (CommandConfiguration.onlyEcho(task.command)) { + // The globals can have a echo set which would override the local echo + // Saves the need of a additional fill method. But might be necessary + // at some point. + let oldEcho = task.command.echo; + CommandConfiguration.merge(task.command, globals.command); + task.command.echo = oldEcho; + } + // promptOnClose is inferred from isBackground if available + if (task.promptOnClose === void 0 && task.isBackground === void 0 && globals.promptOnClose !== void 0) { + task.promptOnClose = globals.promptOnClose; + } + if (task.suppressTaskName === void 0 && globals.suppressTaskName !== void 0) { + task.suppressTaskName = globals.suppressTaskName; + } + if (task.showOutput === void 0 && globals.showOutput !== void 0) { + task.showOutput = globals.showOutput; + } + } + + export function fillDefaults(task: TaskSystem.TaskDescription): void { + CommandConfiguration.fillDefaults(task.command); + if (task.args === void 0 && task.command === void 0) { + task.args = EMPTY_ARRAY; + } + if (task.suppressTaskName === void 0) { + task.suppressTaskName = false; + } + if (task.promptOnClose === void 0) { + task.promptOnClose = task.isBackground !== void 0 ? !task.isBackground : true; + } + if (task.isBackground === void 0) { + task.isBackground = false; + } + if (task.showOutput === void 0) { + task.showOutput = TaskSystem.ShowOutput.Always; + } + if (task.problemMatchers === void 0) { + task.problemMatchers = EMPTY_ARRAY; + } + } +} + +interface Globals { + command?: TaskSystem.CommandConfiguration; + promptOnClose?: boolean; + suppressTaskName?: boolean; + showOutput?: TaskSystem.ShowOutput; +} + +namespace Globals { + export function from(this: void, config: BaseTaskRunnerConfiguration, context: ParseContext): Globals { + let result: Globals = {}; + result.command = CommandConfiguration.from(config, context); + if (Types.isString(config.showOutput)) { + result.showOutput = TaskSystem.ShowOutput.fromString(config.showOutput); + } + if (config.suppressTaskName !== void 0) { + result.suppressTaskName = !!config.suppressTaskName; + } + if (config.promptOnClose !== void 0) { + result.promptOnClose = !!config.promptOnClose; + } return result; } - private getProblemMatcherKind(value: ProblemMatcherConfig.ProblemMatcherType): ProblemMatcherKind { - if (Types.isString(value)) { - return ProblemMatcherKind.String; - } else if (Types.isArray(value)) { - return ProblemMatcherKind.Array; - } else if (!Types.isUndefined(value)) { - return ProblemMatcherKind.ProblemMatcher; - } else { - return ProblemMatcherKind.Unknown; + export function isEmpty(value: Globals): boolean { + return !value || value.command === void 0 && value.promptOnClose === void 0 && value.showOutput === void 0 && value.suppressTaskName === void 0; + } + + export function merge(target: Globals, source: Globals): Globals { + if (isEmpty(source)) { + return target; + } + if (isEmpty(target)) { + return source; + } + target.command = CommandConfiguration.merge(target.command, source.command); + mergeProperty(target, source, 'promptOnClose'); + mergeProperty(target, source, 'suppressTaskName'); + mergeProperty(target, source, 'showOutput'); + return target; + } + + export function fillDefaults(value: Globals): void { + if (!value) { + return; + } + CommandConfiguration.fillDefaults(value.command); + if (value.suppressTaskName === void 0) { + value.suppressTaskName = false; + } + if (value.showOutput === void 0) { + value.showOutput = TaskSystem.ShowOutput.Always; + } + if (value.promptOnClose === void 0) { + value.promptOnClose = true; } } - private resolveProblemMatcher(value: string | ProblemMatcherConfig.ProblemMatcher): ProblemMatcher { - if (Types.isString(value)) { - let variableName = value; - if (variableName.length > 1 && variableName[0] === '$') { - variableName = variableName.substring(1); - let global = ProblemMatcherRegistry.get(variableName); - if (global) { - return Objects.clone(global); - } - let localProblemMatcher = this.namedProblemMatchers[variableName]; - if (localProblemMatcher) { - localProblemMatcher = Objects.clone(localProblemMatcher); - // remove the name - delete localProblemMatcher.name; - return localProblemMatcher; - } - } - this.validationStatus.state = ValidationState.Error; - this.log(nls.localize('ConfigurationParser.invalidVaraibleReference', 'Error: Invalid problemMatcher reference: {0}\n', value)); - return null; - } else { - let json = value; - return new ProblemMatcherParser(ProblemMatcherRegistry, this.logger, this.validationStatus).parse(json); + export function freeze(value: Globals): void { + Object.freeze(value); + if (value.command) { + CommandConfiguration.freeze(value.command); } } } +export interface ParseResult { + validationStatus: ValidationStatus; + configuration: TaskSystem.TaskRunnerConfiguration; +} + +export interface ILogger { + log(value: string): void; +} + +class ConfigurationParser { + + private validationStatus: ValidationStatus; + + private logger: ILogger; + + constructor(logger: ILogger) { + this.logger = logger; + this.validationStatus = new ValidationStatus(); + } + + public run(fileConfig: ExternalTaskRunnerConfiguration): ParseResult { + let context: ParseContext = { logger: this.logger, validationStatus: this.validationStatus, namedProblemMatchers: undefined }; + return { + validationStatus: this.validationStatus, + configuration: this.createTaskRunnerConfiguration(fileConfig, context), + }; + } + + private createTaskRunnerConfiguration(fileConfig: ExternalTaskRunnerConfiguration, context: ParseContext): TaskSystem.TaskRunnerConfiguration { + let globals = this.createGlobals(fileConfig, context); + if (context.validationStatus.isFatal()) { + return undefined; + } + context.namedProblemMatchers = ProblemMatcherConverter.namedFrom(fileConfig.declares, context); + let globalTasks: TaskDescription.TaskConfiguration; + if (fileConfig.windows && Platform.platform === Platform.Platform.Windows) { + globalTasks = TaskDescription.from(fileConfig.windows.tasks, globals, context); + } else if (fileConfig.osx && Platform.platform === Platform.Platform.Mac) { + globalTasks = TaskDescription.from(fileConfig.osx.tasks, globals, context); + } else if (fileConfig.linux && Platform.platform === Platform.Platform.Linux) { + globalTasks = TaskDescription.from(fileConfig.linux.tasks, globals, context); + } + + let taskConfig: TaskDescription.TaskConfiguration; + if (fileConfig.tasks) { + taskConfig = TaskDescription.from(fileConfig.tasks, globals, context); + if (globalTasks) { + taskConfig = TaskDescription.merge(taskConfig, globalTasks); + } + } else { + let tasks: IStringDictionary = Object.create(null); + let buildTask: string; + if (globals.command && globals.command.name) { + let matchers: ProblemMatcher[] = ProblemMatcherConverter.from(fileConfig.problemMatcher, context);; + let isBackground = fileConfig.isBackground ? !!fileConfig.isBackground : fileConfig.isWatching ? !!fileConfig.isWatching : undefined; + let task: TaskSystem.TaskDescription = { + id: UUID.generateUuid(), + name: globals.command.name, + command: undefined, + isBackground: isBackground, + showOutput: undefined, + suppressTaskName: true, // this must be true since we infer the task from the global data. + problemMatchers: matchers + }; + TaskDescription.mergeGlobals(task, globals); + TaskDescription.fillDefaults(task); + tasks[task.id] = task; + buildTask = task.id; + } + taskConfig = { + tasks: tasks, + buildTask + }; + } + + return { + tasks: taskConfig.tasks, + buildTasks: taskConfig.buildTask ? [taskConfig.buildTask] : [], + testTasks: taskConfig.testTask ? [taskConfig.testTask] : [] + }; + } + + private createGlobals(fileConfig: ExternalTaskRunnerConfiguration, context: ParseContext): Globals { + let result = Globals.from(fileConfig, context); + let osGlobals: Globals = undefined; + if (fileConfig.windows && Platform.platform === Platform.Platform.Windows) { + osGlobals = Globals.from(fileConfig.windows, context); + } else if (fileConfig.osx && Platform.platform === Platform.Platform.Mac) { + osGlobals = Globals.from(fileConfig.osx, context); + } else if (fileConfig.linux && Platform.platform === Platform.Platform.Linux) { + osGlobals = Globals.from(fileConfig.linux, context); + } + if (osGlobals) { + result = Globals.merge(result, osGlobals); + } + Globals.fillDefaults(result); + Globals.freeze(result); + return result; + } +} + export function parse(configuration: ExternalTaskRunnerConfiguration, logger: ILogger): ParseResult { return (new ConfigurationParser(logger)).run(configuration); } \ No newline at end of file diff --git a/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts b/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts index 643d5bcc4b5..51fd1bed2a8 100644 --- a/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts @@ -21,14 +21,16 @@ import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { ValidationStatus } from 'vs/base/common/parsers'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEvents } from 'vs/workbench/parts/tasks/common/problemCollectors'; -import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskRunnerConfiguration, TaskDescription, CommandOptions, ShowOutput, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType } from 'vs/workbench/parts/tasks/common/taskSystem'; -import * as FileConfig from './processRunnerConfiguration'; +import { + ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskRunnerConfiguration, + TaskDescription, CommandOptions, ShowOutput, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType, + CommandConfiguration +} from 'vs/workbench/parts/tasks/common/taskSystem'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -36,16 +38,12 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { public static TelemetryEventName: string = 'taskService'; - private fileConfig: FileConfig.ExternalTaskRunnerConfiguration; private markerService: IMarkerService; private modelService: IModelService; private outputService: IOutputService; private telemetryService: ITelemetryService; private configurationResolverService: IConfigurationResolverService; - private validationStatus: ValidationStatus; - private defaultBuildTaskIdentifier: string; - private defaultTestTaskIdentifier: string; private configuration: TaskRunnerConfiguration; private outputChannel: IOutputChannel; @@ -54,18 +52,16 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { private activeTaskIdentifier: string; private activeTaskPromise: TPromise; - constructor(fileConfig: FileConfig.ExternalTaskRunnerConfiguration, markerService: IMarkerService, modelService: IModelService, telemetryService: ITelemetryService, + constructor(configuration: TaskRunnerConfiguration, markerService: IMarkerService, modelService: IModelService, telemetryService: ITelemetryService, outputService: IOutputService, configurationResolverService: IConfigurationResolverService, outputChannelId: string, clearOutput: boolean = true) { super(); - this.fileConfig = fileConfig; + this.configuration = configuration; this.markerService = markerService; this.modelService = modelService; this.outputService = outputService; this.telemetryService = telemetryService; this.configurationResolverService = configurationResolverService; - this.defaultBuildTaskIdentifier = null; - this.defaultTestTaskIdentifier = null; this.childProcess = null; this.activeTaskIdentifier = null; this.activeTaskPromise = null; @@ -75,27 +71,19 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { this.clearOutput(); } this.errorsShown = false; - let parseResult = FileConfig.parse(fileConfig, this); - this.validationStatus = parseResult.validationStatus; - this.configuration = parseResult.configuration; - this.defaultBuildTaskIdentifier = parseResult.defaultBuildTaskIdentifier; - this.defaultTestTaskIdentifier = parseResult.defaultTestTaskIdentifier; - - if (!this.validationStatus.isOK()) { - this.showOutput(); - } } public build(): ITaskExecuteResult { + let buildTaskIdentifier = this.configuration.buildTasks.length > 0 ? this.configuration.buildTasks[0] : undefined; if (this.activeTaskIdentifier) { let task = this.configuration.tasks[this.activeTaskIdentifier]; - return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultBuildTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise }; + return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === buildTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise }; } - if (!this.defaultBuildTaskIdentifier) { + if (!buildTaskIdentifier) { throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.noBuildTask', 'No task is marked as a build task in the tasks.json. Mark a task with \'isBuildCommand\'.'), TaskErrors.NoBuildTask); } - return this.executeTask(this.defaultBuildTaskIdentifier, Triggers.shortcut); + return this.executeTask(buildTaskIdentifier, Triggers.shortcut); } public rebuild(): ITaskExecuteResult { @@ -107,14 +95,15 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { } public runTest(): ITaskExecuteResult { + let testTaskIdentifier = this.configuration.testTasks.length > 0 ? this.configuration.testTasks[0] : undefined; if (this.activeTaskIdentifier) { let task = this.configuration.tasks[this.activeTaskIdentifier]; - return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultTestTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise }; + return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === testTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise }; } - if (!this.defaultTestTaskIdentifier) { + if (!testTaskIdentifier) { throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.noTestTask', 'No test task configured.'), TaskErrors.NoTestTask); } - return this.executeTask(this.defaultTestTaskIdentifier, Triggers.shortcut); + return this.executeTask(testTaskIdentifier, Triggers.shortcut); } public run(taskIdentifier: string): ITaskExecuteResult { @@ -164,9 +153,6 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { } private executeTask(taskIdentifier: string, trigger: string = Triggers.command): ITaskExecuteResult { - if (this.validationStatus.isFatal()) { - throw new TaskError(Severity.Error, nls.localize('TaskRunnerSystem.fatalError', 'The provided task configuration has validation errors. See tasks output log for details.'), TaskErrors.ConfigValidationError); - } let task = this.configuration.tasks[taskIdentifier]; if (!task) { throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.norebuild', 'No task to execute found.'), TaskErrors.TaskNotFound); @@ -205,19 +191,19 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { private doExecuteTask(task: TaskDescription, telemetryEvent: TelemetryEvent): ITaskExecuteResult { let taskSummary: ITaskSummary = {}; - let configuration = this.configuration; - if (!this.validationStatus.isOK() && !this.errorsShown) { + let commandConfig: CommandConfiguration = task.command; + if (!this.errorsShown) { this.showOutput(); this.errorsShown = true; } else { this.clearOutput(); } - let args: string[] = this.configuration.args ? this.configuration.args.slice() : []; + let args: string[] = commandConfig.args ? commandConfig.args.slice() : []; // We need to first pass the task name if (!task.suppressTaskName) { - if (this.fileConfig.taskSelector) { - args.push(this.fileConfig.taskSelector + task.name); + if (commandConfig.taskSelector) { + args.push(commandConfig.taskSelector + task.name); } else { args.push(task.name); } @@ -227,15 +213,15 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { args = args.concat(task.args); } args = this.resolveVariables(args); - let command: string = this.resolveVariable(configuration.command); - this.childProcess = new LineProcess(command, args, configuration.isShellCommand, this.resolveOptions(configuration.options)); + let command: string = this.resolveVariable(commandConfig.name); + this.childProcess = new LineProcess(command, args, commandConfig.isShellCommand, this.resolveOptions(commandConfig.options)); telemetryEvent.command = this.childProcess.getSanitizedCommand(); // we have no problem matchers defined. So show the output log if (task.showOutput === ShowOutput.Always || (task.showOutput === ShowOutput.Silent && task.problemMatchers.length === 0)) { this.showOutput(); } - if (task.echoCommand) { + if (commandConfig.echo) { let prompt: string = Platform.isWindows ? '>' : '$'; this.log(`running command${prompt} ${command} ${args.join(' ')}`); } @@ -339,8 +325,8 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { private handleError(task: TaskDescription, error: ErrorData): Promise { let makeVisible = false; if (error.error && !error.terminated) { - let args: string = this.configuration.args ? this.configuration.args.join(' ') : ''; - this.log(nls.localize('TaskRunnerSystem.childProcessError', 'Failed to launch external program {0} {1}.', this.configuration.command, args)); + let args: string = task.command.args ? task.command.args.join(' ') : ''; + this.log(nls.localize('TaskRunnerSystem.childProcessError', 'Failed to launch external program {0} {1}.', task.command.name, args)); this.outputChannel.append(error.error.message); makeVisible = true; } diff --git a/src/vs/workbench/parts/tasks/test/node/configuration.test.ts b/src/vs/workbench/parts/tasks/test/node/configuration.test.ts index 4b83891a7b0..b8fbadc93e7 100644 --- a/src/vs/workbench/parts/tasks/test/node/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/node/configuration.test.ts @@ -28,51 +28,90 @@ class ConfiguationBuilder { public result: TaskSystem.TaskRunnerConfiguration; - constructor(command: string) { + constructor() { this.result = { - command, + tasks: Object.create(null), + buildTasks: [], + testTasks: [] + }; + } + + public task(name: string, command: string): TaskBuilder { + let builder = new TaskBuilder(this, name, command); + this.result.tasks[builder.result.name] = builder.result; + return builder; + } + + public buildTask(id: string): ConfiguationBuilder { + this.result.buildTasks.push(id); + return this; + } + + public testTask(id: string): ConfiguationBuilder { + this.result.testTasks.push(id); + return this; + } +} + +class CommandConfigurationBuilder { + public result: TaskSystem.CommandConfiguration; + + constructor(public parent: TaskBuilder, command: string) { + this.result = { + name: command, isShellCommand: false, args: [], options: { cwd: '${workspaceRoot}' }, - tasks: Object.create(null) + echo: false }; } - public shell(value: boolean): ConfiguationBuilder { + public name(value: string): CommandConfigurationBuilder { + this.result.name = value; + return this; + } + + public shell(value: boolean): CommandConfigurationBuilder { this.result.isShellCommand = value; return this; } - public args(value: string[]): ConfiguationBuilder { + public args(value: string[]): CommandConfigurationBuilder { this.result.args = value; return this; } - public options(value: TaskSystem.CommandOptions): ConfiguationBuilder { + public options(value: TaskSystem.CommandOptions): CommandConfigurationBuilder { this.result.options = value; return this; } - public task(name: string): TaskBuilder { - let builder = new TaskBuilder(this, name); - this.result.tasks[builder.result.name] = builder.result; - return builder; + public echo(value: boolean): CommandConfigurationBuilder { + this.result.echo = value; + return this; + } + + public taskSelector(value: string): CommandConfigurationBuilder { + this.result.taskSelector = value; + return this; } } class TaskBuilder { public result: TaskSystem.TaskDescription; + private commandBuilder: CommandConfigurationBuilder; - constructor(public parent: ConfiguationBuilder, name: string) { + constructor(public parent: ConfiguationBuilder, name: string, command: string) { + this.commandBuilder = new CommandConfigurationBuilder(this, command); this.result = { id: name, name: name, + command: this.commandBuilder.result, showOutput: TaskSystem.ShowOutput.Always, suppressTaskName: false, - echoCommand: false, isBackground: false, promptOnClose: true, problemMatchers: [] @@ -94,11 +133,6 @@ class TaskBuilder { return this; } - public echoCommand(value: boolean): TaskBuilder { - this.result.echoCommand = value; - return this; - } - public isBackground(value: boolean): TaskBuilder { this.result.isBackground = value; return this; @@ -114,6 +148,10 @@ class TaskBuilder { this.result.problemMatchers.push(builder.result); return builder; } + + public command(): CommandConfigurationBuilder { + return this.commandBuilder; + } } class ProblemMatcherBuilder { @@ -234,8 +272,8 @@ class PatternBuilder { suite('Tasks Configuration parsing tests', () => { test('tasks: all default', () => { - let builder = new ConfiguationBuilder('tsc'); - builder.task('tsc'). + let builder = new ConfiguationBuilder(); + builder.task('tsc', 'tsc'). suppressTaskName(true); testGobalCommand( { @@ -245,10 +283,11 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global isShellCommand', () => { - let builder = new ConfiguationBuilder('tsc'); - builder.shell(true). - task('tsc'). - suppressTaskName(true); + let builder = new ConfiguationBuilder(); + builder.task('tsc', 'tsc'). + suppressTaskName(true). + command(). + shell(true); testGobalCommand( { version: '0.1.0', @@ -259,9 +298,9 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global show output silent', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. - task('tsc'). + task('tsc', 'tsc'). suppressTaskName(true). showOutput(TaskSystem.ShowOutput.Silent); testGobalCommand( @@ -275,8 +314,8 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global promptOnClose default', () => { - let builder = new ConfiguationBuilder('tsc'); - builder.task('tsc'). + let builder = new ConfiguationBuilder(); + builder.task('tsc', 'tsc'). suppressTaskName(true); testGobalCommand( { @@ -289,8 +328,8 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global promptOnClose', () => { - let builder = new ConfiguationBuilder('tsc'); - builder.task('tsc'). + let builder = new ConfiguationBuilder(); + builder.task('tsc', 'tsc'). suppressTaskName(true). promptOnClose(false); testGobalCommand( @@ -304,8 +343,8 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global promptOnClose default watching', () => { - let builder = new ConfiguationBuilder('tsc'); - builder.task('tsc'). + let builder = new ConfiguationBuilder(); + builder.task('tsc', 'tsc'). suppressTaskName(true). isBackground(true). promptOnClose(false); @@ -320,9 +359,9 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global show output never', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. - task('tsc'). + task('tsc', 'tsc'). suppressTaskName(true). showOutput(TaskSystem.ShowOutput.Never); testGobalCommand( @@ -336,11 +375,12 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global echo Command', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. - task('tsc'). + task('tsc', 'tsc'). suppressTaskName(true). - echoCommand(true); + command(). + echo(true); testGobalCommand( { version: '0.1.0', @@ -352,11 +392,12 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: global args', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. - args(['--p']). - task('tsc'). - suppressTaskName(true); + task('tsc', 'tsc'). + suppressTaskName(true). + command(). + args(['--p']); testGobalCommand( { version: '0.1.0', @@ -370,13 +411,14 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: options - cwd', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. + task('tsc', 'tsc'). + suppressTaskName(true). + command(). options({ cwd: 'myPath' - }). - task('tsc'). - suppressTaskName(true); + }); testGobalCommand( { version: '0.1.0', @@ -390,11 +432,12 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: options - env', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. - options({ cwd: '${workspaceRoot}', env: { key: 'value' } }). - task('tsc'). - suppressTaskName(true); + task('tsc', 'tsc'). + suppressTaskName(true). + command(). + options({ cwd: '${workspaceRoot}', env: { key: 'value' } }); testGobalCommand( { version: '0.1.0', @@ -411,9 +454,9 @@ suite('Tasks Configuration parsing tests', () => { test('tasks: os windows', () => { let name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; - let builder = new ConfiguationBuilder(name); + let builder = new ConfiguationBuilder(); builder. - task(name). + task(name, name). suppressTaskName(true); let external: ExternalTaskRunnerConfiguration = { version: '0.1.0', @@ -427,11 +470,12 @@ suite('Tasks Configuration parsing tests', () => { test('tasks: os windows & global isShellCommand', () => { let name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; - let builder = new ConfiguationBuilder(name); + let builder = new ConfiguationBuilder(); builder. - shell(true). - task(name). - suppressTaskName(true); + task(name, name). + suppressTaskName(true). + command(). + shell(true); let external: ExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', @@ -445,9 +489,9 @@ suite('Tasks Configuration parsing tests', () => { test('tasks: os mac', () => { let name: string = Platform.isMacintosh ? 'tsc.osx' : 'tsc'; - let builder = new ConfiguationBuilder(name); + let builder = new ConfiguationBuilder(); builder. - task(name). + task(name, name). suppressTaskName(true); let external: ExternalTaskRunnerConfiguration = { version: '0.1.0', @@ -461,9 +505,9 @@ suite('Tasks Configuration parsing tests', () => { test('tasks: os linux', () => { let name: string = Platform.isLinux ? 'tsc.linux' : 'tsc'; - let builder = new ConfiguationBuilder(name); + let builder = new ConfiguationBuilder(); builder. - task(name). + task(name, name). suppressTaskName(true); let external: ExternalTaskRunnerConfiguration = { version: '0.1.0', @@ -476,9 +520,9 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: overwrite showOutput', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. - task('tsc'). + task('tsc', 'tsc'). showOutput(Platform.isWindows ? TaskSystem.ShowOutput.Always : TaskSystem.ShowOutput.Never). suppressTaskName(true); let external: ExternalTaskRunnerConfiguration = { @@ -493,11 +537,12 @@ suite('Tasks Configuration parsing tests', () => { }); test('tasks: overwrite echo Command', () => { - let builder = new ConfiguationBuilder('tsc'); + let builder = new ConfiguationBuilder(); builder. - task('tsc'). - echoCommand(Platform.isWindows ? false : true). - suppressTaskName(true); + task('tsc', 'tsc'). + suppressTaskName(true). + command(). + echo(Platform.isWindows ? false : true); let external: ExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', @@ -537,8 +582,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName'); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc'); testConfiguration(external, builder); }); @@ -553,10 +598,10 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName'); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc'); let result = testConfiguration(external, builder); - assert.ok(result.defaultBuildTaskIdentifier); + assert.ok(result.configuration.buildTasks.length === 1); }); test('tasks: default build task', () => { @@ -569,10 +614,10 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('build'); + let builder = new ConfiguationBuilder(); + builder.task('build', 'tsc'); let result = testConfiguration(external, builder); - assert.ok(result.defaultBuildTaskIdentifier); + assert.ok(result.configuration.buildTasks.length === 1); }); test('tasks: test task', () => { @@ -586,10 +631,10 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName'); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc'); let result = testConfiguration(external, builder); - assert.ok(result.defaultTestTaskIdentifier); + assert.ok(result.configuration.testTasks.length === 1); }); test('tasks: default test task', () => { @@ -602,10 +647,10 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('test'); + let builder = new ConfiguationBuilder(); + builder.task('test', 'tsc'); let result = testConfiguration(external, builder); - assert.ok(result.defaultTestTaskIdentifier); + assert.ok(result.configuration.testTasks.length === 1); }); test('tasks: task with values', () => { @@ -622,16 +667,17 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('test'). + let builder = new ConfiguationBuilder(); + builder.task('test', 'tsc'). showOutput(TaskSystem.ShowOutput.Never). - echoCommand(true). args(['--p']). isBackground(true). - promptOnClose(false); + promptOnClose(false). + command(). + echo(true); let result = testConfiguration(external, builder); - assert.ok(result.defaultTestTaskIdentifier); + assert.ok(result.configuration.testTasks.length === 1); }); test('tasks: task inherits global values', () => { @@ -646,13 +692,14 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('test'). + let builder = new ConfiguationBuilder(); + builder.task('test', 'tsc'). showOutput(TaskSystem.ShowOutput.Never). - echoCommand(true); + command(). + echo(true); let result = testConfiguration(external, builder); - assert.ok(result.defaultTestTaskIdentifier); + assert.ok(result.configuration.testTasks.length === 1); }); test('tasks: problem matcher default', () => { @@ -670,8 +717,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').problemMatcher().pattern(/abc/); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').problemMatcher().pattern(/abc/); testConfiguration(external, builder); }); @@ -690,8 +737,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').problemMatcher().pattern(/.*/); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').problemMatcher().pattern(/.*/); testConfiguration(external, builder); }); @@ -714,8 +761,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').problemMatcher(). + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').problemMatcher(). owner('myOwner'). applyTo(ApplyToKind.closedDocuments). severity(Severity.Warning). @@ -741,8 +788,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').problemMatcher(). + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').problemMatcher(). fileLocation(FileLocationKind.Relative). filePrefix('myPath'). pattern(/abc/); @@ -769,8 +816,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').problemMatcher(). + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').problemMatcher(). pattern(/abc/).file(10).message(11).location(12).severity(13).code(14); testConfiguration(external, builder); }); @@ -798,8 +845,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').problemMatcher(). + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').problemMatcher(). pattern(/abc/).file(10).message(11). line(12).column(13).endLine(14).endColumn(15). severity(16).code(17); @@ -816,8 +863,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').promptOnClose(true); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').promptOnClose(true); testConfiguration(external, builder); }); @@ -832,8 +879,8 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').isBackground(true).promptOnClose(false); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').isBackground(true).promptOnClose(false); testConfiguration(external, builder); }); @@ -848,8 +895,61 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').promptOnClose(false); + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc').promptOnClose(false); + testConfiguration(external, builder); + }); + + test('tasks: task selector set', () => { + let external: ExternalTaskRunnerConfiguration = { + version: '0.1.0', + command: 'tsc', + taskSelector: '/t', + tasks: [ + { + taskName: 'taskName', + } + ] + }; + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc'). + command(). + taskSelector('/t'); + testConfiguration(external, builder); + }); + + test('tasks: suppress task name set', () => { + let external: ExternalTaskRunnerConfiguration = { + version: '0.1.0', + command: 'tsc', + suppressTaskName: false, + tasks: [ + { + taskName: 'taskName', + suppressTaskName: true + } + ] + }; + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc'). + suppressTaskName(true); + testConfiguration(external, builder); + }); + + test('tasks: suppress task name inerit', () => { + let external: ExternalTaskRunnerConfiguration = { + version: '0.1.0', + command: 'tsc', + suppressTaskName: true, + tasks: [ + { + taskName: 'taskName' + } + ] + }; + let builder = new ConfiguationBuilder(); + builder.task('taskName', 'tsc'). + suppressTaskName(true); testConfiguration(external, builder); }); @@ -866,9 +966,68 @@ suite('Tasks Configuration parsing tests', () => { } ] }; - let builder = new ConfiguationBuilder('tsc'); - builder.task('taskNameOne'); - builder.task('taskNameTwo'); + let builder = new ConfiguationBuilder(); + builder.task('taskNameOne', 'tsc'); + builder.task('taskNameTwo', 'tsc'); + testConfiguration(external, builder); + }); + + test('tasks: with command', () => { + let external: ExternalTaskRunnerConfiguration = { + version: '0.1.0', + tasks: [ + { + taskName: 'taskNameOne', + command: 'tsc' + } + ] + }; + let builder = new ConfiguationBuilder(); + builder.task('taskNameOne', 'tsc').suppressTaskName(true); + testConfiguration(external, builder); + }); + + test('tasks: two tasks with command', () => { + let external: ExternalTaskRunnerConfiguration = { + version: '0.1.0', + tasks: [ + { + taskName: 'taskNameOne', + command: 'tsc' + }, + { + taskName: 'taskNameTwo', + command: 'dir' + } + ] + }; + let builder = new ConfiguationBuilder(); + builder.task('taskNameOne', 'tsc').suppressTaskName(true); + builder.task('taskNameTwo', 'dir').suppressTaskName(true); + testConfiguration(external, builder); + }); + + test('tasks: with command and args', () => { + let external: ExternalTaskRunnerConfiguration = { + version: '0.1.0', + tasks: [ + { + taskName: 'taskNameOne', + command: 'tsc', + isShellCommand: true, + args: ['arg'], + options: { + cwd: 'cwd', + env: { + env: 'env' + } + } + } + ] + }; + let builder = new ConfiguationBuilder(); + builder.task('taskNameOne', 'tsc').suppressTaskName(true).command(). + shell(true).args(['arg']).options({ cwd: 'cwd', env: { env: 'env' } }); testConfiguration(external, builder); }); @@ -887,8 +1046,8 @@ suite('Tasks Configuration parsing tests', () => { function testGobalCommand(external: ExternalTaskRunnerConfiguration, builder: ConfiguationBuilder) { let result = testConfiguration(external, builder); - assert.ok(result.defaultBuildTaskIdentifier); - assert.ok(!result.defaultTestTaskIdentifier); + assert.ok(result.configuration.buildTasks.length === 1); + assert.ok(result.configuration.testTasks.length === 0); } function testConfiguration(external: ExternalTaskRunnerConfiguration, builder: ConfiguationBuilder): ParseResult { @@ -904,17 +1063,6 @@ suite('Tasks Configuration parsing tests', () => { function assertConfiguration(result: ParseResult, expected: TaskSystem.TaskRunnerConfiguration) { assert.ok(result.validationStatus.isOK()); let actual = result.configuration; - assert.strictEqual(actual.command, expected.command); - assert.strictEqual(actual.isShellCommand, expected.isShellCommand); - assert.deepEqual(actual.args, expected.args); - assert.strictEqual(typeof actual.options, typeof expected.options); - if (actual.options && expected.options) { - assert.strictEqual(actual.options.cwd, expected.options.cwd); - assert.strictEqual(typeof actual.options.env, typeof expected.options.env); - if (actual.options.env && expected.options.env) { - assert.deepEqual(actual.options.env, expected.options.env); - } - } assert.strictEqual(typeof actual.tasks, typeof expected.tasks); if (actual.tasks && expected.tasks) { // We can't compare Ids since the parser uses UUID which are random @@ -940,9 +1088,9 @@ suite('Tasks Configuration parsing tests', () => { function assertTask(actual: TaskSystem.TaskDescription, expected: TaskSystem.TaskDescription) { assert.ok(actual.id); assert.strictEqual(actual.name, expected.name, 'name'); + assertCommandConfiguration(actual.command, expected.command); assert.strictEqual(actual.showOutput, expected.showOutput, 'showOutput'); assert.strictEqual(actual.suppressTaskName, expected.suppressTaskName, 'suppressTaskName'); - assert.strictEqual(actual.echoCommand, expected.echoCommand, 'echoCommand'); assert.strictEqual(actual.isBackground, expected.isBackground, 'isBackground'); assert.strictEqual(actual.promptOnClose, expected.promptOnClose, 'promptOnClose'); assert.strictEqual(typeof actual.problemMatchers, typeof expected.problemMatchers); @@ -954,6 +1102,25 @@ suite('Tasks Configuration parsing tests', () => { } } + function assertCommandConfiguration(actual: TaskSystem.CommandConfiguration, expected: TaskSystem.CommandConfiguration) { + assert.strictEqual(typeof actual, typeof expected); + if (actual && expected) { + assert.strictEqual(actual.name, expected.name, 'name'); + assert.strictEqual(actual.isShellCommand, expected.isShellCommand, 'isShellCommand'); + assert.deepEqual(actual.args, expected.args, 'args'); + assert.strictEqual(typeof actual.options, typeof expected.options); + if (actual.options && expected.options) { + assert.strictEqual(actual.options.cwd, expected.options.cwd, 'cwd'); + assert.strictEqual(typeof actual.options.env, typeof expected.options.env, 'env'); + if (actual.options.env && expected.options.env) { + assert.deepEqual(actual.options.env, expected.options.env, 'env'); + } + } + assert.strictEqual(actual.echo, expected.echo, 'echo'); + assert.strictEqual(actual.taskSelector, expected.taskSelector, 'taskSelector'); + } + } + function assertProblemMatcher(actual: ProblemMatcher, expected: ProblemMatcher) { if (expected.owner === ProblemMatcherBuilder.DEFAULT_UUID) { try { diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 1f926d30a20..96bd0eca09f 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -28,6 +28,12 @@ export const KEYBINDING_CONTEXT_TERMINAL_TEXT_NOT_SELECTED: ContextKeyExpr = KEY export const ITerminalService = createDecorator(TERMINAL_SERVICE_ID); +export const TerminalCursorStyle = { + BLOCK: 'block', + LINE: 'line', + UNDERLINE: 'underline' +}; + export interface ITerminalConfiguration { terminal: { integrated: { @@ -43,6 +49,7 @@ export interface ITerminalConfiguration { }, rightClickCopyPaste: boolean, cursorBlinking: boolean, + cursorStyle: string, fontFamily: string, fontLigatures: boolean, fontSize: number, diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css index 73d39cf84b8..e9a3e5bfb61 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css @@ -52,19 +52,19 @@ opacity: 0 !important; } -.monaco-workbench .panel.integrated-terminal .xterm.focus .reverse-video, -.monaco-workbench .panel.integrated-terminal .xterm:focus .reverse-video { color: #CCC; } -.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.focus .reverse-video, -.vs-dark .monaco-workbench .panel.integrated-terminal .xterm:focus .reverse-video { color: #1e1e1e; } -.hc-black .monaco-workbench .panel.integrated-terminal .xterm.focus .reverse-video, -.hc-black .monaco-workbench .panel.integrated-terminal .xterm:focus .reverse-video { color: #000; } +.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .reverse-video, +.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .reverse-video { color: #CCC; } +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .reverse-video, +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .reverse-video { color: #1e1e1e; } +.hc-black .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .reverse-video, +.hc-black .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .reverse-video { color: #000; } -.monaco-workbench .panel.integrated-terminal .xterm.focus .terminal-cursor, -.monaco-workbench .panel.integrated-terminal .xterm:focus .terminal-cursor { background-color: #333; } -.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.focus .terminal-cursor, -.vs-dark .monaco-workbench .panel.integrated-terminal .xterm:focus .terminal-cursor { background-color: #CCC; } -.hc-black .monaco-workbench .panel.integrated-terminal .xterm.focus .terminal-cursor, -.hc-black .monaco-workbench .panel.integrated-terminal .xterm:focus .terminal-cursor { background-color: #FFF; } +.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor, +.monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { background-color: #333; } +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor, +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { background-color: #CCC; } +.hc-black .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor, +.hc-black .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { background-color: #FFF; } .monaco-workbench .panel.integrated-terminal .xterm:not(.focus):not(:focus) .terminal-cursor { background-color: transparent; @@ -82,12 +82,12 @@ outline-offset: -1px; } -.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink.focus .terminal-cursor, -.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:focus .terminal-cursor { animation: cursor-blink 1.2s infinite step-end; } -.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink.focus .terminal-cursor, -.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:focus .terminal-cursor { animation: cursor-blink-dark 1.2s infinite step-end; } -.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink.focus .terminal-cursor, -.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:focus .terminal-cursor { animation: cursor-blink-hc-black 1.2s infinite step-end; } +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor, +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { animation: cursor-blink 1.2s infinite step-end; } +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor, +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { animation: cursor-blink-dark 1.2s infinite step-end; } +.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .terminal-cursor, +.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-blink:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .terminal-cursor { animation: cursor-blink-hc-black 1.2s infinite step-end; } @keyframes cursor-blink { 0% { @@ -122,6 +122,62 @@ } } +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar .terminal-cursor, +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline .terminal-cursor { + position: relative; +} +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar .terminal-cursor::before, +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline .terminal-cursor::before { + content: ""; + display: block; + position: absolute; + background-color: #333; +} +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar .terminal-cursor::before, +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline .terminal-cursor::before { + background-color: #CCC; +} +.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar .terminal-cursor::before, +.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline .terminal-cursor::before { + background-color: #fff; +} +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar .terminal-cursor::before { + top: 0; + bottom: 0; + left: 0; + width: 1px; +} +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline .terminal-cursor::before { + bottom: 0; + left: 0; + right: 0; + height: 1px; +} +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before, +.monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before { + animation: xterm-cursor-non-bar-blink 1.2s infinite step-end; +} +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before, +.vs-dark .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before { + animation: xterm-cursor-non-bar-blink-dark 1.2s infinite step-end; +} +.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before, +.hc-black .monaco-workbench .panel.integrated-terminal .xterm.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before { + animation: xterm-cursor-non-bar-blink-hc-black 1.2s infinite step-end; +} +@keyframes xterm-cursor-non-bar-blink { + 0% { background-color: #333; } + 50% { background-color: transparent; } +} +@keyframes xterm-cursor-non-bar-blink-dark { + 0% { background-color: #ccc; } + 50% { background-color: transparent; } +} +@keyframes xterm-cursor-non-bar-blink-hc-black { + 0% { background-color: #fff; } + 50% { background-color: transparent; } +} + .monaco-workbench .panel.integrated-terminal .xterm .xterm-viewport { overflow-y: scroll; } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 0c1697dc9e7..9883976221c 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -11,7 +11,7 @@ import * as platform from 'vs/base/common/platform'; import nls = require('vs/nls'); import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { GlobalQuickOpenAction } from 'vs/workbench/browser/parts/quickopen/quickopen.contribution'; -import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, TERMINAL_DEFAULT_RIGHT_CLICK_COPY_PASTE } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, TERMINAL_DEFAULT_RIGHT_CLICK_COPY_PASTE, TerminalCursorStyle } from 'vs/workbench/parts/terminal/common/terminal'; import { TERMINAL_DEFAULT_SHELL_LINUX, TERMINAL_DEFAULT_SHELL_OSX, TERMINAL_DEFAULT_SHELL_WINDOWS } from 'vs/workbench/parts/terminal/electron-browser/terminal'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -103,6 +103,11 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'default': false }, + 'terminal.integrated.cursorStyle': { + 'description': nls.localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."), + 'enum': [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE], + 'default': TerminalCursorStyle.BLOCK + }, 'terminal.integrated.scrollback': { 'description': nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."), 'type': 'number', diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts index 3416940e88b..c62cdccdcf2 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts @@ -142,6 +142,11 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { return terminalConfig.terminal.integrated.cursorBlinking; } + public getCursorStyle(): string { + const terminalConfig = this._configurationService.getConfiguration(); + return terminalConfig.terminal.integrated.cursorStyle; + } + public getRightClickCopyPaste(): boolean { const config = this._configurationService.getConfiguration(); return config.terminal.integrated.rightClickCopyPaste; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index b31f2d9d79e..fed08badbed 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -528,6 +528,7 @@ export class TerminalInstance implements ITerminalInstance { public updateConfig(): void { this._setCursorBlink(this._configHelper.getCursorBlink()); + this._setCursorStyle(this._configHelper.getCursorStyle()); this._setCommandsToSkipShell(this._configHelper.getCommandsToSkipShell()); this._setScrollback(this._configHelper.getScrollback()); } @@ -539,6 +540,14 @@ export class TerminalInstance implements ITerminalInstance { } } + private _setCursorStyle(style: string): void { + if (this._xterm && this._xterm.getOption('cursorStyle') !== style) { + // 'line' is used instead of bar in VS Code to be consistent with editor.cursorStyle + const xtermOption = style === 'line' ? 'bar' : style; + this._xterm.setOption('cursorStyle', xtermOption); + } + } + private _setCommandsToSkipShell(commands: string[]): void { this._skipTerminalCommands = commands; } diff --git a/src/vs/workbench/services/configuration/common/model.ts b/src/vs/workbench/services/configuration/common/model.ts index 79d11ea950f..8eb157c6c58 100644 --- a/src/vs/workbench/services/configuration/common/model.ts +++ b/src/vs/workbench/services/configuration/common/model.ts @@ -32,12 +32,10 @@ export class WorkspaceConfigModel extends ConfigModel { } private consolidate(): void { - let result = new ConfigModel(null).merge(this.workspaceSettingsConfig); + this.doMerge(this, this.workspaceSettingsConfig); for (const configModel of this.scopedConfigs) { - result = result.merge(configModel); + this.doMerge(this, configModel); } - this._contents = result.contents; - this._overrides = result.overrides; } public get keys(): string[] { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index b75c9d6a110..cf81812a487 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -27,7 +27,7 @@ export interface IEditorPart { openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise; openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, position?: Position): TPromise; openEditors(editors: { input: IEditorInput, position: Position, options?: IEditorOptions | ITextEditorOptions }[]): TPromise; - replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[]): TPromise; + replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise; closeEditor(position: Position, input: IEditorInput): TPromise; closeEditors(position: Position, except?: IEditorInput, direction?: Direction): TPromise; closeAllEditors(except?: Position): TPromise; @@ -164,9 +164,9 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { }); } - public replaceEditors(editors: { toReplace: IResourceInput | IResourceDiffInput | IResourceSideBySideInput, replaceWith: IResourceInput | IResourceDiffInput | IResourceSideBySideInput }[]): TPromise; - public replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions }[]): TPromise; - public replaceEditors(editors: any[]): TPromise { + public replaceEditors(editors: { toReplace: IResourceInput | IResourceDiffInput | IResourceSideBySideInput, replaceWith: IResourceInput | IResourceDiffInput | IResourceSideBySideInput }[], position?: Position): TPromise; + public replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions }[], position?: Position): TPromise; + public replaceEditors(editors: any[], position?: Position): TPromise { return TPromise.join(editors.map(editor => this.createInput(editor.toReplace))).then(toReplaceInputs => { return TPromise.join(editors.map(editor => this.createInput(editor.replaceWith))).then(replaceWithInputs => { const typedReplacements: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: EditorOptions }[] = editors.map((editor, index) => { @@ -179,7 +179,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { }; }); - return this.editorPart.replaceEditors(typedReplacements); + return this.editorPart.replaceEditors(typedReplacements, position); }); }); } diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 3d22a66f41b..25ec2b3be0b 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -65,8 +65,8 @@ export interface IWorkbenchEditorService extends IEditorService { * Given a list of editors to replace, will look across all groups where this editor is open (active or hidden) * and replace it with the new editor and the provied options. */ - replaceEditors(editors: { toReplace: IResourceInput | IResourceDiffInput | IResourceSideBySideInput, replaceWith: IResourceInput | IResourceDiffInput | IResourceSideBySideInput }[]): TPromise; - replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[]): TPromise; + replaceEditors(editors: { toReplace: IResourceInput | IResourceDiffInput | IResourceSideBySideInput, replaceWith: IResourceInput | IResourceDiffInput | IResourceSideBySideInput }[], position?: Position): TPromise; + replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise; /** * Closes the editor at the provided position. diff --git a/src/vs/workbench/services/scm/common/scm.ts b/src/vs/workbench/services/scm/common/scm.ts index c0e56763a4d..5e3355cd9b2 100644 --- a/src/vs/workbench/services/scm/common/scm.ts +++ b/src/vs/workbench/services/scm/common/scm.ts @@ -50,8 +50,9 @@ export interface ISCMProvider extends IDisposable { export interface ISCMService { readonly _serviceBrand: any; - readonly activeProvider: ISCMProvider | undefined; readonly onDidChangeProvider: Event; + readonly providers: ISCMProvider[]; + activeProvider: ISCMProvider | undefined; registerSCMProvider(provider: ISCMProvider): IDisposable; } \ No newline at end of file diff --git a/src/vs/workbench/services/scm/common/scmService.ts b/src/vs/workbench/services/scm/common/scmService.ts index 1f8d62785e8..d4aeb497ec4 100644 --- a/src/vs/workbench/services/scm/common/scmService.ts +++ b/src/vs/workbench/services/scm/common/scmService.ts @@ -15,9 +15,30 @@ export class SCMService implements ISCMService { _serviceBrand; private activeProviderContextKey: IContextKey; - private providers: ISCMProvider[] = []; + private _activeProvider: ISCMProvider | undefined; + get activeProvider(): ISCMProvider | undefined { + return this._activeProvider; + } + + set activeProvider(provider: ISCMProvider | undefined) { + if (!provider) { + throw new Error('invalid provider'); + } + + if (provider && this._providers.indexOf(provider) === -1) { + throw new Error('Provider not registered'); + } + + this._activeProvider = provider; + this.activeProviderContextKey.set(provider ? provider.id : void 0); + this._onDidChangeProvider.fire(provider); + } + + private _providers: ISCMProvider[] = []; + get providers(): ISCMProvider[] { return [...this._providers]; } + private _onDidChangeProvider = new Emitter(); get onDidChangeProvider(): Event { return this._onDidChangeProvider.event; } @@ -27,38 +48,24 @@ export class SCMService implements ISCMService { this.activeProviderContextKey = contextKeyService.createKey('scm.provider', void 0); } - get activeProvider(): ISCMProvider | undefined { - return this._activeProvider; - } - - set activeProvider(provider: ISCMProvider) { - if (provider && this.providers.indexOf(provider) === -1) { - throw new Error('Provider not registered'); - } - - this._activeProvider = provider; - this.activeProviderContextKey.set(provider ? provider.id : void 0); - this._onDidChangeProvider.fire(provider); - } - registerSCMProvider(provider: ISCMProvider): IDisposable { - this.providers = [provider, ...this.providers]; + this._providers = [provider, ...this._providers]; - if (this.providers.length === 1) { + if (this._providers.length === 1) { this.activeProvider = provider; } return toDisposable(() => { - const index = this.providers.indexOf(provider); + const index = this._providers.indexOf(provider); if (index < 0) { return; } - this.providers.splice(index, 1); + this._providers.splice(index, 1); if (this.activeProvider === provider) { - this.activeProvider = this.providers[0]; + this.activeProvider = this._providers[0]; } }); } diff --git a/src/vs/workbench/services/themes/electron-browser/stylesContributions.ts b/src/vs/workbench/services/themes/electron-browser/stylesContributions.ts index 5d52b4426c6..291475edf27 100644 --- a/src/vs/workbench/services/themes/electron-browser/stylesContributions.ts +++ b/src/vs/workbench/services/themes/electron-browser/stylesContributions.ts @@ -33,6 +33,7 @@ interface ThemeGlobalSettings { referenceHighlight?: string; + linkForeground?: string; activeLinkForeground?: string; ansiBlack?: string; @@ -215,6 +216,9 @@ class EditorLinkStyleRules extends EditorStyleRules { cssRules.push(`.monaco-editor.${theme.getSelector()} .detected-link-active { color: ${new Color(theme.getGlobalSettings().activeLinkForeground)} !important; }`); cssRules.push(`.monaco-editor.${theme.getSelector()} .goto-definition-link { color: ${new Color(theme.getGlobalSettings().activeLinkForeground)} !important; }`); } + if (theme.getGlobalSettings().linkForeground) { + cssRules.push(`.monaco-editor.${theme.getSelector()} .detected-link { color: ${new Color(theme.getGlobalSettings().linkForeground)} !important; }`); + } } } diff --git a/src/vs/workbench/services/thread/electron-browser/threadService.ts b/src/vs/workbench/services/thread/electron-browser/threadService.ts index 2e080fbb5c9..924a8a88a6d 100644 --- a/src/vs/workbench/services/thread/electron-browser/threadService.ts +++ b/src/vs/workbench/services/thread/electron-browser/threadService.ts @@ -7,7 +7,7 @@ import * as strings from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IMainProcessExtHostIPC, create } from 'vs/platform/extensions/common/ipcRemoteCom'; +import { IMainProcessExtHostIPC, create } from 'vs/platform/extensions/node/ipcRemoteCom'; import { AbstractThreadService } from 'vs/workbench/services/thread/common/abstractThreadService'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -49,4 +49,4 @@ export class MainThreadService extends AbstractThreadService implements IThreadS protected _callOnRemote(proxyId: string, path: string, args: any[]): TPromise { return this.remoteCom.callOnRemote(proxyId, path, args); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/test/node/api/extHostEditors.test.ts b/src/vs/workbench/test/node/api/extHostEditors.test.ts index 84de2253a89..05490a666f9 100644 --- a/src/vs/workbench/test/node/api/extHostEditors.test.ts +++ b/src/vs/workbench/test/node/api/extHostEditors.test.ts @@ -34,6 +34,7 @@ suite('ExtHostTextEditorOptions', () => { $tryRevealRange: undefined, $trySetSelections: undefined, $tryApplyEdits: undefined, + $tryInsertSnippet: undefined }; opts = new ExtHostTextEditorOptions(mockProxy, '1', { tabSize: 4,