diff --git a/.travis.yml b/.travis.yml index b552f3ee567..15af121f5c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,8 +27,8 @@ before_install: - git submodule update --init --recursive - git clone --depth 1 https://github.com/creationix/nvm.git ./.nvm - source ./.nvm/nvm.sh - - nvm install 7.4.0 - - nvm use 7.4.0 + - nvm install 7.9.0 + - nvm use 7.9.0 - npm config set python `which python` - npm install -g gulp - if [ $TRAVIS_OS_NAME == "linux" ]; then diff --git a/appveyor.yml b/appveyor.yml index 49e80fed313..eaf86cb5b20 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ environment: VSCODE_BUILD_VERBOSE: true install: - - ps: Install-Product node 7.4.0 x64 + - ps: Install-Product node 7.9.0 x64 - npm install -g npm@4 --silent - npm install -g gulp mocha --silent diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 3c351d454ad..81dd6d2a7a8 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -479,8 +479,10 @@ gulp.task('generate-vscode-configuration', () => { return reject(new Error('$AGENT_BUILDDIRECTORY not set')); } + const userDataDir = path.join(os.tmpdir(), 'tmpuserdata'); + const extensionsDir = path.join(os.tmpdir(), 'tmpextdir'); const appPath = path.join(buildDir, 'VSCode-darwin/Visual\\ Studio\\ Code\\ -\\ Insiders.app/Contents/Resources/app/bin/code'); - const codeProc = cp.exec(`${appPath} --export-default-configuration='${allConfigDetailsPath}' --wait`); + const codeProc = cp.exec(`${appPath} --export-default-configuration='${allConfigDetailsPath}' --wait --user-data-dir='${userDataDir}' --extensions-dir='${extensionsDir}'`); const timer = setTimeout(() => { codeProc.kill(); diff --git a/build/tfs/common/common.sh b/build/tfs/common/common.sh index 52f53537943..cdd656676a3 100755 --- a/build/tfs/common/common.sh +++ b/build/tfs/common/common.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # set agent specific npm cache diff --git a/build/tfs/common/node.sh b/build/tfs/common/node.sh index 87f95a5e1d7..67f3f59552f 100755 --- a/build/tfs/common/node.sh +++ b/build/tfs/common/node.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # setup nvm diff --git a/build/tfs/linux/build-ia32.sh b/build/tfs/linux/build-ia32.sh index 0b0f1c2a458..0d9f98c692d 100755 --- a/build/tfs/linux/build-ia32.sh +++ b/build/tfs/linux/build-ia32.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash set -e ./build/tfs/linux/build.sh ia32 "$@" \ No newline at end of file diff --git a/build/tfs/linux/build-x64.sh b/build/tfs/linux/build-x64.sh index fb5b38e02b3..e193a01a5b7 100755 --- a/build/tfs/linux/build-x64.sh +++ b/build/tfs/linux/build-x64.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash set -e ./build/tfs/linux/build.sh x64 "$@" \ No newline at end of file diff --git a/build/tfs/linux/build.sh b/build/tfs/linux/build.sh index b3d1825c2d9..4af53947033 100755 --- a/build/tfs/linux/build.sh +++ b/build/tfs/linux/build.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash . ./build/tfs/common/node.sh . ./scripts/env.sh diff --git a/build/tfs/linux/ia32/run-agent.sh b/build/tfs/linux/ia32/run-agent.sh index bcf9017f3cf..efcc96632a3 100755 --- a/build/tfs/linux/ia32/run-agent.sh +++ b/build/tfs/linux/ia32/run-agent.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ ! -f pat ]; then echo "Error: file pat not found" diff --git a/build/tfs/linux/ia32/xvfb.init b/build/tfs/linux/ia32/xvfb.init index 4d77d253a26..74f6e3b2522 100644 --- a/build/tfs/linux/ia32/xvfb.init +++ b/build/tfs/linux/ia32/xvfb.init @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # /etc/rc.d/init.d/xvfbd # diff --git a/build/tfs/linux/release.sh b/build/tfs/linux/release.sh index 41f6d35e675..e4d2009a172 100755 --- a/build/tfs/linux/release.sh +++ b/build/tfs/linux/release.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash . ./scripts/env.sh . ./build/tfs/common/common.sh diff --git a/build/tfs/linux/repoapi_client.sh b/build/tfs/linux/repoapi_client.sh index b214ef10726..80de4db4a2a 100755 --- a/build/tfs/linux/repoapi_client.sh +++ b/build/tfs/linux/repoapi_client.sh @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/usr/bin/env bash -e # This is a VERY basic script for Create/Delete operations on repos and packages # cmd=$1 diff --git a/build/tfs/linux/smoketest.sh b/build/tfs/linux/smoketest.sh index 308fb047590..9866874a208 100644 --- a/build/tfs/linux/smoketest.sh +++ b/build/tfs/linux/smoketest.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e . ./build/tfs/common/node.sh diff --git a/build/tfs/linux/x64/run-agent.sh b/build/tfs/linux/x64/run-agent.sh index 76978ce2b02..1f122aa40dd 100755 --- a/build/tfs/linux/x64/run-agent.sh +++ b/build/tfs/linux/x64/run-agent.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ ! -f pat ]; then echo "Error: file pat not found" diff --git a/build/tfs/linux/x64/xvfb.init b/build/tfs/linux/x64/xvfb.init index 4d77d253a26..74f6e3b2522 100644 --- a/build/tfs/linux/x64/xvfb.init +++ b/build/tfs/linux/x64/xvfb.init @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # /etc/rc.d/init.d/xvfbd # diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index df47f973a01..7b4c807989d 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -169,7 +169,7 @@ export class CommandCenter { const opts: TextDocumentShowOptions = { preserveFocus, preview, - viewColumn: window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One + viewColumn: ViewColumn.Active }; const activeTextEditor = window.activeTextEditor; @@ -365,7 +365,7 @@ export class CommandCenter { const opts: TextDocumentShowOptions = { preserveFocus, preview: preview, - viewColumn: activeTextEditor && activeTextEditor.viewColumn || ViewColumn.One + viewColumn: ViewColumn.Active }; if (activeTextEditor && activeTextEditor.document.uri.toString() === uri.toString()) { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index e2331a308e5..7f2e3e213c6 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -382,9 +382,7 @@ export class Repository implements Disposable { const onRelevantGitChange = filterEvent(onRelevantRepositoryChange, uri => /\/\.git\//.test(uri.path)); onRelevantGitChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables); - const label = `${path.basename(repository.root)} (Git)`; - - this._sourceControl = scm.createSourceControl('git', label); + this._sourceControl = scm.createSourceControl('git', 'Git', Uri.parse(repository.root)); this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] }; this._sourceControl.quickDiffProvider = this; this.disposables.push(this._sourceControl); diff --git a/extensions/markdown/src/tableOfContentsProvider.ts b/extensions/markdown/src/tableOfContentsProvider.ts index 26e64e4d837..c93dd25f6e7 100644 --- a/extensions/markdown/src/tableOfContentsProvider.ts +++ b/extensions/markdown/src/tableOfContentsProvider.ts @@ -79,7 +79,7 @@ export class TableOfContentsProvider { } private static getHeaderText(header: string): string { - return header.replace(/^\s*#+\s*(.*?)\s*\1*$/, (_, word) => `${word.trim()}`); + return header.replace(/^\s*#+\s*(.*?)\s*#*$/, (_, word) => word.trim()); } public static slugify(header: string): string { diff --git a/extensions/npm-shrinkwrap.json b/extensions/npm-shrinkwrap.json index 2cdcc763dd5..cf47f857cf2 100644 --- a/extensions/npm-shrinkwrap.json +++ b/extensions/npm-shrinkwrap.json @@ -3,9 +3,9 @@ "version": "0.0.1", "dependencies": { "typescript": { - "version": "2.5.3-insiders.20170909", - "from": "typescript@2.5.3-insiders.20170909", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3-insiders.20170909.tgz" + "version": "2.5.3-insiders.20170919", + "from": "typescript@2.5.3-insiders.20170919", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3-insiders.20170919.tgz" } } } diff --git a/extensions/package.json b/extensions/package.json index 3ad8d484a3d..9e0ef9f6fd2 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "2.5.3-insiders.20170909" + "typescript": "2.5.3-insiders.20170919" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/typescript/snippets/typescriptreact.json b/extensions/typescript/snippets/typescriptreact.json index c0d38f23c18..894d6177276 100644 --- a/extensions/typescript/snippets/typescriptreact.json +++ b/extensions/typescript/snippets/typescriptreact.json @@ -50,7 +50,7 @@ "Import external module.": { "prefix": "import statement", "body": [ - "import ${1:name} = require('$0');" + "import { $0 } from \"${1:module}\";" ], "description": "Import external module." }, diff --git a/extensions/typescript/src/features/baseCodeLensProvider.ts b/extensions/typescript/src/features/baseCodeLensProvider.ts index 84ba69a0a96..9f5d6012d80 100644 --- a/extensions/typescript/src/features/baseCodeLensProvider.ts +++ b/extensions/typescript/src/features/baseCodeLensProvider.ts @@ -7,6 +7,7 @@ import { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange } from '../utils/convert'; export class ReferencesCodeLens extends CodeLens { constructor( @@ -99,10 +100,7 @@ export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider return null; } - const range = new Range( - span.start.line - 1, span.start.offset - 1, - span.end.line - 1, span.end.offset - 1); - + const range = tsTextSpanToVsRange(span); const text = document.getText(range); const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${(item.text || '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}(\\b|\\W)`, 'gm'); diff --git a/extensions/typescript/src/features/codeActionProvider.ts b/extensions/typescript/src/features/codeActionProvider.ts index bf04c0b069a..037a1039dc7 100644 --- a/extensions/typescript/src/features/codeActionProvider.ts +++ b/extensions/typescript/src/features/codeActionProvider.ts @@ -7,6 +7,7 @@ import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionC import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsRangeToTsFileRange } from '../utils/convert'; interface NumberSet { [key: number]: boolean; @@ -67,11 +68,7 @@ export default class TypeScriptCodeActionProvider implements CodeActionProvider formattingOptions: formattingOptions }; const args: Proto.CodeFixRequestArgs = { - file: file, - startLine: range.start.line + 1, - endLine: range.end.line + 1, - startOffset: range.start.character + 1, - endOffset: range.end.character + 1, + ...vsRangeToTsFileRange(file, range), errorCodes: Array.from(supportedActions) }; const response = await this.client.execute('getCodeFixes', args, token); @@ -112,9 +109,7 @@ export default class TypeScriptCodeActionProvider implements CodeActionProvider for (const change of action.changes) { for (const textChange of change.textChanges) { workspaceEdit.replace(this.client.asUrl(change.fileName), - new Range( - textChange.start.line - 1, textChange.start.offset - 1, - textChange.end.line - 1, textChange.end.offset - 1), + tsTextSpanToVsRange(textChange), textChange.newText); } } diff --git a/extensions/typescript/src/features/completionItemProvider.ts b/extensions/typescript/src/features/completionItemProvider.ts index 1fbcb707863..e91e71ac9fd 100644 --- a/extensions/typescript/src/features/completionItemProvider.ts +++ b/extensions/typescript/src/features/completionItemProvider.ts @@ -9,8 +9,9 @@ import { ITypescriptServiceClient } from '../typescriptService'; import TypingsStatus from '../utils/typingsStatus'; import * as PConst from '../protocol.const'; -import { CompletionEntry, CompletionsRequestArgs, CompletionDetailsRequestArgs, CompletionEntryDetails, FileLocationRequestArgs } from '../protocol'; +import { CompletionEntry, CompletionsRequestArgs, CompletionDetailsRequestArgs, CompletionEntryDetails } from '../protocol'; import * as Previewer from './previewer'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; let localize = nls.loadMessageBundle(); @@ -32,7 +33,7 @@ class MyCompletionItem extends CompletionItem { let span: protocol.TextSpan = entry.replacementSpan; // The indexing for the range returned by the server uses 1-based indexing. // We convert to 0-based indexing. - this.textEdit = TextEdit.replace(new Range(span.start.line - 1, span.start.offset - 1, span.end.line - 1, span.end.offset - 1), entry.name); + this.textEdit = TextEdit.replace(tsTextSpanToVsRange(span), entry.name); } else { // Try getting longer, prefix based range for completions that span words const wordRange = document.getWordRangeAtPosition(position); @@ -174,12 +175,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP if (!file) { return Promise.resolve([]); } - const args: CompletionsRequestArgs = { - file: file, - line: position.line + 1, - offset: position.character + 1 - }; - + const args: CompletionsRequestArgs = vsPositionToTsFileLocation(file, position); return this.client.execute('completions', args, token).then((msg) => { // This info has to come from the tsserver. See https://github.com/Microsoft/TypeScript/issues/2831 // let isMemberCompletion = false; @@ -238,9 +234,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP return null; } const args: CompletionDetailsRequestArgs = { - file: filepath, - line: item.position.line + 1, - offset: item.position.character + 1, + ...vsPositionToTsFileLocation(filepath, item.position), entryNames: [item.label] }; return this.client.execute('completionEntryDetails', args, token).then((response) => { @@ -269,11 +263,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP } private isValidFunctionCompletionContext(filepath: string, position: Position): Promise { - const args: FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = vsPositionToTsFileLocation(filepath, position); // Workaround for https://github.com/Microsoft/TypeScript/issues/12677 // Don't complete function calls inside of destructive assigments or imports return this.client.execute('quickinfo', args).then(infoResponse => { diff --git a/extensions/typescript/src/features/definitionProviderBase.ts b/extensions/typescript/src/features/definitionProviderBase.ts index 78768df2fee..bf5ffabd704 100644 --- a/extensions/typescript/src/features/definitionProviderBase.ts +++ b/extensions/typescript/src/features/definitionProviderBase.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Position, Range, CancellationToken, Location } from 'vscode'; +import { TextDocument, Position, CancellationToken, Location } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptDefinitionProviderBase { constructor( @@ -22,11 +23,7 @@ export default class TypeScriptDefinitionProviderBase { if (!filepath) { return Promise.resolve(null); } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = vsPositionToTsFileLocation(filepath, position); return this.client.execute(definitionType, args, token).then(response => { const locations: Proto.FileSpan[] = (response && response.body) || []; if (!locations || locations.length === 0) { @@ -37,7 +34,7 @@ export default class TypeScriptDefinitionProviderBase { if (resource === null) { return null; } else { - return new Location(resource, new Range(location.start.line - 1, location.start.offset - 1, location.end.line - 1, location.end.offset - 1)); + return new Location(resource, tsTextSpanToVsRange(location)); } }).filter(x => x !== null) as Location[]; }, () => { diff --git a/extensions/typescript/src/features/documentHighlightProvider.ts b/extensions/typescript/src/features/documentHighlightProvider.ts index 15224ca2c2e..d91cb8fd90b 100644 --- a/extensions/typescript/src/features/documentHighlightProvider.ts +++ b/extensions/typescript/src/features/documentHighlightProvider.ts @@ -5,8 +5,8 @@ import { DocumentHighlightProvider, DocumentHighlight, DocumentHighlightKind, TextDocument, Position, Range, CancellationToken } from 'vscode'; -import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptDocumentHighlightProvider implements DocumentHighlightProvider { @@ -18,11 +18,7 @@ export default class TypeScriptDocumentHighlightProvider implements DocumentHigh if (!filepath) { return Promise.resolve([]); } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = vsPositionToTsFileLocation(filepath, position); return this.client.execute('occurrences', args, token).then((response): DocumentHighlight[] => { let data = response.body; if (data && data.length) { @@ -37,10 +33,10 @@ export default class TypeScriptDocumentHighlightProvider implements DocumentHigh return []; } } - return data.map((item) => { - return new DocumentHighlight(new Range(item.start.line - 1, item.start.offset - 1, item.end.line - 1, item.end.offset - 1), - item.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Read); - }); + return data.map(item => + new DocumentHighlight( + tsTextSpanToVsRange(item), + item.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Read)); } return []; }, () => { diff --git a/extensions/typescript/src/features/documentSymbolProvider.ts b/extensions/typescript/src/features/documentSymbolProvider.ts index 456776cf3a6..bf289d24ee2 100644 --- a/extensions/typescript/src/features/documentSymbolProvider.ts +++ b/extensions/typescript/src/features/documentSymbolProvider.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument, Range, Location, CancellationToken, Uri } from 'vscode'; +import { DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument, Location, CancellationToken, Uri } from 'vscode'; import * as Proto from '../protocol'; import * as PConst from '../protocol.const'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange } from '../utils/convert'; const outlineTypeTable: { [kind: string]: SymbolKind } = Object.create(null); outlineTypeTable[PConst.Kind.module] = SymbolKind.Module; @@ -25,9 +26,6 @@ outlineTypeTable[PConst.Kind.variable] = SymbolKind.Variable; outlineTypeTable[PConst.Kind.function] = SymbolKind.Function; outlineTypeTable[PConst.Kind.localFunction] = SymbolKind.Function; -function textSpan2Range(value: Proto.TextSpan): Range { - return new Range(value.start.line - 1, value.start.offset - 1, value.end.line - 1, value.end.offset - 1); -} export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolProvider { public constructor( @@ -73,7 +71,7 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP let result = new SymbolInformation(item.text, outlineTypeTable[item.kind as string] || SymbolKind.Variable, containerLabel ? containerLabel : '', - new Location(resource, textSpan2Range(item.spans[0]))); + new Location(resource, tsTextSpanToVsRange(item.spans[0]))); foldingMap[key] = result; bucket.push(result); } @@ -88,7 +86,7 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP const result = new SymbolInformation(item.text, outlineTypeTable[item.kind as string] || SymbolKind.Variable, containerLabel ? containerLabel : '', - new Location(resource, textSpan2Range(item.spans[0])) + new Location(resource, tsTextSpanToVsRange(item.spans[0])) ); if (item.childItems && item.childItems.length > 0) { for (const child of item.childItems) { diff --git a/extensions/typescript/src/features/formattingProvider.ts b/extensions/typescript/src/features/formattingProvider.ts index 1c5e7866b8f..b4f26fdcd0a 100644 --- a/extensions/typescript/src/features/formattingProvider.ts +++ b/extensions/typescript/src/features/formattingProvider.ts @@ -7,6 +7,7 @@ import { workspace as Workspace, DocumentRangeFormattingEditProvider, OnTypeForm import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange } from '../utils/convert'; interface Configuration { enable: boolean; @@ -205,8 +206,7 @@ export class TypeScriptFormattingProvider implements DocumentRangeFormattingEdit } private codeEdit2SingleEditOperation(edit: Proto.CodeEdit): TextEdit { - return new TextEdit(new Range(edit.start.line - 1, edit.start.offset - 1, edit.end.line - 1, edit.end.offset - 1), - edit.newText); + return new TextEdit(tsTextSpanToVsRange(edit), edit.newText); } private getFormatOptions(options: FormattingOptions): Proto.FormatCodeSettings { diff --git a/extensions/typescript/src/features/hoverProvider.ts b/extensions/typescript/src/features/hoverProvider.ts index 9fd1483b625..9ee37c5954b 100644 --- a/extensions/typescript/src/features/hoverProvider.ts +++ b/extensions/typescript/src/features/hoverProvider.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { HoverProvider, Hover, TextDocument, Position, Range, CancellationToken } from 'vscode'; +import { HoverProvider, Hover, TextDocument, Position, CancellationToken } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; import { tagsMarkdownPreview } from './previewer'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptHoverProvider implements HoverProvider { @@ -19,19 +20,14 @@ export default class TypeScriptHoverProvider implements HoverProvider { if (!filepath) { return undefined; } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; - + const args = vsPositionToTsFileLocation(filepath, position); try { const response = await this.client.execute('quickinfo', args, token); if (response && response.body) { const data = response.body; return new Hover( TypeScriptHoverProvider.getContents(data), - new Range(data.start.line - 1, data.start.offset - 1, data.end.line - 1, data.end.offset - 1)); + tsTextSpanToVsRange(data)); } } catch (e) { // noop diff --git a/extensions/typescript/src/features/implementationsCodeLensProvider.ts b/extensions/typescript/src/features/implementationsCodeLensProvider.ts index f2901a29f47..b51da2970a2 100644 --- a/extensions/typescript/src/features/implementationsCodeLensProvider.ts +++ b/extensions/typescript/src/features/implementationsCodeLensProvider.ts @@ -9,6 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -35,11 +36,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { const codeLens = inputCodeLens as ReferencesCodeLens; - const args: Proto.FileLocationRequestArgs = { - file: codeLens.file, - line: codeLens.range.start.line + 1, - offset: codeLens.range.start.character + 1 - }; + const args = vsPositionToTsFileLocation(codeLens.file, codeLens.range.start); return this.client.execute('implementation', args, token).then(response => { if (!response || !response.body) { throw codeLens; @@ -50,9 +47,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip // Only take first line on implementation: https://github.com/Microsoft/vscode/issues/23924 new Location(this.client.asUrl(reference.file), reference.start.line === reference.end.line - ? new Range( - reference.start.line - 1, reference.start.offset - 1, - reference.end.line - 1, reference.end.offset - 1) + ? tsTextSpanToVsRange(reference) : new Range( reference.start.line - 1, reference.start.offset - 1, reference.start.line, 0))) diff --git a/extensions/typescript/src/features/jsDocCompletionProvider.ts b/extensions/typescript/src/features/jsDocCompletionProvider.ts index 50f0147cbc0..d9c87da1cc0 100644 --- a/extensions/typescript/src/features/jsDocCompletionProvider.ts +++ b/extensions/typescript/src/features/jsDocCompletionProvider.ts @@ -6,9 +6,10 @@ import { Position, Range, CompletionItemProvider, CompletionItemKind, TextDocument, CancellationToken, CompletionItem, window, Uri, ProviderResult, TextEditor, SnippetString, workspace } from 'vscode'; import { ITypescriptServiceClient } from '../typescriptService'; -import { FileLocationRequestArgs, DocCommandTemplateResponse } from '../protocol'; +import { DocCommandTemplateResponse } from '../protocol'; import * as nls from 'vscode-nls'; +import { vsPositionToTsFileLocation } from '../utils/convert'; const localize = nls.loadMessageBundle(); const configurationNamespace = 'jsDocCompletion'; @@ -118,11 +119,7 @@ export class TryCompleteJsDocCommand { } private tryInsertJsDocFromTemplate(editor: TextEditor, file: string, position: Position): Promise { - const args: FileLocationRequestArgs = { - file: file, - line: position.line + 1, - offset: position.character + 1 - }; + const args = vsPositionToTsFileLocation(file, position); return Promise.race([ this.lazyClient().execute('docCommentTemplate', args), new Promise((_, reject) => setTimeout(reject, 250)) diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 1b9062998b5..d87d6c01076 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -5,10 +5,11 @@ 'use strict'; -import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionContext, Command, commands, workspace, WorkspaceEdit, window, QuickPickItem, Selection, Position } from 'vscode'; +import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionContext, Command, commands, workspace, WorkspaceEdit, window, QuickPickItem, Selection } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsRangeToTsFileRange, tsLocationToVsPosition } from '../utils/convert'; export default class TypeScriptRefactorProvider implements CodeActionProvider { @@ -24,7 +25,6 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { commands.registerCommand(this.doRefactorCommandId, this.doRefactoring, this); commands.registerCommand(this.selectRefactorCommandId, this.selectRefactoring, this); - } public async provideCodeActions( @@ -42,14 +42,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { return []; } - const args: Proto.GetApplicableRefactorsRequestArgs = { - file: file, - startLine: range.start.line + 1, - startOffset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1 - }; - + const args: Proto.GetApplicableRefactorsRequestArgs = vsRangeToTsFileRange(file, range); try { const response = await this.client.execute('getApplicableRefactors', args, token); if (!response || !response.body) { @@ -85,9 +78,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { for (const edit of edits) { for (const textChange of edit.textChanges) { workspaceEdit.replace(this.client.asUrl(edit.fileName), - new Range( - textChange.start.line - 1, textChange.start.offset - 1, - textChange.end.line - 1, textChange.end.offset - 1), + tsTextSpanToVsRange(textChange), textChange.newText); } } @@ -108,13 +99,9 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { private async doRefactoring(file: string, refactor: string, action: string, range: Range): Promise { const args: Proto.GetEditsForRefactorRequestArgs = { - file, + ...vsRangeToTsFileRange(file, range), refactor, - action, - startLine: range.start.line + 1, - startOffset: range.start.character + 1, - endLine: range.end.line + 1, - endOffset: range.end.character + 1 + action }; const response = await this.client.execute('getEditsForRefactor', args); @@ -130,7 +117,7 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { const renameLocation = response.body.renameLocation; if (renameLocation) { if (window.activeTextEditor && window.activeTextEditor.document.uri.fsPath === file) { - const pos = new Position(renameLocation.line - 1, renameLocation.offset - 1); + const pos = tsLocationToVsPosition(renameLocation); window.activeTextEditor.selection = new Selection(pos, pos); await commands.executeCommand('editor.action.rename'); } diff --git a/extensions/typescript/src/features/referenceProvider.ts b/extensions/typescript/src/features/referenceProvider.ts index 9f7ccf9f49a..1bce17a5b65 100644 --- a/extensions/typescript/src/features/referenceProvider.ts +++ b/extensions/typescript/src/features/referenceProvider.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ReferenceProvider, Location, TextDocument, Position, Range, CancellationToken } from 'vscode'; +import { ReferenceProvider, Location, TextDocument, Position, CancellationToken } from 'vscode'; -import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptReferenceSupport implements ReferenceProvider { public constructor( @@ -17,11 +17,7 @@ export default class TypeScriptReferenceSupport implements ReferenceProvider { if (!filepath) { return Promise.resolve([]); } - const args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args = vsPositionToTsFileLocation(filepath, position); const apiVersion = this.client.apiVersion; return this.client.execute('references', args, token).then((msg) => { const result: Location[] = []; @@ -35,9 +31,7 @@ export default class TypeScriptReferenceSupport implements ReferenceProvider { continue; } const url = this.client.asUrl(ref.file); - const location = new Location( - url, - new Range(ref.start.line - 1, ref.start.offset - 1, ref.end.line - 1, ref.end.offset - 1)); + const location = new Location(url, tsTextSpanToVsRange(ref)); result.push(location); } return result; diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts index 6ce55aa371f..641dc28cf8a 100644 --- a/extensions/typescript/src/features/referencesCodeLensProvider.ts +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -9,6 +9,7 @@ import * as PConst from '../protocol.const'; import { TypeScriptBaseCodeLensProvider, ReferencesCodeLens } from './baseCodeLensProvider'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -35,11 +36,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { const codeLens = inputCodeLens as ReferencesCodeLens; - const args: Proto.FileLocationRequestArgs = { - file: codeLens.file, - line: codeLens.range.start.line + 1, - offset: codeLens.range.start.character + 1 - }; + const args = vsPositionToTsFileLocation(codeLens.file, codeLens.range.start); return this.client.execute('references', args, token).then(response => { if (!response || !response.body) { throw codeLens; @@ -47,10 +44,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase const locations = response.body.refs .map(reference => - new Location(this.client.asUrl(reference.file), - new Range( - reference.start.line - 1, reference.start.offset - 1, - reference.end.line - 1, reference.end.offset - 1))) + new Location(this.client.asUrl(reference.file), tsTextSpanToVsRange(reference))) .filter(location => // Exclude original definition from references !(location.uri.toString() === codeLens.document.toString() && diff --git a/extensions/typescript/src/features/renameProvider.ts b/extensions/typescript/src/features/renameProvider.ts index 291be640ca7..26bbb6a29ea 100644 --- a/extensions/typescript/src/features/renameProvider.ts +++ b/extensions/typescript/src/features/renameProvider.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RenameProvider, WorkspaceEdit, TextDocument, Position, Range, CancellationToken } from 'vscode'; +import { RenameProvider, WorkspaceEdit, TextDocument, Position, CancellationToken } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange, vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptRenameProvider implements RenameProvider { public constructor( @@ -24,9 +25,7 @@ export default class TypeScriptRenameProvider implements RenameProvider { } const args: Proto.RenameRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1, + ...vsPositionToTsFileLocation(filepath, position), findInStrings: false, findInComments: false }; @@ -49,9 +48,7 @@ export default class TypeScriptRenameProvider implements RenameProvider { continue; } for (const textSpan of spanGroup.locs) { - result.replace(resource, - new Range(textSpan.start.line - 1, textSpan.start.offset - 1, textSpan.end.line - 1, textSpan.end.offset - 1), - newName); + result.replace(resource, tsTextSpanToVsRange(textSpan), newName); } } return result; diff --git a/extensions/typescript/src/features/signatureHelpProvider.ts b/extensions/typescript/src/features/signatureHelpProvider.ts index d4fb33e2bfb..ea0de1f04d5 100644 --- a/extensions/typescript/src/features/signatureHelpProvider.ts +++ b/extensions/typescript/src/features/signatureHelpProvider.ts @@ -8,6 +8,7 @@ import { SignatureHelpProvider, SignatureHelp, SignatureInformation, ParameterIn import * as Previewer from './previewer'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { vsPositionToTsFileLocation } from '../utils/convert'; export default class TypeScriptSignatureHelpProvider implements SignatureHelpProvider { @@ -19,11 +20,7 @@ export default class TypeScriptSignatureHelpProvider implements SignatureHelpPro if (!filepath) { return Promise.resolve(null); } - const args: Proto.SignatureHelpRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; + const args: Proto.SignatureHelpRequestArgs = vsPositionToTsFileLocation(filepath, position); return this.client.execute('signatureHelp', args, token).then((response) => { const info = response.body; if (!info) { diff --git a/extensions/typescript/src/features/workspaceSymbolProvider.ts b/extensions/typescript/src/features/workspaceSymbolProvider.ts index ab2329501a3..8885eb877ac 100644 --- a/extensions/typescript/src/features/workspaceSymbolProvider.ts +++ b/extensions/typescript/src/features/workspaceSymbolProvider.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, window, Uri, WorkspaceSymbolProvider, SymbolInformation, SymbolKind, Range, Location, CancellationToken } from 'vscode'; +import { workspace, window, Uri, WorkspaceSymbolProvider, SymbolInformation, SymbolKind, Location, CancellationToken } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import { tsTextSpanToVsRange } from '../utils/convert'; function getSymbolKind(item: Proto.NavtoItem): SymbolKind { switch (item.kind) { @@ -67,7 +68,7 @@ export default class TypeScriptWorkspaceSymbolProvider implements WorkspaceSymbo if (!item.containerName && item.kind === 'alias') { continue; } - const range = new Range(item.start.line - 1, item.start.offset - 1, item.end.line - 1, item.end.offset - 1); + const range = tsTextSpanToVsRange(item); let label = item.name; if (item.kind === 'method' || item.kind === 'function') { label += '()'; diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 9c472f187f0..87c312eb268 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -33,6 +33,7 @@ import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'; import VersionStatus from './utils/versionStatus'; import { getContributedTypeScriptServerPlugins, TypeScriptServerPlugin } from './utils/plugins'; import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './utils/tsconfig'; +import { tsLocationToVsPosition } from './utils/convert'; interface LanguageDescription { id: string; @@ -681,7 +682,7 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost { const result: Diagnostic[] = []; for (let diagnostic of diagnostics) { const { start, end, text } = diagnostic; - const range = new Range(start.line - 1, start.offset - 1, end.line - 1, end.offset - 1); + const range = new Range(tsLocationToVsPosition(start), tsLocationToVsPosition(end)); const converted = new Diagnostic(range, text); converted.severity = this.getDiagnosticSeverity(diagnostic); converted.source = diagnostic.source || source; diff --git a/extensions/typescript/src/utils/convert.ts b/extensions/typescript/src/utils/convert.ts new file mode 100644 index 00000000000..299597f34d5 --- /dev/null +++ b/extensions/typescript/src/utils/convert.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as Proto from '../protocol'; + + +export const tsTextSpanToVsRange = (span: Proto.TextSpan) => + new vscode.Range( + span.start.line - 1, span.start.offset - 1, + span.end.line - 1, span.end.offset - 1); + +export const tsLocationToVsPosition = (tslocation: Proto.Location) => + new vscode.Position(tslocation.line - 1, tslocation.offset - 1); + +export const vsPositionToTsFileLocation = (file: string, position: vscode.Position): Proto.FileLocationRequestArgs => ({ + file, + line: position.line + 1, + offset: position.character + 1 +}); + +export const vsRangeToTsFileRange = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ + file, + startLine: range.start.line + 1, + startOffset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 +}); \ No newline at end of file diff --git a/extensions/vscode-api-tests/src/window.test.ts b/extensions/vscode-api-tests/src/window.test.ts index a9d78e8cad3..275ac5ee5ca 100644 --- a/extensions/vscode-api-tests/src/window.test.ts +++ b/extensions/vscode-api-tests/src/window.test.ts @@ -138,6 +138,26 @@ suite('window namespace tests', () => { assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.One); }); + test('issue #27408 - showTextDocument & vscode.diff always default to ViewColumn.One', async () => { + const [docA, docB, docC] = await Promise.all([ + workspace.openTextDocument(await createRandomFile()), + workspace.openTextDocument(await createRandomFile()), + workspace.openTextDocument(await createRandomFile()) + ]); + + await window.showTextDocument(docA, ViewColumn.One); + await window.showTextDocument(docB, ViewColumn.Two); + + assert.ok(window.activeTextEditor); + assert.ok(window.activeTextEditor!.document === docB); + assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + + await window.showTextDocument(docC, ViewColumn.Active); + + assert.ok(window.activeTextEditor!.document === docC); + assert.equal(window.activeTextEditor!.viewColumn, ViewColumn.Two); + }); + test('issue #5362 - Incorrect TextEditor passed by onDidChangeTextEditorSelection', (done) => { const file10Path = join(workspace.rootPath || '', './10linefile.ts'); const file30Path = join(workspace.rootPath || '', './30linefile.ts'); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ad603585240..e5f51982d1d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -574,7 +574,7 @@ "xterm": { "version": "3.0.0", "from": "Tyriar/xterm.js#vscode-release/1.17", - "resolved": "git+https://github.com/Tyriar/xterm.js.git#35088059e61ba654ac78df453633c7a9272ed8bd" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#875b219802d116106d3e05a1731bf895bc95851b" }, "yauzl": { "version": "2.8.0", diff --git a/resources/linux/debian/postrm.template b/resources/linux/debian/postrm.template index c43a2b16ae3..1dfa892a0ea 100755 --- a/resources/linux/debian/postrm.template +++ b/resources/linux/debian/postrm.template @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. diff --git a/scripts/env.sh b/scripts/env.sh index 35d09f66bb2..f530bf28369 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash export npm_config_disturl=https://atom.io/download/electron export npm_config_target=$(node -p "require('./package.json').electronVersion") export npm_config_runtime=electron diff --git a/scripts/npm.sh b/scripts/npm.sh index 69c6d0c48ae..02268eafd6e 100755 --- a/scripts/npm.sh +++ b/scripts/npm.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index d0154a4101d..2bfd21a20c7 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/scripts/test-mocha.sh b/scripts/test-mocha.sh index 9aa16fa3241..5d1d71a2da2 100755 --- a/scripts/test-mocha.sh +++ b/scripts/test-mocha.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } diff --git a/scripts/test.sh b/scripts/test.sh index ce1e5e11856..157c6da2cc7 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index 9f622f16fdb..95cf18196c1 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { escape } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import * as objects from 'vs/base/common/objects'; -import { expand as expandOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; +import { render as renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; export interface IHighlight { start: number; @@ -64,19 +63,19 @@ export class HighlightedLabel implements IDisposable { } if (pos < highlight.start) { htmlContent.push(''); - htmlContent.push(expandOcticons(escape(this.text.substring(pos, highlight.start)))); + htmlContent.push(renderOcticons(this.text.substring(pos, highlight.start))); htmlContent.push(''); pos = highlight.end; } htmlContent.push(''); - htmlContent.push(expandOcticons(escape(this.text.substring(highlight.start, highlight.end)))); + htmlContent.push(renderOcticons(this.text.substring(highlight.start, highlight.end))); htmlContent.push(''); pos = highlight.end; } if (pos < this.text.length) { htmlContent.push(''); - htmlContent.push(expandOcticons(escape(this.text.substring(pos)))); + htmlContent.push(renderOcticons(this.text.substring(pos))); htmlContent.push(''); } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 18b09e697e8..603024b7c73 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -179,7 +179,13 @@ export class InputBox extends Widget { }); } - setTimeout(() => this.updateMirror(), 0); + setTimeout(() => { + if (!this.input) { + return; + } + + this.updateMirror(); + }, 0); // Support actions if (this.options.actions) { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 9eb101a4f35..cbba77216b3 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -398,7 +398,7 @@ class MouseController implements IDisposable { if (isSelectionRangeChangeEvent(e) && reference !== undefined) { const min = Math.min(reference, focus); const max = Math.max(reference, focus); - const rangeSelection = range(max + 1, min); + const rangeSelection = range(min, max + 1); const selection = this.list.getSelection(); const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference); diff --git a/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts b/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts index 1f628552999..46bf06ccb39 100644 --- a/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts +++ b/src/vs/base/browser/ui/octiconLabel/octiconLabel.mock.ts @@ -5,8 +5,8 @@ import octiconLabel = require('vs/base/browser/ui/octiconLabel/octiconLabel'); import { escape } from 'vs/base/common/strings'; -function expand(text: string): string { - return text; +function render(text: string): string { + return escape(text); } class MockOcticonLabel { @@ -18,16 +18,13 @@ class MockOcticonLabel { } set text(text: string) { - let innerHTML = text || ''; - innerHTML = escape(innerHTML); - innerHTML = expand(innerHTML); - this._container.innerHTML = innerHTML; + this._container.innerHTML = render(text || ''); } } var mock: typeof octiconLabel = { - expand: expand, + render: render, OcticonLabel: MockOcticonLabel }; export = mock; \ No newline at end of file diff --git a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts index 50f4f0dbc76..e7339c58bbd 100644 --- a/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts +++ b/src/vs/base/browser/ui/octiconLabel/octiconLabel.ts @@ -8,12 +8,16 @@ import 'vs/css!./octicons/octicons'; import 'vs/css!./octicons/octicons-animations'; import { escape } from 'vs/base/common/strings'; -export function expand(text: string): string { +function expand(text: string): string { return text.replace(/\$\(((.+?)(~(.*?))?)\)/g, (match, g1, name, g3, animation) => { return ``; }); } +export function render(label: string): string { + return expand(escape(label)); +} + export class OcticonLabel { private _container: HTMLElement; @@ -23,13 +27,10 @@ export class OcticonLabel { } set text(text: string) { - let innerHTML = text || ''; - innerHTML = escape(innerHTML); - innerHTML = expand(innerHTML); - this._container.innerHTML = innerHTML; + this._container.innerHTML = render(text || ''); } set title(title: string) { this._container.title = title; } -} +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index 338d07b4681..4fdafd509a4 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -268,6 +268,10 @@ export class Sash extends EventEmitter { this.isDisabled = true; } + get enabled(): boolean { + return !this.isDisabled; + } + public dispose(): void { if (this.$e) { this.$e.destroy(); diff --git a/src/vs/base/browser/ui/splitview/panelview.css b/src/vs/base/browser/ui/splitview/panelview.css new file mode 100644 index 00000000000..6dfe2379b16 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/panelview.css @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-panel-view { + width: 100%; + height: 100%; +} + +.monaco-panel-view .panel { + overflow: hidden; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.monaco-panel-view .panel > .panel-header { + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + padding-left: 20px; + overflow: hidden; + display: flex; + cursor: pointer; +} + +.monaco-panel-view .panel > .panel-header { + background-image: url('arrow-collapse.svg'); + background-position: 2px center; + background-repeat: no-repeat; +} + +.monaco-panel-view .panel > .panel-header.expanded { + background-image: url('arrow-expand.svg'); + background-position: 2px center; + background-repeat: no-repeat; +} + +.vs-dark .monaco-panel-view .panel > .panel-header { + background-image: url('arrow-collapse-dark.svg'); +} + +.vs-dark .monaco-panel-view .panel > .panel-header.expanded { + background-image: url('arrow-expand-dark.svg'); +} + +/* TODO: actions should be part of the panel, but they aren't yet */ +.monaco-panel-view .panel > .panel-header > .actions { + display: none; + flex: 1; +} + +/* TODO: actions should be part of the panel, but they aren't yet */ +.monaco-panel-view .panel:hover > .panel-header > .actions, +.monaco-panel-view .panel > .panel-header.focused > .actions { + display: initial; +} + +/* TODO: actions should be part of the panel, but they aren't yet */ +.monaco-panel-view .panel > .panel-header > .actions .action-label { + width: 28px; + height: 22px; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; + margin-right: 0; +} + +/* Bold font style does not go well with CJK fonts */ +.monaco-panel-view:lang(zh-Hans) .panel > .panel-header, +.monaco-panel-view:lang(zh-Hant) .panel > .panel-header, +.monaco-panel-view:lang(ja) .panel > .panel-header, +.monaco-panel-view:lang(ko) .panel > .panel-header { + font-weight: normal; +} + +.monaco-panel-view .panel > .panel-header.hidden { + display: none; +} + +.monaco-panel-view .panel > .panel-body { + overflow: hidden; + flex: 1; +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts new file mode 100644 index 00000000000..8b02b510168 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -0,0 +1,398 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/css!./panelview'; +import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; +import Event, { Emitter, chain } from 'vs/base/common/event'; +import { domEvent } from 'vs/base/browser/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { $, append, addClass, removeClass, toggleClass, trackFocus } from 'vs/base/browser/dom'; +import { firstIndex } from 'vs/base/common/arrays'; +import { Color, RGBA } from 'vs/base/common/color'; +import { SplitView, IView } from './splitview2'; + +export interface IPanelOptions { + ariaHeaderLabel?: string; + minimumBodySize?: number; + maximumBodySize?: number; + expanded?: boolean; +} + +export interface IPanelStyles { + dropBackground?: Color; + headerForeground?: Color; + headerBackground?: Color; + headerHighContrastBorder?: Color; +} + +export abstract class Panel implements IView { + + private static HEADER_SIZE = 22; + + private _expanded: boolean; + private _headerVisible = true; + private _onDidChange = new Emitter(); + private _minimumBodySize: number; + private _maximumBodySize: number; + private ariaHeaderLabel: string; + + private header: HTMLElement; + protected disposables: IDisposable[] = []; + + get draggable(): HTMLElement { + return this.header; + } + + private _dropBackground: Color | undefined; + get dropBackground(): Color | undefined { + return this._dropBackground; + } + + get minimumBodySize(): number { + return this._minimumBodySize; + } + + set minimumBodySize(size: number) { + this._minimumBodySize = size; + this._onDidChange.fire(); + } + + get maximumBodySize(): number { + return this._maximumBodySize; + } + + set maximumBodySize(size: number) { + this._maximumBodySize = size; + this._onDidChange.fire(); + } + + private get headerSize(): number { + return this.headerVisible ? Panel.HEADER_SIZE : 0; + } + + get minimumSize(): number { + const headerSize = this.headerSize; + const expanded = !this.headerVisible || this.expanded; + const minimumBodySize = expanded ? this._minimumBodySize : 0; + + return headerSize + minimumBodySize; + } + + get maximumSize(): number { + const headerSize = this.headerSize; + const expanded = !this.headerVisible || this.expanded; + const maximumBodySize = expanded ? this._maximumBodySize : 0; + + return headerSize + maximumBodySize; + } + + readonly onDidChange: Event = this._onDidChange.event; + + constructor(options: IPanelOptions = {}) { + this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; + this.ariaHeaderLabel = options.ariaHeaderLabel || ''; + this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 120; + this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; + this.header = $('.panel-header'); + } + + get expanded(): boolean { + return this._expanded; + } + + set expanded(expanded: boolean) { + if (this._expanded === !!expanded) { + return; + } + + this._expanded = !!expanded; + this.updateHeader(); + this._onDidChange.fire(); + } + + get headerVisible(): boolean { + return this._headerVisible; + } + + set headerVisible(visible: boolean) { + if (this._headerVisible === !!visible) { + return; + } + + this._headerVisible = !!visible; + this.updateHeader(); + this._onDidChange.fire(); + } + + render(container: HTMLElement): void { + const panel = append(container, $('.panel')); + + append(panel, this.header); + this.header.setAttribute('tabindex', '0'); + this.header.setAttribute('role', 'toolbar'); + this.header.setAttribute('aria-label', this.ariaHeaderLabel); + this.renderHeader(this.header); + + const focusTracker = trackFocus(this.header); + focusTracker.addFocusListener(() => addClass(this.header, 'focused')); + focusTracker.addBlurListener(() => removeClass(this.header, 'focused')); + + this.updateHeader(); + + const onHeaderKeyDown = chain(domEvent(this.header, 'keydown')) + .map(e => new StandardKeyboardEvent(e)); + + onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) + .event(() => this.expanded = !this.expanded, null, this.disposables); + + onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow) + .event(() => this.expanded = false, null, this.disposables); + + onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) + .event(() => this.expanded = true, null, this.disposables); + + domEvent(this.header, 'click') + (() => this.expanded = !this.expanded, null, this.disposables); + + // TODO@Joao move this down to panelview + // onHeaderKeyDown.filter(e => e.keyCode === KeyCode.UpArrow) + // .event(focusPrevious, this, this.disposables); + + // onHeaderKeyDown.filter(e => e.keyCode === KeyCode.DownArrow) + // .event(focusNext, this, this.disposables); + + const body = append(panel, $('.panel-body')); + this.renderBody(body); + } + + layout(size: number): void { + const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0; + this.layoutBody(size - headerSize); + } + + style(styles: IPanelStyles): void { + this.header.style.color = styles.headerForeground ? styles.headerForeground.toString() : null; + this.header.style.backgroundColor = styles.headerBackground ? styles.headerBackground.toString() : null; + this.header.style.borderTop = styles.headerHighContrastBorder ? `1px solid ${styles.headerHighContrastBorder}` : null; + this._dropBackground = styles.dropBackground; + } + + private updateHeader(): void { + const expanded = !this.headerVisible || this.expanded; + + this.header.style.height = `${this.headerSize}px`; + this.header.style.lineHeight = `${this.headerSize}px`; + toggleClass(this.header, 'hidden', !this.headerVisible); + toggleClass(this.header, 'expanded', expanded); + this.header.setAttribute('aria-expanded', String(expanded)); + } + + protected abstract renderHeader(container: HTMLElement): void; + protected abstract renderBody(container: HTMLElement): void; + protected abstract layoutBody(size: number): void; + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +interface IDndContext { + draggable: PanelDraggable | null; +} + +class PanelDraggable implements IDisposable { + + private static DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5)); + + // see https://github.com/Microsoft/vscode/issues/14470 + private dragOverCounter = 0; + private disposables: IDisposable[] = []; + + private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>(); + readonly onDidDrop = this._onDidDrop.event; + + constructor(private panel: Panel, private context: IDndContext) { + domEvent(panel.draggable, 'dragstart')(this.onDragStart, this, this.disposables); + domEvent(panel.draggable, 'dragenter')(this.onDragEnter, this, this.disposables); + domEvent(panel.draggable, 'dragleave')(this.onDragLeave, this, this.disposables); + domEvent(panel.draggable, 'dragend')(this.onDragEnd, this, this.disposables); + domEvent(panel.draggable, 'drop')(this.onDrop, this, this.disposables); + } + + private onDragStart(e: DragEvent): void { + e.dataTransfer.effectAllowed = 'move'; + + const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.draggable.textContent)); + e.dataTransfer.setDragImage(dragImage, -10, -10); + setTimeout(() => document.body.removeChild(dragImage), 0); + + this.context.draggable = this; + } + + private onDragEnter(e: DragEvent): void { + if (!this.context.draggable || this.context.draggable === this) { + return; + } + + this.dragOverCounter++; + this.renderHeader(); + } + + private onDragLeave(e: DragEvent): void { + if (!this.context.draggable || this.context.draggable === this) { + return; + } + + this.dragOverCounter--; + + if (this.dragOverCounter === 0) { + this.renderHeader(); + } + } + + private onDragEnd(e: DragEvent): void { + if (!this.context.draggable) { + return; + } + + this.dragOverCounter = 0; + this.renderHeader(); + this.context.draggable = null; + } + + private onDrop(e: DragEvent): void { + if (!this.context.draggable) { + return; + } + + this.dragOverCounter = 0; + this.renderHeader(); + + if (this.context.draggable !== this) { + this._onDidDrop.fire({ from: this.context.draggable.panel, to: this.panel }); + } + + this.context.draggable = null; + } + + private renderHeader(): void { + let backgroundColor: string = null; + + if (this.dragOverCounter > 0) { + backgroundColor = (this.panel.dropBackground || PanelDraggable.DefaultDragOverBackgroundColor).toString(); + } + + this.panel.draggable.style.backgroundColor = backgroundColor; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +export class IPanelViewOptions { + dnd?: boolean; +} + +interface IPanelItem { + panel: Panel; + disposable: IDisposable; +} + +export class PanelView implements IDisposable { + + private dnd: boolean; + private dndContext: IDndContext = { draggable: null }; + private el: HTMLElement; + private panelItems: IPanelItem[] = []; + private splitview: SplitView; + private animationTimer: number | null = null; + + private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>(); + readonly onDidDrop: Event<{ from: Panel, to: Panel }> = this._onDidDrop.event; + + constructor(private container: HTMLElement, options: IPanelViewOptions = {}) { + this.dnd = !!options.dnd; + this.el = append(container, $('.monaco-panel-view')); + this.splitview = new SplitView(this.el); + } + + addPanel(panel: Panel, size: number, index = this.splitview.length): void { + const disposables: IDisposable[] = []; + panel.onDidChange(this.setupAnimation, this, disposables); + + if (this.dnd) { + const draggable = new PanelDraggable(panel, this.dndContext); + disposables.push(draggable); + draggable.onDidDrop(this._onDidDrop.fire, this._onDidDrop, disposables); + } + + const panelItem = { panel, disposable: combinedDisposable(disposables) }; + + this.panelItems.splice(index, 0, panelItem); + this.splitview.addView(panel, size, index); + } + + removePanel(panel: Panel): void { + const index = firstIndex(this.panelItems, item => item.panel === panel); + + if (index === -1) { + return; + } + + this.splitview.removeView(index); + const panelItem = this.panelItems.splice(index, 1)[0]; + panelItem.disposable.dispose(); + } + + movePanel(from: Panel, to: Panel): void { + const fromIndex = firstIndex(this.panelItems, item => item.panel === from); + const toIndex = firstIndex(this.panelItems, item => item.panel === to); + + if (fromIndex === -1 || toIndex === -1) { + return; + } + + const [panelItem] = this.panelItems.splice(fromIndex, 1); + this.panelItems.splice(toIndex < fromIndex ? toIndex : toIndex - 1, 0, panelItem); + + this.splitview.moveView(fromIndex, toIndex); + } + + resizePanel(panel: Panel, size: number): void { + const index = firstIndex(this.panelItems, item => item.panel === panel); + + if (index === -1) { + return; + } + + this.splitview.resizeView(index, size); + } + + layout(size: number): void { + this.splitview.layout(size); + } + + private setupAnimation(): void { + if (typeof this.animationTimer === 'number') { + window.clearTimeout(this.animationTimer); + } + + addClass(this.el, 'animated'); + + this.animationTimer = window.setTimeout(() => { + this.animationTimer = null; + removeClass(this.el, 'animated'); + }, 200); + } + + dispose(): void { + this.panelItems.forEach(i => i.disposable.dispose()); + this.splitview.dispose(); + } +} diff --git a/src/vs/base/browser/ui/splitview/splitview2.css b/src/vs/base/browser/ui/splitview/splitview2.css new file mode 100644 index 00000000000..41d3de5c826 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/splitview2.css @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-split-view2 { + position: relative; + overflow: hidden; + width: 100%; + height: 100%; +} + +.monaco-split-view2 > .split-view-view { + overflow: hidden; +} + +.monaco-split-view2.vertical > .split-view-view { + width: 100%; +} + +.monaco-split-view2.horizontal > .split-view-view { + height: 100%; +} diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts new file mode 100644 index 00000000000..b2deecf27f9 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -0,0 +1,342 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/css!./splitview2'; +import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event'; +import types = require('vs/base/common/types'); +import dom = require('vs/base/browser/dom'); +import { clamp } from 'vs/base/common/numbers'; +import { range, firstIndex } from 'vs/base/common/arrays'; +import { Sash, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; +export { Orientation } from 'vs/base/browser/ui/sash/sash'; + +export interface ISplitViewOptions { + orientation?: Orientation; // default Orientation.VERTICAL +} + +export interface IView { + readonly minimumSize: number; + readonly maximumSize: number; + readonly onDidChange: Event; + render(container: HTMLElement, orientation: Orientation): void; + layout(size: number, orientation: Orientation): void; +} + +interface ISashEvent { + sash: Sash; + start: number; + current: number; +} + +interface IViewItem { + view: IView; + size: number; + container: HTMLElement; + disposable: IDisposable; + layout(): void; +} + +interface ISashItem { + sash: Sash; + disposable: IDisposable; +} + +interface ISashDragState { + index: number; + start: number; + sizes: number[]; + minDelta: number; + maxDelta: number; +} + +export class SplitView implements IDisposable { + + private orientation: Orientation; + private el: HTMLElement; + private size = 0; + private contentSize = 0; + private viewItems: IViewItem[] = []; + private sashItems: ISashItem[] = []; + private sashDragState: ISashDragState; + + get length(): number { + return this.viewItems.length; + } + + constructor(private container: HTMLElement, options: ISplitViewOptions = {}) { + this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; + + this.el = document.createElement('div'); + dom.addClass(this.el, 'monaco-split-view2'); + dom.addClass(this.el, this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal'); + container.appendChild(this.el); + } + + addView(view: IView, size: number, index = this.viewItems.length): void { + // Add view + const container = dom.$('.split-view-view'); + + if (this.viewItems.length === 1) { + this.el.appendChild(container); + } else { + this.el.insertBefore(container, this.el.children.item(index)); + } + + const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this); + const containerDisposable = toDisposable(() => this.el.removeChild(container)); + const disposable = combinedDisposable([onChangeDisposable, containerDisposable]); + + const layoutContainer = this.orientation === Orientation.VERTICAL + ? size => item.container.style.height = `${item.size}px` + : size => item.container.style.width = `${item.size}px`; + + const layout = () => { + layoutContainer(item.size); + item.view.layout(item.size, this.orientation); + }; + + size = Math.round(size); + const item: IViewItem = { view, container, size, layout, disposable }; + this.viewItems.splice(index, 0, item); + + // Add sash + if (this.viewItems.length > 1) { + const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; + const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: sash => this.getSashPosition(sash) } : { getVerticalSashLeft: sash => this.getSashPosition(sash) }; + const sash = new Sash(this.el, layoutProvider, { orientation }); + const sashEventMapper = this.orientation === Orientation.VERTICAL + ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY }) + : (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX }); + + const onStart = mapEvent(fromEventEmitter(sash, 'start'), sashEventMapper); + const onStartDisposable = onStart(this.onSashStart, this); + const onChange = mapEvent(fromEventEmitter(sash, 'change'), sashEventMapper); + const onSashChangeDisposable = onChange(this.onSashChange, this); + const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]); + const sashItem: ISashItem = { sash, disposable }; + + this.sashItems.splice(index - 1, 0, sashItem); + } + + view.render(container, this.orientation); + this.relayout(); + } + + removeView(index: number): void { + if (index < 0 || index >= this.viewItems.length) { + return; + } + + // Remove view + const viewItem = this.viewItems.splice(index, 1)[0]; + viewItem.disposable.dispose(); + + // Remove sash + if (this.viewItems.length >= 1) { + const sashIndex = Math.max(index - 1, 0); + const sashItem = this.sashItems.splice(sashIndex, 1)[0]; + sashItem.disposable.dispose(); + } + + this.relayout(); + } + + moveView(from: number, to: number): void { + if (from < 0 || from >= this.viewItems.length) { + return; + } + + if (to < 0 || to >= this.viewItems.length) { + return; + } + + if (from === to) { + return; + } + + const viewItem = this.viewItems.splice(from, 1)[0]; + this.viewItems.splice(to, 0, viewItem); + this.layoutViews(); + } + + private relayout(): void { + const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + this.resize(this.viewItems.length - 1, this.contentSize - contentSize); + } + + layout(size: number): void { + const previousSize = Math.max(this.size, this.contentSize); + this.size = size; + this.resize(this.viewItems.length - 1, size - previousSize); + } + + private onSashStart({ sash, start }: ISashEvent): void { + const index = firstIndex(this.sashItems, item => item.sash === sash); + const sizes = this.viewItems.map(i => i.size); + + const upIndexes = range(index, -1); + const collapseUp = upIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0); + const expandUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - sizes[i]), 0); + + const downIndexes = range(index + 1, this.viewItems.length); + const collapseDown = downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0); + const expandDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - sizes[i]), 0); + + const minDelta = -Math.min(collapseUp, expandDown); + const maxDelta = Math.min(collapseDown, expandUp); + + this.sashDragState = { start, index, sizes, minDelta, maxDelta }; + } + + private onSashChange({ sash, current }: ISashEvent): void { + const { index, start, sizes, minDelta, maxDelta } = this.sashDragState; + const delta = clamp(current - start, minDelta, maxDelta); + + this.resize(index, delta, sizes); + } + + private onViewChange(item: IViewItem): void { + const index = this.viewItems.indexOf(item); + + if (index < 0 || index >= this.viewItems.length) { + return; + } + + const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize); + item.size = size; + this.relayout(); + } + + resizeView(index: number, size: number): void { + if (index < 0 || index >= this.viewItems.length) { + return; + } + + const item = this.viewItems[index]; + size = Math.round(size); + size = clamp(size, item.view.minimumSize, item.view.maximumSize); + let delta = size - item.size; + + if (delta !== 0 && index < this.viewItems.length - 1) { + const downIndexes = range(index + 1, this.viewItems.length); + const collapseDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].size - this.viewItems[i].view.minimumSize), 0); + const expandDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - this.viewItems[i].size), 0); + const deltaDown = clamp(delta, -expandDown, collapseDown); + + this.resize(index, deltaDown); + delta -= deltaDown; + } + + if (delta !== 0 && index > 0) { + const upIndexes = range(index - 1, -1); + const collapseUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].size - this.viewItems[i].view.minimumSize), 0); + const expandUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - this.viewItems[i].size), 0); + const deltaUp = clamp(-delta, -collapseUp, expandUp); + + this.resize(index - 1, deltaUp); + } + } + + private resize(index: number, delta: number, sizes = this.viewItems.map(i => i.size)): void { + if (index < 0 || index >= this.viewItems.length) { + return; + } + + if (delta !== 0) { + const upIndexes = range(index, -1); + const up = upIndexes.map(i => this.viewItems[i]); + const upSizes = upIndexes.map(i => sizes[i]); + const downIndexes = range(index + 1, this.viewItems.length); + const down = downIndexes.map(i => this.viewItems[i]); + const downSizes = downIndexes.map(i => sizes[i]); + + for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) { + const item = up[i]; + const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - upSizes[i]; + + deltaUp -= viewDelta; + item.size = size; + } + + for (let i = 0, deltaDown = delta; deltaDown !== 0 && i < down.length; i++) { + const item = down[i]; + const size = clamp(downSizes[i] - deltaDown, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - downSizes[i]; + + deltaDown += viewDelta; + item.size = size; + } + } + + let contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + let emptyDelta = this.size - contentSize; + + for (let i = this.viewItems.length - 1; emptyDelta > 0 && i >= 0; i--) { + const item = this.viewItems[i]; + const size = clamp(item.size + emptyDelta, item.view.minimumSize, item.view.maximumSize); + const viewDelta = size - item.size; + + emptyDelta -= viewDelta; + item.size = size; + } + + this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); + + this.layoutViews(); + } + + private layoutViews(): void { + this.viewItems.forEach(item => item.layout()); + this.sashItems.forEach(item => item.sash.layout()); + + // Update sashes enablement + let previous = false; + const collapsesDown = this.viewItems.map(i => previous = (i.size - i.view.minimumSize > 0) || previous); + + previous = false; + const expandsDown = this.viewItems.map(i => previous = (i.view.maximumSize - i.size > 0) || previous); + + const reverseViews = [...this.viewItems].reverse(); + previous = false; + const collapsesUp = reverseViews.map(i => previous = (i.size - i.view.minimumSize > 0) || previous).reverse(); + + previous = false; + const expandsUp = reverseViews.map(i => previous = (i.view.maximumSize - i.size > 0) || previous).reverse(); + + this.sashItems.forEach((s, i) => { + if ((collapsesDown[i] && expandsUp[i + 1]) || (expandsDown[i] && collapsesUp[i + 1])) { + s.sash.enable(); + } else { + s.sash.disable(); + } + }); + } + + private getSashPosition(sash: Sash): number { + let position = 0; + + for (let i = 0; i < this.sashItems.length; i++) { + position += this.viewItems[i].size; + + if (this.sashItems[i].sash === sash) { + return position; + } + } + + return 0; + } + + dispose(): void { + this.viewItems.forEach(i => i.disposable.dispose()); + this.viewItems = []; + + this.sashItems.forEach(i => i.disposable.dispose()); + this.sashItems = []; + } +} diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 4be8d60f396..a64b84276eb 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -325,11 +325,43 @@ export function flatten(arr: T[][]): T[] { return arr.reduce((r, v) => r.concat(v), []); } -export function range(to: number, from = 0): number[] { +export function range(to: number): number[]; +export function range(from: number, to: number): number[]; +export function range(arg: number, to?: number): number[] { + let from = typeof to === 'number' ? arg : 0; + + if (typeof to === 'number') { + from = arg; + } else { + from = 0; + to = arg; + } + const result: number[] = []; - for (let i = from; i < to; i++) { - result.push(i); + if (from <= to) { + for (let i = from; i < to; i++) { + result.push(i); + } + } else { + for (let i = from; i > to; i--) { + result.push(i); + } + } + + return result; +} + +export function weave(a: T[], b: T[]): T[] { + const result: T[] = []; + let ai = 0, bi = 0; + + for (let i = 0, length = a.length + b.length; i < length; i++) { + if ((i % 2 === 0 && ai < a.length) || bi >= b.length) { + result.push(a[ai++]); + } else { + result.push(b[bi++]); + } } return result; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index cd322eb4b29..9ca573aac02 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, combinedDisposable, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import CallbackList from 'vs/base/common/callbackList'; import { EventEmitter } from 'vs/base/common/eventEmitter'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -528,3 +528,21 @@ export function echo(event: Event, nextTick = false, buffer: T[] = []): Ev return emitter.event; } + +export class Relay implements IDisposable { + + private emitter = new Emitter(); + readonly output: Event = this.emitter.event; + + private disposable: IDisposable = EmptyDisposable; + + set input(event: Event) { + this.disposable.dispose(); + this.disposable = event(this.emitter.fire, this.emitter); + } + + dispose() { + this.disposable.dispose(); + this.emitter.dispose(); + } +} \ No newline at end of file diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index ab7ae6065f1..ce013b916cc 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -56,7 +56,7 @@ export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownStri export function isMarkdownString(thing: any): thing is IMarkdownString { if (thing instanceof MarkdownString) { return true; - } else if (typeof thing === 'object') { + } else if (thing && typeof thing === 'object') { return typeof (thing).value === 'string' && (typeof (thing).isTrusted === 'boolean' || (thing).isTrusted === void 0); } diff --git a/src/vs/base/common/numbers.ts b/src/vs/base/common/numbers.ts index 65bcbbd7922..9f804fe6dd9 100644 --- a/src/vs/base/common/numbers.ts +++ b/src/vs/base/common/numbers.ts @@ -46,3 +46,8 @@ export function countToArray(fromOrTo: number, to?: number): number[] { return result; } + + +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} \ No newline at end of file diff --git a/src/vs/base/node/terminateProcess.sh b/src/vs/base/node/terminateProcess.sh index acdcbf8ed42..dceeae9745f 100755 --- a/src/vs/base/node/terminateProcess.sh +++ b/src/vs/base/node/terminateProcess.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash terminateTree() { for cpid in $(/usr/bin/pgrep -P $1); do diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts new file mode 100644 index 00000000000..07fb5816104 --- /dev/null +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -0,0 +1,314 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter } from 'vs/base/common/event'; +import { SplitView, IView, Orientation } from 'vs/base/browser/ui/splitview/splitview2'; +import { Sash } from 'vs/base/browser/ui/sash/sash'; + +class TestView implements IView { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + get minimumSize(): number { return this._minimumSize; } + set minimumSize(size: number) { this._minimumSize = size; this._onDidChange.fire(); } + + get maximumSize(): number { return this._maximumSize; } + set maximumSize(size: number) { this._maximumSize = size; this._onDidChange.fire(); } + + private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>(); + readonly onDidRender = this._onDidRender.event; + + private _size = 0; + get size(): number { return this._size; } + private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>(); + readonly onDidLayout = this._onDidLayout.event; + + private _onDidFocus = new Emitter(); + readonly onDidFocus = this._onDidFocus.event; + + constructor( + private _minimumSize: number, + private _maximumSize: number + ) { + assert(_minimumSize <= _maximumSize, 'splitview view minimum size must be <= maximum size'); + } + + render(container: HTMLElement, orientation: Orientation): void { + this._onDidRender.fire({ container, orientation }); + } + + layout(size: number, orientation: Orientation): void { + this._size = size; + this._onDidLayout.fire({ size, orientation }); + } + + focus(): void { + this._onDidFocus.fire(); + } + + dispose(): void { + this._onDidChange.dispose(); + this._onDidRender.dispose(); + this._onDidLayout.dispose(); + this._onDidFocus.dispose(); + } +} + +function getSashes(splitview: SplitView): Sash[] { + return (splitview as any).sashItems.map(i => i.sash) as Sash[]; +} + +suite('Splitview', () => { + let container: HTMLElement; + + setup(() => { + container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.width = `${200}px`; + container.style.height = `${200}px`; + }); + + teardown(() => { + container = null; + }); + + test('empty splitview has empty DOM', () => { + const splitview = new SplitView(container); + assert.equal(container.firstElementChild.childElementCount, 0, 'split view should be empty'); + splitview.dispose(); + }); + + test('has views and sashes as children', () => { + const view1 = new TestView(20, 20); + const view2 = new TestView(20, 20); + const view3 = new TestView(20, 20); + const splitview = new SplitView(container); + + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); + + let viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); + assert.equal(viewQuery.length, 3, 'split view should have 3 views'); + + let sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); + assert.equal(sashQuery.length, 2, 'split view should have 2 sashes'); + + splitview.removeView(2); + + viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); + assert.equal(viewQuery.length, 2, 'split view should have 2 views'); + + sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); + assert.equal(sashQuery.length, 1, 'split view should have 1 sash'); + + splitview.removeView(0); + + viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); + assert.equal(viewQuery.length, 1, 'split view should have 1 view'); + + sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); + assert.equal(sashQuery.length, 0, 'split view should have no sashes'); + + splitview.removeView(0); + + viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view'); + assert.equal(viewQuery.length, 0, 'split view should have no views'); + + sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash'); + assert.equal(sashQuery.length, 0, 'split view should have no sashes'); + + splitview.dispose(); + view1.dispose(); + view2.dispose(); + view3.dispose(); + }); + + test('calls view methods on addView and removeView', () => { + const view = new TestView(20, 20); + const splitview = new SplitView(container); + + let didLayout = false; + const layoutDisposable = view.onDidLayout(() => didLayout = true); + + let didRender = false; + const renderDisposable = view.onDidRender(() => didRender = true); + + splitview.addView(view, 20); + + assert.equal(view.size, 20, 'view has right size'); + assert(didLayout, 'layout is called'); + assert(didLayout, 'render is called'); + + splitview.dispose(); + layoutDisposable.dispose(); + renderDisposable.dispose(); + view.dispose(); + }); + + test('stretches view to viewport', () => { + const view = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(200); + + splitview.addView(view, 20); + assert.equal(view.size, 200, 'view is stretched'); + + splitview.layout(200); + assert.equal(view.size, 200, 'view stayed the same'); + + splitview.layout(100); + assert.equal(view.size, 100, 'view is collapsed'); + + splitview.layout(20); + assert.equal(view.size, 20, 'view is collapsed'); + + splitview.layout(10); + assert.equal(view.size, 20, 'view is clamped'); + + splitview.layout(200); + assert.equal(view.size, 200, 'view is stretched'); + + splitview.dispose(); + view.dispose(); + }); + + test('can resize views', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(200); + + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); + + assert.equal(view1.size, 160, 'view1 is stretched'); + assert.equal(view2.size, 20, 'view2 size is 20'); + assert.equal(view3.size, 20, 'view3 size is 20'); + + splitview.resizeView(1, 40); + + assert.equal(view1.size, 140, 'view1 is collapsed'); + assert.equal(view2.size, 40, 'view2 is stretched'); + assert.equal(view3.size, 20, 'view3 stays the same'); + + splitview.resizeView(0, 70); + + assert.equal(view1.size, 70, 'view1 is collapsed'); + assert.equal(view2.size, 110, 'view2 is expanded'); + assert.equal(view3.size, 20, 'view3 stays the same'); + + splitview.resizeView(2, 40); + + assert.equal(view1.size, 70, 'view1 stays the same'); + assert.equal(view2.size, 90, 'view2 is collapsed'); + assert.equal(view3.size, 40, 'view3 is stretched'); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); + + test('reacts to view changes', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(200); + + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); + + assert.equal(view1.size, 160, 'view1 is stretched'); + assert.equal(view2.size, 20, 'view2 size is 20'); + assert.equal(view3.size, 20, 'view3 size is 20'); + + view1.maximumSize = 20; + + assert.equal(view1.size, 20, 'view1 is collapsed'); + assert.equal(view2.size, 20, 'view2 stays the same'); + assert.equal(view3.size, 160, 'view3 is stretched'); + + view3.maximumSize = 40; + + assert.equal(view1.size, 20, 'view1 stays the same'); + assert.equal(view2.size, 140, 'view2 is stretched'); + assert.equal(view3.size, 40, 'view3 is collapsed'); + + view2.maximumSize = 200; + + assert.equal(view1.size, 20, 'view1 stays the same'); + assert.equal(view2.size, 140, 'view2 stays the same'); + assert.equal(view3.size, 40, 'view3 stays the same'); + + view3.maximumSize = Number.POSITIVE_INFINITY; + view3.minimumSize = 100; + + assert.equal(view1.size, 20, 'view1 is collapsed'); + assert.equal(view2.size, 80, 'view2 is collapsed'); + assert.equal(view3.size, 100, 'view3 is stretched'); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); + + test('sashes are properly enabled/disabled', () => { + const view1 = new TestView(20, Number.POSITIVE_INFINITY); + const view2 = new TestView(20, Number.POSITIVE_INFINITY); + const view3 = new TestView(20, Number.POSITIVE_INFINITY); + const splitview = new SplitView(container); + splitview.layout(200); + + splitview.addView(view1, 20); + splitview.addView(view2, 20); + splitview.addView(view3, 20); + + let sashes = getSashes(splitview); + assert.equal(sashes.length, 2, 'there are two sashes'); + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + splitview.layout(60); + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, false, 'second sash is disabled'); + + splitview.layout(20); + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, false, 'second sash is disabled'); + + splitview.layout(200); + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + view1.maximumSize = 20; + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + view2.maximumSize = 20; + assert.equal(sashes[0].enabled, false, 'first sash is disabled'); + assert.equal(sashes[1].enabled, false, 'second sash is disabled'); + + view1.maximumSize = 300; + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + view2.maximumSize = 200; + assert.equal(sashes[0].enabled, true, 'first sash is enabled'); + assert.equal(sashes[1].enabled, true, 'second sash is enabled'); + + splitview.dispose(); + view3.dispose(); + view2.dispose(); + view1.dispose(); + }); +}); \ No newline at end of file diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 3eea14eb51a..23ed80e4d83 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -18,7 +18,7 @@ import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingReso import { IKeybindingEvent, KeybindingSource, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfirmation, IMessageService } from 'vs/platform/message/common/message'; -import { IWorkspaceContextService, IWorkspace, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace, WorkbenchState, WorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; @@ -525,8 +525,8 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { private readonly _onDidChangeWorkspaceName: Emitter = new Emitter(); public readonly onDidChangeWorkspaceName: Event = this._onDidChangeWorkspaceName.event; - private readonly _onDidChangeWorkspaceFolders: Emitter = new Emitter(); - public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; + private readonly _onDidChangeWorkspaceFolders: Emitter = new Emitter(); + public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; private readonly _onDidChangeWorkbenchState: Emitter = new Emitter(); public readonly onDidChangeWorkbenchState: Event = this._onDidChangeWorkbenchState.event; diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 24d82c47bb5..df42ecd966c 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -228,17 +228,23 @@ export interface InstallExtensionEvent { gallery?: IGalleryExtension; } +export enum ErrorCode { + OBSOLETE = 1, + GALLERY, + LOCAL +} + export interface DidInstallExtensionEvent { id: string; zipPath?: string; gallery?: IGalleryExtension; local?: ILocalExtension; - error?: Error; + error?: ErrorCode; } export interface DidUninstallExtensionEvent { id: string; - error?: Error; + error?: ErrorCode; } export interface IExtensionManagementService { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 24f88da7d2a..ae321626480 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -18,7 +18,8 @@ import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IExtensionManifest, IGalleryMetadata, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, LocalExtensionType, - StatisticType + StatisticType, + ErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getLocalExtensionIdFromGallery, getLocalExtensionIdFromManifest, getGalleryExtensionIdFromLocal, getIdAndVersionFromLocalExtensionId, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localizeManifest } from '../common/extensionNls'; @@ -68,6 +69,12 @@ function readManifest(extensionPath: string): TPromise<{ manifest: IExtensionMan }); } +interface InstallableExtension { + zipPath: string; + id: string; + metadata: IGalleryMetadata; +} + export class ExtensionManagementService implements IExtensionManagementService { _serviceBrand: any; @@ -107,12 +114,12 @@ export class ExtensionManagementService implements IExtensionManagementService { return this.isObsolete(id).then(isObsolete => { if (isObsolete) { - return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name))); + return TPromise.wrapError(new Error(nls.localize('restartCodeLocal', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name))); } this._onInstallExtension.fire({ id, zipPath }); - return this.installExtension(zipPath, id) + return this.installExtension({ zipPath, id, metadata: null }) .then( local => this._onDidInstallExtension.fire({ id, zipPath, local }), error => { this._onDidInstallExtension.fire({ id, zipPath, error }); return TPromise.wrapError(error); } @@ -122,91 +129,83 @@ export class ExtensionManagementService implements IExtensionManagementService { } installFromGallery(extension: IGalleryExtension): TPromise { - const id = getLocalExtensionIdFromGallery(extension, extension.version); + return this.prepareAndCollectExtensionsToInstall(extension) + .then(extensionsToInstall => this.downloadAndInstallExtensions(extensionsToInstall) + .then(local => this.onDidInstallExtensions(extensionsToInstall, local))); + } - return this.isObsolete(id).then(isObsolete => { - if (isObsolete) { - return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name))); - } - this._onInstallExtension.fire({ id, gallery: extension }); - return this.installCompatibleVersion(extension, true) + private prepareAndCollectExtensionsToInstall(extension: IGalleryExtension): TPromise { + this.onInstallExtensions([extension]); + return this.collectExtensionsToInstall(extension) + .then( + extensionsToInstall => this.checkForObsolete(extensionsToInstall) .then( - local => this._onDidInstallExtension.fire({ id, local, gallery: extension }), - error => { - this._onDidInstallExtension.fire({ id, gallery: extension, error }); - return TPromise.wrapError(error); - } - ); - }); - } - - private installCompatibleVersion(extension: IGalleryExtension, installDependencies: boolean): TPromise { - return this.galleryService.loadCompatibleVersion(extension) - .then(compatibleVersion => this.getDependenciesToInstall(extension, installDependencies) - .then(dependencies => dependencies.length ? this.installWithDependencies(compatibleVersion) : this.downloadAndInstall(compatibleVersion))); - } - - private getDependenciesToInstall(extension: IGalleryExtension, checkDependecies: boolean): TPromise { - if (!checkDependecies) { - return TPromise.wrap([]); - } - // Filter out self - const dependencies = extension.properties.dependencies ? extension.properties.dependencies.filter(id => id !== extension.id) : []; - if (!dependencies.length) { - return TPromise.wrap([]); - } - // Filter out installed dependencies - return this.getInstalled().then(installed => { - return dependencies.filter(dep => installed.every(i => `${i.manifest.publisher}.${i.manifest.name}` !== dep)); - }); - } - - private installWithDependencies(extension: IGalleryExtension): TPromise { - return this.galleryService.getAllDependencies(extension) - .then(allDependencies => this.filterDependenciesToInstall(extension, allDependencies)) - .then(toInstall => this.filterObsolete(...toInstall.map(i => getLocalExtensionIdFromGallery(i, i.version))) - .then((obsolete) => { - if (obsolete.length) { - return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name))); + extensionsToInstall => { + if (extensionsToInstall.length > 1) { + this.onInstallExtensions(extensionsToInstall.slice(1)); } - return this.bulkInstallWithDependencies(extension, toInstall); - }) + return extensionsToInstall; + }, + error => this.onDidInstallExtensions([extension], null, ErrorCode.OBSOLETE, error) + ), + error => this.onDidInstallExtensions([extension], null, ErrorCode.GALLERY, error) ); } - private bulkInstallWithDependencies(extension: IGalleryExtension, dependecies: IGalleryExtension[]): TPromise { - for (const dependency of dependecies) { - const id = getLocalExtensionIdFromGallery(dependency, dependency.version); - this._onInstallExtension.fire({ id, gallery: dependency }); - } - return this.downloadAndInstall(extension) - .then(localExtension => { - return TPromise.join(dependecies.map((dep) => this.installCompatibleVersion(dep, false))) - .then(installedLocalExtensions => { - for (const installedLocalExtension of installedLocalExtensions) { - const gallery = this.getGalleryExtensionForLocalExtension(dependecies, installedLocalExtension); - this._onDidInstallExtension.fire({ id: installedLocalExtension.id, local: installedLocalExtension, gallery }); - } - return localExtension; - }, error => { - return this.rollback(localExtension, dependecies).then(() => { - return TPromise.wrapError(Array.isArray(error) ? error[error.length - 1] : error); - }); - }); - }) - .then(localExtension => localExtension, error => { - for (const dependency of dependecies) { - this._onDidInstallExtension.fire({ id: getLocalExtensionIdFromGallery(dependency, dependency.version), gallery: dependency, error }); - } - return TPromise.wrapError(error); - }); + private downloadAndInstallExtensions(extensions: IGalleryExtension[]): TPromise { + return TPromise.join(extensions.map(extensionToInstall => this.downloadInstallableExtension(extensionToInstall))) + .then( + installableExtensions => TPromise.join(installableExtensions.map(installableExtension => this.installExtension(installableExtension))) + .then(null, error => this.rollback(extensions).then(() => this.onDidInstallExtensions(extensions, null, ErrorCode.LOCAL, error))), + error => this.onDidInstallExtensions(extensions, null, ErrorCode.GALLERY, error)); } - private rollback(localExtension: ILocalExtension, dependecies: IGalleryExtension[]): TPromise { - return this.doUninstall(localExtension) - .then(() => this.filterOutUninstalled(dependecies)) - .then(installed => TPromise.join(installed.map((i) => this.doUninstall(i)))) - .then(() => null); + private collectExtensionsToInstall(extension: IGalleryExtension): TPromise { + return this.galleryService.loadCompatibleVersion(extension) + .then(extensionToInstall => this.galleryService.getAllDependencies(extension) + .then(allDependencies => this.filterDependenciesToInstall(extension, allDependencies)) + .then(dependenciesToInstall => [extensionToInstall, ...dependenciesToInstall])); + } + + private checkForObsolete(extensionsToInstall: IGalleryExtension[]): TPromise { + return this.filterObsolete(...extensionsToInstall.map(i => getLocalExtensionIdFromGallery(i, i.version))) + .then(obsolete => obsolete.length ? TPromise.wrapError(new Error(nls.localize('restartCodeGallery', "Please restart Code before reinstalling."))) : extensionsToInstall); + } + + private downloadInstallableExtension(extension: IGalleryExtension): TPromise { + const id = getLocalExtensionIdFromGallery(extension, extension.version); + const metadata = { + id: extension.uuid, + publisherId: extension.publisherId, + publisherDisplayName: extension.publisherDisplayName, + }; + return this.galleryService.download(extension) + .then(zipPath => validate(zipPath).then(() => ({ zipPath, id, metadata }))); + } + + private rollback(extensions: IGalleryExtension[]): TPromise { + return this.filterOutUninstalled(extensions) + .then(installed => TPromise.join(installed.map(local => this.uninstallExtension(local.id)))) + .then(() => null, () => null); + } + + private onInstallExtensions(extensions: IGalleryExtension[]): void { + for (const extension of extensions) { + const id = getLocalExtensionIdFromGallery(extension, extension.version); + this._onInstallExtension.fire({ id, gallery: extension }); + } + } + + private onDidInstallExtensions(extensions: IGalleryExtension[], local: ILocalExtension[], errorCode?: ErrorCode, error?: any): TPromise { + extensions.forEach((gallery, index) => { + const id = getLocalExtensionIdFromGallery(gallery, gallery.version); + if (errorCode) { + this._onDidInstallExtension.fire({ id, gallery, error: errorCode }); + } else { + this._onDidInstallExtension.fire({ id, gallery, local: local[index] }); + } + }); + return error ? TPromise.wrapError(Array.isArray(error) ? this.joinErrors(error) : error) : TPromise.as(null); } private filterDependenciesToInstall(extension: IGalleryExtension, dependencies: IGalleryExtension[]): TPromise { @@ -232,20 +231,7 @@ export class ExtensionManagementService implements IExtensionManagementService { return filtered.length ? filtered[0] : null; } - private downloadAndInstall(extension: IGalleryExtension): TPromise { - const id = getLocalExtensionIdFromGallery(extension, extension.version); - const metadata = { - id: extension.uuid, - publisherId: extension.publisherId, - publisherDisplayName: extension.publisherDisplayName, - }; - - return this.galleryService.download(extension) - .then(zipPath => validate(zipPath).then(() => zipPath)) - .then(zipPath => this.installExtension(zipPath, id, metadata)); - } - - private installExtension(zipPath: string, id: string, metadata: IGalleryMetadata = null): TPromise { + private installExtension({ zipPath, id, metadata }: InstallableExtension): TPromise { const extensionPath = path.join(this.extensionsPath, id); return pfs.rimraf(extensionPath).then(() => { @@ -291,7 +277,7 @@ export class ExtensionManagementService implements IExtensionManagementService { } return errors.reduce((previousValue: Error, currentValue: Error | string) => { - return new Error(`${previousValue.message}\n${currentValue instanceof Error ? currentValue.message : currentValue}`); + return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); }, new Error('')); } @@ -300,7 +286,7 @@ export class ExtensionManagementService implements IExtensionManagementService { .then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.promptAndUninstall(extension, installed, force)) .then(() => this.postUninstallExtension(extension), error => { - this.postUninstallExtension(extension, error); + this.postUninstallExtension(extension, ErrorCode.LOCAL); return TPromise.wrapError(error); }); } @@ -416,7 +402,7 @@ export class ExtensionManagementService implements IExtensionManagementService { .then(() => this.uninstallExtension(extension.id)) .then(() => this.postUninstallExtension(extension), error => { - this.postUninstallExtension(extension, error); + this.postUninstallExtension(extension, ErrorCode.LOCAL); return TPromise.wrapError(error); }); } @@ -435,7 +421,7 @@ export class ExtensionManagementService implements IExtensionManagementService { .then(() => this.unsetObsolete(id)); } - private async postUninstallExtension(extension: ILocalExtension, error?: any): TPromise { + private async postUninstallExtension(extension: ILocalExtension, error?: ErrorCode): TPromise { if (!error) { await this.galleryService.reportStatistic(extension.manifest.publisher, extension.manifest.name, extension.manifest.version, StatisticType.Uninstall); } diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 243d6c820ac..d5d4698e424 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -15,11 +15,15 @@ export interface IThemable { style: styleFn; } -export function attachStyler(themeService: IThemeService, optionsMapping: { [optionsKey: string]: ColorIdentifier | ColorFunction }, widgetOrCallback: IThemable | styleFn): IDisposable { +export interface IColorMapping { + [optionsKey: string]: ColorIdentifier | ColorFunction | undefined; +} + +export function attachStyler(themeService: IThemeService, optionsMapping: T, widgetOrCallback: IThemable | styleFn): IDisposable { function applyStyles(theme: ITheme): void { const styles = Object.create(null); for (let key in optionsMapping) { - const value = optionsMapping[key]; + const value = optionsMapping[key as string]; if (typeof value === 'string') { styles[key] = theme.getColor(value); } else if (typeof value === 'function') { diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index c1ea01d2faa..1ebcb21877d 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -21,6 +21,12 @@ export enum WorkbenchState { WORKSPACE } +export interface IWorkspaceFoldersChangeEvent { + added: WorkspaceFolder[]; + removed: WorkspaceFolder[]; + changed: WorkspaceFolder[]; +} + export interface IWorkspaceContextService { _serviceBrand: any; @@ -52,7 +58,7 @@ export interface IWorkspaceContextService { /** * An event which fires on workspace folders change. */ - onDidChangeWorkspaceFolders: Event; + onDidChangeWorkspaceFolders: Event; /** * Returns the folder for the given resource from the workspace. diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 019e8d45a08..47bcdcb64a5 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3311,6 +3311,7 @@ declare module 'vscode' { * used to show editors side by side. */ export enum ViewColumn { + Active = -1, One = 1, Two = 2, Three = 3 @@ -5506,6 +5507,11 @@ declare module 'vscode' { */ readonly label: string; + /** + * The (optional) Uri of the root of this source control. + */ + readonly rootUri: Uri | undefined; + /** * The [input box](#SourceControlInputBox) for this source control. */ @@ -5574,9 +5580,10 @@ declare module 'vscode' { * * @param id An `id` for the source control. Something short, eg: `git`. * @param label A human-readable string for the source control. Eg: `Git`. + * @param rootUri An optional Uri of the root of the source control. Eg: `Uri.parse(workspaceRoot)`. * @return An instance of [source control](#SourceControl). */ - export function createSourceControl(id: string, label: string): SourceControl; + export function createSourceControl(id: string, label: string, rootUri?: Uri): SourceControl; } /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts index cbfeac52f1d..64253cfa2d4 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts @@ -100,18 +100,17 @@ class MainThreadSCMProvider implements ISCMProvider { get handle(): number { return this._handle; } get label(): string { return this._label; } + get rootUri(): URI | undefined { return this._rootUri; } get contextValue(): string { return this._contextValue; } get commitTemplate(): string | undefined { return this.features.commitTemplate; } get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; } get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; } + get count(): number | undefined { return this.features.count; } private _onDidChangeCommitTemplate = new Emitter(); get onDidChangeCommitTemplate(): Event { return this._onDidChangeCommitTemplate.event; } - private _count: number | undefined = undefined; - get count(): number | undefined { return this._count; } - private _onDidChange = new Emitter(); get onDidChange(): Event { return this._onDidChange.event; } @@ -120,15 +119,12 @@ class MainThreadSCMProvider implements ISCMProvider { private _handle: number, private _contextValue: string, private _label: string, + private _rootUri: URI | undefined, @ISCMService scmService: ISCMService, @ICommandService private commandService: ICommandService ) { } $updateSourceControl(features: SCMProviderFeatures): void { - if ('count' in features) { - this._count = features.count; - } - this.features = assign(this.features, features); this._onDidChange.fire(); @@ -275,8 +271,8 @@ export class MainThreadSCM implements MainThreadSCMShape { this._disposables = dispose(this._disposables); } - $registerSourceControl(handle: number, id: string, label: string): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, this.scmService, this.commandService); + $registerSourceControl(handle: number, id: string, label: string, rootUri: string | undefined): void { + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri && URI.parse(rootUri), this.scmService, this.commandService); const repository = this.scmService.registerSCMProvider(provider); this._repositories[handle] = repository; diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 904bb38e0bb..13fc250f8cc 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -489,14 +489,14 @@ export function createApiFactory( get inputBox() { return extHostSCM.getLastInputBox(extension); }, - createSourceControl(id: string, label: string) { + createSourceControl(id: string, label: string, rootUri?: vscode.Uri) { mainThreadTelemetry.$publicLog('registerSCMProvider', { extensionId: extension.id, providerId: id, providerLabel: label }); - return extHostSCM.createSourceControl(extension, id, label); + return extHostSCM.createSourceControl(extension, id, label, rootUri); } }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 92f7b206369..b2c0b08a943 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -367,7 +367,7 @@ export type SCMRawResourceSplices = [ ]; export interface MainThreadSCMShape extends IDisposable { - $registerSourceControl(handle: number, id: string, label: string): void; + $registerSourceControl(handle: number, id: string, label: string, rootUri: string | undefined): void; $updateSourceControl(handle: number, features: SCMProviderFeatures): void; $unregisterSourceControl(handle: number): void; diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index 31e5ace5eb1..ec9ad8b38ee 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -287,6 +287,10 @@ class ExtHostSourceControl implements vscode.SourceControl { return this._label; } + get rootUri(): vscode.Uri | undefined { + return this._rootUri; + } + private _inputBox: ExtHostSCMInputBox; get inputBox(): ExtHostSCMInputBox { return this._inputBox; } @@ -356,9 +360,10 @@ class ExtHostSourceControl implements vscode.SourceControl { private _commands: ExtHostCommands, private _id: string, private _label: string, + private _rootUri?: vscode.Uri ) { this._inputBox = new ExtHostSCMInputBox(this._proxy, this.handle); - this._proxy.$registerSourceControl(this.handle, _id, _label); + this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri && _rootUri.toString()); } private updatedResourceGroups = new Set(); @@ -468,9 +473,9 @@ export class ExtHostSCM { }); } - createSourceControl(extension: IExtensionDescription, id: string, label: string): vscode.SourceControl { + createSourceControl(extension: IExtensionDescription, id: string, label: string, rootUri: vscode.Uri | undefined): vscode.SourceControl { const handle = ExtHostSCM._handlePool++; - const sourceControl = new ExtHostSourceControl(this._proxy, this._commands, id, label); + const sourceControl = new ExtHostSourceControl(this._proxy, this._commands, id, label, rootUri); this._sourceControls.set(handle, sourceControl); const sourceControls = this._sourceControlsByExtension.get(extension.id) || []; diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 55436407d05..d8aef198fd8 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -115,6 +115,8 @@ export function fromViewColumn(column?: vscode.ViewColumn): EditorPosition { editorColumn = EditorPosition.TWO; } else if (column === types.ViewColumn.Three) { editorColumn = EditorPosition.THREE; + } else if (column === types.ViewColumn.Active) { + editorColumn = undefined; } return editorColumn; } @@ -156,7 +158,7 @@ export namespace MarkdownString { } function isCodeblock(thing: any): thing is Codeblock { - return typeof thing === 'object' + return thing && typeof thing === 'object' && typeof (thing).language === 'string' && typeof (thing).value === 'string'; } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 48930903cef..d63e57a1506 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -953,6 +953,7 @@ export class CompletionList { } export enum ViewColumn { + Active = -1, One = 1, Two = 2, Three = 3 diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 21a78d1b501..58f09abc65b 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -15,15 +15,18 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import URI from 'vs/base/common/uri'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isLinux } from 'vs/base/common/platform'; import { dirname } from 'vs/base/common/paths'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; -import { isParent } from 'vs/platform/files/common/files'; +import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; +import { isParent, FileKind } from 'vs/platform/files/common/files'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IQuickOpenService, IFilePickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; export class OpenFolderAction extends Action { @@ -282,4 +285,65 @@ export class OpenWorkspaceConfigFileAction extends Action { public run(): TPromise { return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration }); } -} \ No newline at end of file +} + +export const PICK_WORKSPACE_FOLDER_COMMAND = '_workbench.pickWorkspaceFolder'; + +CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND, function (accessor: ServicesAccessor, args?: [IPickOptions, CancellationToken]) { + const contextService = accessor.get(IWorkspaceContextService); + const quickOpenService = accessor.get(IQuickOpenService); + const environmentService = accessor.get(IEnvironmentService); + + const folders = contextService.getWorkspace().folders; + if (!folders.length) { + return void 0; + } + + const folderPicks = folders.map(folder => { + return { + label: folder.name, + description: getPathLabel(dirname(folder.uri.fsPath), void 0, environmentService), + folder, + resource: folder.uri, + fileKind: FileKind.ROOT_FOLDER + } as IFilePickOpenEntry; + }); + + let options: IPickOptions; + if (args) { + options = args[0]; + } + + if (!options) { + options = Object.create(null); + } + + if (!options.autoFocus) { + options.autoFocus = { autoFocusFirstEntry: true }; + } + + if (!options.placeHolder) { + options.placeHolder = nls.localize('workspaceFolderPickerPlaceholder', "Select workspace folder"); + } + + if (typeof options.matchOnDescription !== 'boolean') { + options.matchOnDescription = true; + } + + let token: CancellationToken; + if (args) { + token = args[1]; + } + + if (!token) { + token = CancellationToken.None; + } + + return quickOpenService.pick(folderPicks, options, token).then(pick => { + if (!pick) { + return void 0; + } + + return folders[folderPicks.indexOf(pick)]; + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 75c85fdb9f7..244b7a49abd 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -211,14 +211,19 @@ export class FileLabel extends ResourceLabel { } public setFile(resource: uri, options?: IFileLabelOptions): void { + const hideLabel = options && options.hideLabel; let name: string; - if (options && options.hideLabel) { - name = void 0; - } else if (options && options.fileKind === FileKind.ROOT_FOLDER) { - const workspaceFolder = this.contextService.getWorkspaceFolder(resource); - name = workspaceFolder.name; - } else { - name = paths.basename(resource.fsPath); + if (!hideLabel) { + if (options && options.fileKind === FileKind.ROOT_FOLDER) { + const workspaceFolder = this.contextService.getWorkspaceFolder(resource); + if (workspaceFolder) { + name = workspaceFolder.name; + } + } + + if (!name) { + name = paths.basename(resource.fsPath); + } } diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 468894d710e..1c7c48aa596 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -414,9 +414,14 @@ export class ViewsViewlet extends Viewlet { } } - private toggleViewVisibility(id: string): void { + toggleViewVisibility(id: string, visible?: boolean): void { const view = this.getView(id); let viewState = this.viewsStates.get(id); + + if ((visible === true && view) || (visible === false && !view)) { + return; + } + if (view) { viewState = viewState || this.createViewState(view); viewState.isHidden = true; @@ -557,7 +562,7 @@ export class ViewsViewlet extends Viewlet { private canBeVisible(viewDescriptor: IViewDescriptor): boolean { const viewstate = this.viewsStates.get(viewDescriptor.id); - if (viewDescriptor.canToggleVisibility && viewstate && viewstate.isHidden) { + if (viewstate && viewstate.isHidden) { return false; } return this.contextKeyService.contextMatchesRules(viewDescriptor.when); diff --git a/src/vs/workbench/browser/parts/views/views2.ts b/src/vs/workbench/browser/parts/views/views2.ts new file mode 100644 index 00000000000..1721592a293 --- /dev/null +++ b/src/vs/workbench/browser/parts/views/views2.ts @@ -0,0 +1,272 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import Event, { Emitter } from 'vs/base/common/event'; +import { ColorIdentifier, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { attachStyler, IColorMapping, IThemable } from 'vs/platform/theme/common/styler'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND } from 'vs/workbench/common/theme'; +import { Dimension, Builder } from 'vs/base/browser/builder'; +import { append, $ } from 'vs/base/browser/dom'; +import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { firstIndex } from 'vs/base/common/arrays'; +import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { prepareActions } from 'vs/workbench/browser/actions'; +import { Viewlet, ViewletRegistry, Extensions } from 'vs/workbench/browser/viewlet'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { PanelView, IPanelOptions, Panel } from 'vs/base/browser/ui/splitview/panelview'; + +export interface IPanelColors extends IColorMapping { + dropBackground?: ColorIdentifier; + headerForeground?: ColorIdentifier; + headerBackground?: ColorIdentifier; + headerHighContrastBorder?: ColorIdentifier; +} + +export function attachPanelStyler(widget: IThemable, themeService: IThemeService) { + return attachStyler(themeService, { + headerForeground: SIDE_BAR_SECTION_HEADER_FOREGROUND, + headerBackground: SIDE_BAR_SECTION_HEADER_BACKGROUND, + headerHighContrastBorder: contrastBorder, + dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND + }, widget); +} + +export interface IViewletPanelOptions extends IPanelOptions { + actionRunner?: IActionRunner; +} + +export abstract class ViewletPanel extends Panel { + + private _onDidFocus = new Emitter(); + readonly onDidFocus: Event = this._onDidFocus.event; + + private actionRunner: IActionRunner; + private toolbar: ToolBar; + + constructor( + readonly title: string, + options: IViewletPanelOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService + ) { + super(options); + + this.actionRunner = options.actionRunner; + } + + protected renderHeader(container: HTMLElement): void { + this.renderHeaderTitle(container); + + const actions = append(container, $('.actions')); + this.toolbar = new ToolBar(actions, this.contextMenuService, { + orientation: ActionsOrientation.HORIZONTAL, + actionItemProvider: action => this.getActionItem(action), + ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionRunner: this.actionRunner + }); + + this.disposables.push(this.toolbar); + this.updateActions(); + } + + protected renderHeaderTitle(container: HTMLElement): void { + append(container, $('.title', null, this.title)); + } + + focus(): void { + this._onDidFocus.fire(); + } + + protected updateActions(): void { + this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions()))(); + this.toolbar.context = this.getActionsContext(); + } + + getActions(): IAction[] { + return []; + } + + getSecondaryActions(): IAction[] { + return []; + } + + getActionItem(action: IAction): IActionItem { + return null; + } + + getActionsContext(): any { + return undefined; + } + + getOptimalWidth(): number { + return 0; + } +} + +export interface IViewsViewletOptions { + showHeaderInTitleWhenSingleView: boolean; +} + +interface IViewletPanelItem { + panel: ViewletPanel; + disposable: IDisposable; +} + +export class PanelViewlet extends Viewlet { + + protected lastFocusedPanel: ViewletPanel | undefined; + private panelItems: IViewletPanelItem[] = []; + private panelview: PanelView; + + // TODO@Joao make this into method so people can override it + protected get isSingleView(): boolean { + return this.options.showHeaderInTitleWhenSingleView && this.panelItems.length === 1; + } + + protected get length(): number { + return this.panelItems.length; + } + + constructor( + id: string, + private options: Partial, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService + ) { + super(id, telemetryService, themeService); + } + + async create(parent: Builder): TPromise { + super.create(parent); + + const container = parent.getHTMLElement(); + this.panelview = this._register(new PanelView(container)); + } + + getTitle(): string { + let title = Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; + + if (this.isSingleView) { + title += ': ' + this.panelItems[0].panel.title; + } + + return title; + } + + getActions(): IAction[] { + if (this.isSingleView) { + return this.panelItems[0].panel.getActions(); + } + + return []; + } + + getSecondaryActions(): IAction[] { + if (this.isSingleView) { + return this.panelItems[0].panel.getSecondaryActions(); + } + + return []; + } + + focus(): void { + super.focus(); + + if (this.lastFocusedPanel) { + this.lastFocusedPanel.focus(); + } else if (this.panelItems.length > 0) { + this.panelItems[0].panel.focus(); + } + } + + layout(dimension: Dimension): void { + this.panelview.layout(dimension.height); + } + + getOptimalWidth(): number { + const sizes = this.panelItems + .map(panelItem => panelItem.panel.getOptimalWidth() || 0); + + return Math.max(...sizes); + } + + addPanel(panel: ViewletPanel, size: number, index = this.panelItems.length - 1): void { + const disposables: IDisposable[] = []; + const onDidFocus = panel.onDidFocus(() => this.lastFocusedPanel = panel, null, disposables); + const styler = attachPanelStyler(panel, this.themeService); + const disposable = combinedDisposable([onDidFocus, styler]); + const panelItem: IViewletPanelItem = { panel, disposable }; + + this.panelItems.splice(index, 0, panelItem); + this.panelview.addPanel(panel, size, index); + + this.updateViewHeaders(); + this.updateTitleArea(); + } + + removePanel(panel: ViewletPanel): void { + const index = firstIndex(this.panelItems, i => i.panel === panel); + + if (index === -1) { + return; + } + + if (this.lastFocusedPanel === panel) { + this.lastFocusedPanel = undefined; + } + + this.panelview.removePanel(panel); + const [panelItem] = this.panelItems.splice(index, 1); + panelItem.disposable.dispose(); + + this.updateViewHeaders(); + this.updateTitleArea(); + } + + movePanel(from: ViewletPanel, to: ViewletPanel): void { + const fromIndex = firstIndex(this.panelItems, item => item.panel === from); + const toIndex = firstIndex(this.panelItems, item => item.panel === to); + + if (fromIndex < 0 || fromIndex >= this.panelItems.length) { + return; + } + + if (toIndex < 0 || toIndex >= this.panelItems.length) { + return; + } + + + const [panelItem] = this.panelItems.splice(fromIndex, 1); + this.panelItems.splice(toIndex < fromIndex ? toIndex : toIndex - 1, 0, panelItem); + this.panelview.movePanel(from, to); + } + + resizePanel(panel: ViewletPanel, size: number): void { + this.panelview.resizePanel(panel, size); + } + + private updateViewHeaders(): void { + if (this.isSingleView) { + this.panelItems[0].panel.headerVisible = false; + } else { + this.panelItems.forEach(i => i.panel.headerVisible = true); + } + } + + dispose(): void { + super.dispose(); + this.panelItems.forEach(i => i.disposable.dispose()); + this.panelview.dispose(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 7640073567b..311b186a859 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts @@ -1368,11 +1368,13 @@ export class ConfigureWorkspaceRecommendedExtensionsAction extends AbstractConfi } public run(event: any): TPromise { - const workspace = this.contextService.getWorkspace(); - if (workspace.configuration) { - return this.openWorkspaceConfigurationFile(workspace.configuration); + switch (this.contextService.getWorkbenchState()) { + case WorkbenchState.FOLDER: + return this.openExtensionsFile(this.contextService.toResource(paths.join('.vscode', 'extensions.json'), this.contextService.getWorkspace().folders[0])); + case WorkbenchState.WORKSPACE: + return this.openWorkspaceConfigurationFile(this.contextService.getWorkspace().configuration); } - return this.openExtensionsFile(this.contextService.toResource(paths.join('.vscode', 'extensions.json'), workspace.folders[0])); + return TPromise.as(null); } dispose(): void { diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 9cebb13bc64..d05db9e48fe 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -20,7 +20,7 @@ import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, IExtensionManifest, - InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionTipsService + InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionTipsService, ErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionIdFromLocal, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -725,7 +725,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } if (extension.gallery) { // Report telemetry only for gallery extensions - this.reportTelemetry(installing, !error); + this.reportTelemetry(installing, error); } } this._onChange.fire(); @@ -759,7 +759,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } if (!error) { - this.reportTelemetry(uninstalling, true); + this.reportTelemetry(uninstalling); } this._onChange.fire(); @@ -789,12 +789,12 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { return local ? ExtensionState.Installed : ExtensionState.Uninstalled; } - private reportTelemetry(active: IActiveExtension, success: boolean): void { + private reportTelemetry(active: IActiveExtension, errorcode?: ErrorCode): void { const data = active.extension.telemetryData; const duration = new Date().getTime() - active.start.getTime(); const eventName = toTelemetryEventName(active.operation); - this.telemetryService.publicLog(eventName, assign(data, { success, duration })); + this.telemetryService.publicLog(eventName, assign(data, { success: !errorcode, duration, errorcode })); } private onError(err: any): void { diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index c2027e6fd4f..a20ef64cf68 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -19,7 +19,7 @@ } .scm-viewlet:not(.empty) .empty-message, -.scm-viewlet.empty .monaco-split-view{ +.scm-viewlet.empty .monaco-panel-view { display: none; } @@ -27,6 +27,30 @@ height: 100%; } +.scm-viewlet .monaco-list-row > .scm-provider { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.scm-viewlet .monaco-list-row > .scm-provider > input { + flex-shrink: 0; +} + +.scm-viewlet .scm-provider > .count { + margin: 0 0.5em; +} + +.scm-viewlet .scm-provider > .count.hidden { + display: none; +} + +.scm-viewlet .scm-provider > .name > .type { + opacity: 0.7; + margin-left: 0.5em; + font-size: 0.9em; +} + .scm-viewlet .monaco-list-row { padding: 0 12px 0 20px; line-height: 22px; @@ -125,4 +149,8 @@ .scm-viewlet .scm-editor.scroll > .monaco-inputbox > .wrapper > textarea.input { overflow-y: scroll; +} + +.scm-viewlet .spacer { + flex: 1; } \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts index 81489a979b4..f5900b648ef 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts @@ -6,6 +6,7 @@ 'use strict'; import { localize } from 'vs/nls'; +import { basename } from 'vs/base/common/paths'; import { IDisposable, dispose, empty as EmptyDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { filterEvent, any as anyEvent } from 'vs/base/common/event'; import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; @@ -139,9 +140,13 @@ export class StatusBarController implements IWorkbenchContribution { this.statusBarDisposable.dispose(); const commands = repository.provider.statusBarCommands || []; + const label = repository.provider.rootUri + ? `${basename(repository.provider.rootUri.fsPath)} (${repository.provider.label})` + : repository.provider.label; + const disposables = commands.map(c => this.statusbarService.addEntry({ text: c.title, - tooltip: `${repository.provider.label} - ${c.tooltip}`, + tooltip: `${label} - ${c.tooltip}`, command: c.id, arguments: c.arguments }, MainThreadStatusBarAlignment.LEFT, 10000)); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 6fb5efcb992..c88fb426b53 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -8,12 +8,13 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import { chain } from 'vs/base/common/event'; +import Event, { Emitter, chain } from 'vs/base/common/event'; +import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Builder } from 'vs/base/browser/builder'; -import { PersistentViewsViewlet, CollapsibleView, IViewletViewOptions, IViewletView, IViewOptions } from 'vs/workbench/browser/parts/views/views'; -import { append, $, toggleClass, trackFocus } from 'vs/base/browser/dom'; +import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Builder, Dimension } from 'vs/base/browser/builder'; +import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/views2'; +import { append, $, addClass, toggleClass, trackFocus } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; @@ -34,7 +35,7 @@ import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { IAction, Action, IActionItem, ActionRunner } from 'vs/base/common/actions'; import { MenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem'; import { SCMMenus } from './scmMenus'; -import { ActionBar, IActionItemProvider, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, IActionItemProvider, Separator, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; import { isSCMResource } from './scmUtil'; import { attachListStyler, attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -42,15 +43,15 @@ import Severity from 'vs/base/common/severity'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ViewLocation, ViewsRegistry, IViewDescriptor } from 'vs/workbench/browser/parts/views/viewsRegistry'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { ViewSizing } from 'vs/base/browser/ui/splitview/splitview'; import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/parts/extensions/common/extensions'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import * as platform from 'vs/base/common/platform'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { Command } from 'vs/editor/common/modes'; +import { render as renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; // TODO@Joao // Need to subclass MenuItemActionItem in order to respect @@ -67,20 +68,196 @@ class SCMMenuItemActionItem extends MenuItemActionItem { } } -function identityProvider(r: ISCMResourceGroup | ISCMResource): string { - if (isSCMResource(r)) { - const group = r.resourceGroup; - const provider = group.provider; - return `${provider.contextValue}/${group.id}/${r.sourceUri.toString()}`; - } else { - const provider = r.provider; - return `${provider.contextValue}/${r.id}`; +export interface ISpliceEvent { + index: number; + deleteCount: number; + elements: T[]; +} + +export interface IViewModel { + readonly repositories: ISCMRepository[]; + readonly onDidSplice: Event>; +} + +class ProvidersListDelegate implements IDelegate { + + getHeight(element: ISCMRepository): number { + return 22; + } + + getTemplateId(element: ISCMRepository): string { + return 'provider'; } } -interface SearchInputEvent extends Event { - target: HTMLInputElement; - immediate?: boolean; +class StatusBarAction extends Action { + + constructor( + private command: Command, + private commandService: ICommandService + ) { + super(`statusbaraction{${command.id}}`, command.title, '', true); + this.tooltip = command.tooltip; + } + + run(): TPromise { + return this.commandService.executeCommand(this.command.id, ...this.command.arguments); + } +} + +class StatusBarActionItem extends ActionItem { + + constructor(action: StatusBarAction) { + super(null, action, {}); + } + + _updateLabel(): void { + if (this.options.label) { + this.$e.innerHtml(renderOcticons(this.getAction().label)); + } + } +} + +interface RepositoryTemplateData { + title: HTMLElement; + type: HTMLElement; + countContainer: HTMLElement; + count: CountBadge; + actionBar: ActionBar; + disposable: IDisposable; + templateDisposable: IDisposable; +} + +class ProviderRenderer implements IRenderer { + + readonly templateId = 'provider'; + + constructor( + @ICommandService protected commandService: ICommandService, + @IThemeService protected themeService: IThemeService + ) { } + + renderTemplate(container: HTMLElement): RepositoryTemplateData { + const provider = append(container, $('.scm-provider')); + const name = append(provider, $('.name')); + const title = append(name, $('span.title')); + const type = append(name, $('span.type')); + const countContainer = append(provider, $('.count')); + + append(provider, $('.spacer')); + + const count = new CountBadge(countContainer); + const badgeStyler = attachBadgeStyler(count, this.themeService); + const actionBar = new ActionBar(provider, { actionItemProvider: a => new StatusBarActionItem(a as StatusBarAction) }); + const disposable = EmptyDisposable; + const templateDisposable = combinedDisposable([actionBar, badgeStyler]); + + return { title, type, countContainer, count, actionBar, disposable, templateDisposable }; + } + + renderElement(repository: ISCMRepository, index: number, templateData: RepositoryTemplateData): void { + templateData.disposable.dispose(); + const disposables: IDisposable[] = []; + + if (repository.provider.rootUri) { + templateData.title.textContent = basename(repository.provider.rootUri.fsPath); + templateData.type.textContent = repository.provider.label; + } else { + templateData.title.textContent = repository.provider.label; + templateData.type.textContent = ''; + } + + // const disposables = commands.map(c => this.statusbarService.addEntry({ + // text: c.title, + // tooltip: `${repository.provider.label} - ${c.tooltip}`, + // command: c.id, + // arguments: c.arguments + // }, MainThreadStatusBarAlignment.LEFT, 10000)); + + const actions = []; + const disposeActions = () => dispose(actions); + disposables.push({ dispose: disposeActions }); + + const update = () => { + disposeActions(); + + const commands = repository.provider.statusBarCommands || []; + actions.splice(0, actions.length, ...commands.map(c => new StatusBarAction(c, this.commandService))); + templateData.actionBar.clear(); + templateData.actionBar.push(actions); + + const count = repository.provider.count || 0; + toggleClass(templateData.countContainer, 'hidden', count === 0); + templateData.count.setCount(repository.provider.count); + }; + + repository.provider.onDidChange(update, null, disposables); + update(); + + templateData.disposable = combinedDisposable(disposables); + } + + disposeTemplate(templateData: RepositoryTemplateData): void { + templateData.disposable.dispose(); + templateData.templateDisposable.dispose(); + } +} + +class MainPanel extends ViewletPanel { + + private list: List; + + private _onSelectionChange = new Emitter(); + readonly onSelectionChange: Event = this._onSelectionChange.event; + + constructor( + protected viewModel: IViewModel, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @ISCMService protected scmService: ISCMService, + @IInstantiationService private instantiationService: IInstantiationService, + @IThemeService private themeService: IThemeService + ) { + super(localize('scm providers', "Source Control Providers"), {}, keybindingService, contextMenuService); + this.updateBodySize(); + } + + private splice(index: number, deleteCount: number, repositories: ISCMRepository[] = []): void { + const wasEmpty = this.list.length === 0; + + this.list.splice(index, deleteCount, repositories); + this.updateBodySize(); + + // Automatically select the first one + if (wasEmpty && this.list.length > 0) { + this.list.setSelection([0]); + } + } + + protected renderBody(container: HTMLElement): void { + const delegate = new ProvidersListDelegate(); + const renderer = this.instantiationService.createInstance(ProviderRenderer); + + this.list = new List(container, delegate, [renderer], { + identityProvider: repository => repository.provider.id + }); + + this.disposables.push(this.list); + this.disposables.push(attachListStyler(this.list, this.themeService)); + this.list.onSelectionChange(e => this._onSelectionChange.fire(e.elements), null, this.disposables); + this.viewModel.onDidSplice(({ index, deleteCount, elements }) => this.splice(index, deleteCount, elements), null, this.disposables); + this.splice(0, 0, this.viewModel.repositories); + } + + protected layoutBody(size: number): void { + this.list.layout(size); + } + + private updateBodySize(): void { + const size = Math.min(5, this.viewModel.repositories.length) * 22; + this.minimumBodySize = size; + this.maximumBodySize = size; + } } interface ResourceGroupTemplate { @@ -219,7 +396,7 @@ class ResourceRenderer implements IRenderer { } } -class Delegate implements IDelegate { +class ProviderListDelegate implements IDelegate { getHeight() { return 22; } @@ -228,47 +405,28 @@ class Delegate implements IDelegate { } } -class SourceControlViewDescriptor implements IViewDescriptor { - - // This ID magic needs to happen in order to preserve - // good splitview state when reloading the workbench - static idCount = 0; - static freeIds: string[] = []; - - readonly id: string; - - get repository(): ISCMRepository { return this._repository; } - get name(): string { return this._repository.provider.label; } - get ctor(): any { return null; } - get location(): ViewLocation { return ViewLocation.SCM; } - - constructor(private _repository: ISCMRepository) { - if (SourceControlViewDescriptor.freeIds.length > 0) { - this.id = SourceControlViewDescriptor.freeIds.shift(); - } else { - this.id = `scm${SourceControlViewDescriptor.idCount++}`; - } - } - - dispose(): void { - SourceControlViewDescriptor.freeIds.push(this.id); +function scmResourceIdentityProvider(r: ISCMResourceGroup | ISCMResource): string { + if (isSCMResource(r)) { + const group = r.resourceGroup; + const provider = group.provider; + return `${provider.contextValue}/${group.id}/${r.sourceUri.toString()}`; + } else { + const provider = r.provider; + return `${provider.contextValue}/${r.id}`; } } -class SourceControlView extends CollapsibleView { +export class RepositoryPanel extends ViewletPanel { - private cachedHeight: number | undefined; + private cachedHeight: number | undefined = undefined; private inputBoxContainer: HTMLElement; private inputBox: InputBox; private listContainer: HTMLElement; private list: List; private menus: SCMMenus; - private disposables: IDisposable[] = []; constructor( - initialSize: number, - private repository: ISCMRepository, - options: IViewletViewOptions, + readonly repository: ISCMRepository, @IKeybindingService protected keybindingService: IKeybindingService, @IThemeService protected themeService: IThemeService, @IContextMenuService protected contextMenuService: IContextMenuService, @@ -280,20 +438,31 @@ class SourceControlView extends CollapsibleView { @IEditorGroupService protected editorGroupService: IEditorGroupService, @IInstantiationService protected instantiationService: IInstantiationService ) { - super(initialSize, { ...(options as IViewOptions), sizing: ViewSizing.Flexible }, keybindingService, contextMenuService); - + super(repository.provider.label, {}, keybindingService, contextMenuService); this.menus = instantiationService.createInstance(SCMMenus, repository.provider); + } + + render(container: HTMLElement): void { + super.render(container); this.menus.onDidChangeTitle(this.updateActions, this, this.disposables); } - renderHeader(container: HTMLElement): void { - const title = append(container, $('div.title')); - title.textContent = this.name; + protected renderHeaderTitle(container: HTMLElement): void { + const header = append(container, $('.title.scm-provider')); + const name = append(header, $('.name')); + const title = append(name, $('span.title')); + const type = append(name, $('span.type')); - super.renderHeader(container); + if (this.repository.provider.rootUri) { + title.textContent = basename(this.repository.provider.rootUri.fsPath); + type.textContent = this.repository.provider.label; + } else { + title.textContent = this.repository.provider.label; + type.textContent = ''; + } } - renderBody(container: HTMLElement): void { + protected renderBody(container: HTMLElement): void { const focusTracker = trackFocus(container); this.disposables.push(focusTracker.addFocusListener(() => this.repository.focus())); this.disposables.push(focusTracker); @@ -327,7 +496,7 @@ class SourceControlView extends CollapsibleView { // List this.listContainer = append(container, $('.scm-status.show-file-icons')); - const delegate = new Delegate(); + const delegate = new ProviderListDelegate(); const actionItemProvider = (action: IAction) => this.getActionItem(action); @@ -337,7 +506,7 @@ class SourceControlView extends CollapsibleView { ]; this.list = new List(this.listContainer, delegate, renderers, { - identityProvider, + identityProvider: scmResourceIdentityProvider, keyboardSupport: false }); @@ -362,7 +531,7 @@ class SourceControlView extends CollapsibleView { } layoutBody(height: number = this.cachedHeight): void { - if (!height === undefined) { + if (height === undefined) { return; } @@ -379,7 +548,9 @@ class SourceControlView extends CollapsibleView { } focus(): void { - if (this.isExpanded()) { + super.focus(); + + if (this.expanded) { this.inputBox.focus(); } } @@ -491,16 +662,28 @@ class InstallAdditionalSCMProvidersAction extends Action { } } -export class SCMViewlet extends PersistentViewsViewlet { +export class SCMViewlet extends PanelViewlet implements IViewModel { + private el: HTMLElement; private menus: SCMMenus; - private repositoryToViewDescriptor = new Map(); + private mainPanel: MainPanel | null = null; + private mainPanelDisposable: IDisposable = EmptyDisposable; + private _repositories: ISCMRepository[] = []; + private repositoryPanels: RepositoryPanel[] = []; private disposables: IDisposable[] = []; + private _onDidSplice = new Emitter>(); + readonly onDidSplice: Event> = this._onDidSplice.event; + + private _height: number | undefined = undefined; + get height(): number | undefined { return this._height; } + + get repositories(): ISCMRepository[] { return this._repositories; } + constructor( @ITelemetryService telemetryService: ITelemetryService, @ISCMService protected scmService: ISCMService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService protected instantiationService: IInstantiationService, @IContextViewService protected contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService protected keybindingService: IKeybindingService, @@ -515,53 +698,79 @@ export class SCMViewlet extends PersistentViewsViewlet { @IStorageService storageService: IStorageService, @IExtensionService extensionService: IExtensionService ) { - super(VIEWLET_ID, ViewLocation.SCM, 'scm', true, - telemetryService, storageService, instantiationService, themeService, contextService, contextKeyService, contextMenuService, extensionService); + super(VIEWLET_ID, { showHeaderInTitleWhenSingleView: true }, telemetryService, themeService); this.menus = instantiationService.createInstance(SCMMenus, undefined); this.menus.onDidChangeTitle(this.updateTitleArea, this, this.disposables); } - private onDidAddRepository(repository: ISCMRepository): void { - const viewDescriptor = new SourceControlViewDescriptor(repository); - this.repositoryToViewDescriptor.set(repository.provider.id, viewDescriptor); - - ViewsRegistry.registerViews([viewDescriptor]); - toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); - this.updateTitleArea(); - } - - private onDidRemoveRepository(repository: ISCMRepository): void { - const viewDescriptor = this.repositoryToViewDescriptor.get(repository.provider.id); - this.repositoryToViewDescriptor.delete(repository.provider.id); - viewDescriptor.dispose(); - - ViewsRegistry.deregisterViews([viewDescriptor.id], ViewLocation.SCM); - toggleClass(this.getContainer().getHTMLElement(), 'empty', this.views.length === 0); - this.updateTitleArea(); - } - async create(parent: Builder): TPromise { await super.create(parent); - parent.addClass('scm-viewlet', 'empty'); - append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no source control providers active."))); + this.el = parent.getHTMLElement(); + addClass(this.el, 'scm-viewlet'); + addClass(this.el, 'empty'); + append(parent.getHTMLElement(), $('div.empty-message', null, localize('no open repo', "There are no active source control providers."))); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - this.scmService.repositories.forEach(p => this.onDidAddRepository(p)); + this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); + this.onDidChangeRepositories(); } - protected createView(viewDescriptor: IViewDescriptor, initialSize: number, options: IViewletViewOptions): IViewletView { - if (viewDescriptor instanceof SourceControlViewDescriptor) { - return this.instantiationService.createInstance(SourceControlView, initialSize, viewDescriptor.repository, options); + private onDidAddRepository(repository: ISCMRepository): void { + this.onDidChangeRepositories(); + + const index = this._repositories.length; + this._repositories.push(repository); + this._onDidSplice.fire({ index, deleteCount: 0, elements: [repository] }); + + if (!this.mainPanel) { + this.onSelectionChange(this.repositories); + } + } + + private onDidRemoveRepository(repository: ISCMRepository): void { + this.onDidChangeRepositories(); + + const index = this._repositories.indexOf(repository); + + if (index === -1) { + return; } - return this.instantiationService.createInstance(viewDescriptor.ctor, initialSize, options); + this._repositories.splice(index, 1); + this._onDidSplice.fire({ index, deleteCount: 1, elements: [] }); + + if (!this.mainPanel) { + this.onSelectionChange(this.repositories); + } } - protected getDefaultViewSize(): number | undefined { - return this.dimension && this.dimension.height / Math.max(this.views.length, 1); + private onDidChangeRepositories(): void { + toggleClass(this.el, 'empty', this.scmService.repositories.length === 0); + + const shouldMainPanelBeVisible = this.scmService.repositories.length > 1; + + if (!!this.mainPanel === shouldMainPanelBeVisible) { + return; + } + + if (shouldMainPanelBeVisible) { + this.mainPanel = this.instantiationService.createInstance(MainPanel, this); + const selectionChangeDisposable = this.mainPanel.onSelectionChange(this.onSelectionChange, this); + this.addPanel(this.mainPanel, this.mainPanel.minimumSize, 0); + + this.mainPanelDisposable = toDisposable(() => { + this.removePanel(this.mainPanel); + selectionChangeDisposable.dispose(); + this.mainPanel.dispose(); + }); + } else { + this.mainPanelDisposable.dispose(); + this.mainPanelDisposable = EmptyDisposable; + this.mainPanel = null; + } } getOptimalWidth(): number { @@ -570,19 +779,19 @@ export class SCMViewlet extends PersistentViewsViewlet { getTitle(): string { const title = localize('source control', "Source Control"); - const views = ViewsRegistry.getViews(ViewLocation.SCM); - if (views.length === 1) { - const view = views[0]; - return localize('viewletTitle', "{0}: {1}", title, view.name); + if (this.repositories.length === 1) { + const [repository] = this.repositories; + return localize('viewletTitle', "{0}: {1}", title, repository.provider.label); } else { return title; } } getActions(): IAction[] { - if (this.showHeaderInTitleArea() && this.views.length === 1) { - return this.views[0].getActions(); + if (this.isSingleView && this.repositories.length === 1) { + const [panel] = this.repositoryPanels; + return panel.getActions(); } return this.menus.getTitleActions(); @@ -591,9 +800,11 @@ export class SCMViewlet extends PersistentViewsViewlet { getSecondaryActions(): IAction[] { let result: IAction[]; - if (this.showHeaderInTitleArea() && this.views.length === 1) { + if (this.isSingleView && this.repositories.length === 1) { + const [panel] = this.repositoryPanels; + result = [ - ...this.views[0].getSecondaryActions(), + ...panel.getSecondaryActions(), new Separator() ]; } else { @@ -617,8 +828,46 @@ export class SCMViewlet extends PersistentViewsViewlet { return new SCMMenuItemActionItem(action, this.keybindingService, this.messageService); } + layout(dimension: Dimension): void { + super.layout(dimension); + this._height = dimension.height; + } + + private onSelectionChange(repositories: ISCMRepository[]): void { + // Remove unselected panels + this.repositoryPanels + .filter(p => repositories.every(r => p.repository !== r)) + .forEach(panel => this.removePanel(panel)); + + // Collect panels still selected + const repositoryPanels = this.repositoryPanels + .filter(p => repositories.some(r => p.repository === r)); + + // Collect new selected panels + const newRepositoryPanels = repositories + .filter(r => this.repositoryPanels.every(p => p.repository !== r)) + .map(r => this.instantiationService.createInstance(RepositoryPanel, r)); + + // Add new selected panels + this.repositoryPanels = [...repositoryPanels, ...newRepositoryPanels]; + newRepositoryPanels.forEach(panel => { + this.addPanel(panel, panel.minimumSize, this.length); + panel.repository.focus(); + }); + + // Resize all panels equally + const height = typeof this.height === 'number' ? this.height : 1000; + const mainPanelHeight = this.mainPanel ? this.mainPanel.minimumSize : 0; + const size = (height - mainPanelHeight) / repositories.length; + + for (const panel of this.repositoryPanels) { + this.resizePanel(panel, size); + } + } + dispose(): void { this.disposables = dispose(this.disposables); + this.mainPanelDisposable.dispose(); super.dispose(); } } diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index ec069ee2197..522ae4e54ef 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -324,12 +324,12 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider { const match = element; const searchModel: SearchModel = (tree.getInput()).searchModel; const replace = searchModel.isReplaceActive() && !!searchModel.replaceString; - const preview = match.preview(); + const matchString = match.getMatchString(); const range = match.range(); if (replace) { - return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", preview.inside, match.replaceString, range.startColumn + 1, match.text()); + return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", matchString, match.replaceString, range.startColumn + 1, match.text()); } - return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", preview.inside, range.startColumn + 1, match.text()); + return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, match.text()); } return undefined; } diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css index 1d9961ab042..e032054ea47 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css @@ -41,7 +41,10 @@ height: 100%; } -.monaco-workbench .panel.integrated-terminal .xterm-viewport, +.monaco-workbench .panel.integrated-terminal .xterm-viewport { + margin-right: -20px; +} + .monaco-workbench .panel.integrated-terminal canvas { /* Align the viewport and canvases to the bottom of the panel */ position: absolute; @@ -71,8 +74,9 @@ } .monaco-workbench .panel.integrated-terminal .xterm { - position: relative; - height: 100%; + position: absolute; + bottom: 0; + left: 0; user-select: none; } diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index 056c367bf57..a5199b8236c 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -9,7 +9,6 @@ import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; import Event, { Emitter } from 'vs/base/common/event'; import { StrictResourceMap } from 'vs/base/common/map'; -import { equals } from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; import * as errors from 'vs/base/common/errors'; import * as collections from 'vs/base/common/collections'; @@ -18,7 +17,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { readFile, stat, writeFile } from 'vs/base/node/pfs'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import * as extfs from 'vs/base/node/extfs'; -import { IWorkspaceContextService, IWorkspace, Workspace, WorkbenchState, WorkspaceFolder, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace, Workspace, WorkbenchState, WorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; import { ConfigWatcher } from 'vs/base/node/config'; @@ -253,8 +252,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat protected readonly _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; - protected readonly _onDidChangeWorkspaceFolders: Emitter = this._register(new Emitter()); - public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; + protected readonly _onDidChangeWorkspaceFolders: Emitter = this._register(new Emitter()); + public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; protected readonly _onDidChangeWorkspaceName: Emitter = this._register(new Emitter()); public readonly onDidChangeWorkspaceName: Event = this._onDidChangeWorkspaceName.event; @@ -438,11 +437,25 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat this._onDidChangeWorkspaceName.fire(); } - if (!equals(this.workspace.folders, currentFolders, (folder1, folder2) => folder1.uri.toString() === folder2.uri.toString())) { - this._onDidChangeWorkspaceFolders.fire(); + const changes = this.compareFolders(currentFolders, this.workspace.folders); + if (changes.added.length || changes.removed.length || changes.changed.length) { + this._onDidChangeWorkspaceFolders.fire(changes); } } + private compareFolders(currentFolders: WorkspaceFolder[], newFolders: WorkspaceFolder[]): IWorkspaceFoldersChangeEvent { + const result = { added: [], removed: [], changed: [] }; + + result.added = newFolders.filter(newFolder => !currentFolders.some(currentFolder => newFolder.uri.toString() === currentFolder.uri.toString())); + result.removed = currentFolders.filter(currentFolder => !newFolders.some(newFolder => currentFolder.uri.toString() === newFolder.uri.toString())); + + if (result.added.length === 0 && result.removed.length === 0) { + result.changed = currentFolders.filter((currentFolder, index) => newFolders[index].uri.toString() !== currentFolder.uri.toString()); + } + + return result; + } + private initializeConfiguration(trigger: boolean = true): TPromise { this.resetCaches(); return this.updateConfiguration() @@ -487,12 +500,12 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private onWorkspaceConfigurationChanged(): void { if (this.workspace && this.workspace.configuration) { let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders, URI.file(paths.dirname(this.workspace.configuration.fsPath))); - const foldersChanged = !equals(this.workspace.folders, configuredFolders, (folder1, folder2) => folder1.uri.toString() === folder2.uri.toString()); - if (foldersChanged) { // TODO@Sandeep be smarter here about detecting changes + const changes = this.compareFolders(this.workspace.folders, configuredFolders); + if (changes.added.length || changes.removed.length || changes.changed.length) { // TODO@Sandeep be smarter here about detecting changes this.workspace.folders = configuredFolders; this.onFoldersChanged() .then(configurationChanged => { - this._onDidChangeWorkspaceFolders.fire(); + this._onDidChangeWorkspaceFolders.fire(changes); if (configurationChanged) { this.triggerConfigurationChange(); } diff --git a/src/vs/workbench/services/scm/common/scm.ts b/src/vs/workbench/services/scm/common/scm.ts index 960b678cb14..7147cff15d6 100644 --- a/src/vs/workbench/services/scm/common/scm.ts +++ b/src/vs/workbench/services/scm/common/scm.ts @@ -60,6 +60,7 @@ export interface ISCMProvider extends IDisposable { readonly resources: ISCMResourceGroup[]; readonly onDidChangeResources: Event; + readonly rootUri?: URI; readonly count?: number; readonly commitTemplate?: string; readonly onDidChangeCommitTemplate?: Event; diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 173f8e1d103..e1789dfa8af 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -99,15 +99,9 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } private doSetFolders(folders: IStoredWorkspaceFolder[]): TPromise { - if (folders.length) { - const workspace = this.contextService.getWorkspace(); + const workspace = this.contextService.getWorkspace(); - return this.jsonEditingService.write(workspace.configuration, { key: 'folders', value: folders }, true); - } else { - // TODO: Sandeep - Removing all folders? - } - - return TPromise.as(void 0); + return this.jsonEditingService.write(workspace.configuration, { key: 'folders', value: folders }, true); } private isSupported(): boolean { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 1502ef8fe6e..7f89866c96d 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -27,7 +27,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorInput, IEditorOptions, Position, Direction, IEditor, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IMessageService, IConfirmation } from 'vs/platform/message/common/message'; -import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, WorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { EditorStacksModel } from 'vs/workbench/common/editor/editorStacksModel'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -74,14 +74,14 @@ export class TestContextService implements IWorkspaceContextService { private options: any; private _onDidChangeWorkspaceName: Emitter; - private _onDidChangeWorkspaceFolders: Emitter; + private _onDidChangeWorkspaceFolders: Emitter; private _onDidChangeWorkbenchState: Emitter; constructor(workspace: any = TestWorkspace, options: any = null) { this.workspace = workspace; this.id = generateUuid(); this.options = options || Object.create(null); - this._onDidChangeWorkspaceFolders = new Emitter(); + this._onDidChangeWorkspaceFolders = new Emitter(); this._onDidChangeWorkbenchState = new Emitter(); } @@ -89,7 +89,7 @@ export class TestContextService implements IWorkspaceContextService { return this._onDidChangeWorkspaceName.event; } - public get onDidChangeWorkspaceFolders(): Event { + public get onDidChangeWorkspaceFolders(): Event { return this._onDidChangeWorkspaceFolders.event; }