From b852f032d3076630d95678d17a48f2f239a338ff Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Wed, 17 Apr 2019 23:38:44 -0700 Subject: [PATCH] Merge from vscode 3a6dcb42008d509900b3a3b2d695564eeb4dbdac (#5098) --- resources/win32/bin/code.cmd | 2 +- src/vs/base/node/encoding.ts | 87 +++++++------- .../contextmenu/electron-main/contextmenu.ts | 4 +- src/vs/base/parts/ipc/common/ipc.net.ts | 18 ++- .../base/test/node/encoding/encoding.test.ts | 10 +- .../issue/issueReporterMain.ts | 29 +++-- .../issue/issueReporterModel.ts | 11 +- .../electron-browser/workbench/workbench.html | 4 +- src/vs/code/electron-main/windows.ts | 79 +++++++----- .../editor/browser/services/openerService.ts | 2 +- src/vs/editor/common/modes.ts | 2 + .../common/services/editorSimpleWorker.ts | 4 +- .../services/editorWorkerServiceImpl.ts | 13 +- .../standalone/browser/standaloneServices.ts | 4 +- .../diagnostics/common/diagnosticsService.ts | 2 +- .../extensions/node/extensionsUtil.ts | 21 ++-- .../telemetry/node/commonProperties.ts | 3 +- src/vs/vscode.proposed.d.ts | 39 ++++++ .../api/browser/mainThreadComments.ts | 23 +++- .../workbench/api/browser/mainThreadWindow.ts | 9 +- .../api/browser/mainThreadWorkspace.ts | 6 +- src/vs/workbench/api/common/apiCommands.ts | 3 +- .../workbench/api/common/extHost.protocol.ts | 1 + .../workbench/api/common/extHostWorkspace.ts | 34 ++++-- src/vs/workbench/api/node/extHost.api.impl.ts | 8 +- .../api/node/extHostExtensionService.ts | 7 +- .../api/node/extHostTerminalService.ts | 82 +++++++------ .../parts/editor/noTabsTitleControl.ts | 13 +- .../browser/parts/editor/tabsTitleControl.ts | 6 + .../browser/parts/editor/titleControl.ts | 6 +- .../notifications/notificationsViewer.ts | 2 +- src/vs/workbench/common/notifications.ts | 20 +++- .../browser/commentsEditorContribution.ts | 1 + .../contrib/debug/common/debugModel.ts | 7 +- .../contrib/debug/common/debugSource.ts | 2 +- .../test/electron-browser/debugModel.test.ts | 16 +++ .../extensions.contribution.ts | 29 ----- .../electron-browser/extensionsActions.ts | 17 ++- .../electron-browser/extensionsList.ts | 3 +- .../electron-browser/extensionsViewlet.ts | 9 +- .../electron-browser/extensionsViews.ts | 7 +- .../contrib/files/browser/fileActions.ts | 113 +++++++----------- .../contrib/files/browser/fileCommands.ts | 24 +++- .../preferences/browser/settingsTreeModels.ts | 4 + .../browser/terminalProcessManager.ts | 49 ++------ .../terminal/common/terminalEnvironment.ts | 51 +++++++- .../electron-browser/extensionHost.ts | 4 +- .../node/multiExtensionManagement.ts | 7 +- .../services/files/common/fileService.ts | 35 ++++-- .../watcher/unix/chokidarWatcherService.ts | 13 +- .../files/test/node/diskFileService.test.ts | 6 +- .../services/label/common/labelService.ts | 36 +++--- .../common/textFileEditorModelManager.ts | 13 +- .../textfile/common/textFileService.ts | 4 +- .../services/textfile/node/textFileService.ts | 18 +-- .../textfile/test/textFileService.io.test.ts | 24 ++++ .../themes/browser/workbenchThemeService.ts | 10 +- .../{browser => common}/colorThemeData.ts | 5 +- .../{browser => common}/colorThemeStore.ts | 2 +- .../{browser => common}/themeCompatibility.ts | 0 .../themes/common/workbenchThemeService.ts | 4 + .../test/common/notifications.test.ts | 16 ++- .../workbench/test/workbenchTestServices.ts | 6 +- 63 files changed, 676 insertions(+), 413 deletions(-) rename src/vs/workbench/services/themes/{browser => common}/colorThemeData.ts (98%) rename src/vs/workbench/services/themes/{browser => common}/colorThemeStore.ts (98%) rename src/vs/workbench/services/themes/{browser => common}/themeCompatibility.ts (100%) diff --git a/resources/win32/bin/code.cmd b/resources/win32/bin/code.cmd index 4f31b47469..33c640f5dd 100644 --- a/resources/win32/bin/code.cmd +++ b/resources/win32/bin/code.cmd @@ -2,5 +2,5 @@ setlocal set VSCODE_DEV= set ELECTRON_RUN_AS_NODE=1 -call "%~dp0..\@@NAME@@.exe" "%~dp0..\resources\app\out\cli.js" %* +"%~dp0..\@@NAME@@.exe" "%~dp0..\resources\app\out\cli.js" %* endlocal \ No newline at end of file diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 68088b99c5..ae26c4c60c 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -18,15 +18,20 @@ export const UTF16be_BOM = [0xFE, 0xFF]; export const UTF16le_BOM = [0xFF, 0xFE]; export const UTF8_BOM = [0xEF, 0xBB, 0xBF]; +const ZERO_BYTE_DETECTION_BUFFER_MAX_LEN = 512; // number of bytes to look at to decide about a file being binary or not +const NO_GUESS_BUFFER_MAX_LEN = 512; // when not auto guessing the encoding, small number of bytes are enough +const AUTO_GUESS_BUFFER_MAX_LEN = 512 * 8; // with auto guessing we want a lot more content to be read for guessing + export interface IDecodeStreamOptions { - guessEncoding?: boolean; + guessEncoding: boolean; minBytesRequiredForDetection?: number; - overwriteEncoding?(detectedEncoding: string | null): string; + + overwriteEncoding(detectedEncoding: string | null): string; } export interface IDecodeStreamResult { - detected: IDetectedEncodingResult; stream: NodeJS.ReadableStream; + detected: IDetectedEncodingResult; } export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions): Promise { @@ -34,78 +39,82 @@ export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions options.minBytesRequiredForDetection = options.guessEncoding ? AUTO_GUESS_BUFFER_MAX_LEN : NO_GUESS_BUFFER_MAX_LEN; } - if (!options.overwriteEncoding) { - options.overwriteEncoding = detected => detected || UTF8; - } - return new Promise((resolve, reject) => { const writer = new class extends Writable { private decodeStream: NodeJS.ReadWriteStream; - private decodeStreamConstruction: Promise; - private buffer: Buffer[] = []; + private decodeStreamPromise: Promise; + + private bufferedChunks: Buffer[] = []; private bytesBuffered = 0; - _write(chunk: any, encoding: string, callback: Function): void { + _write(chunk: Buffer, encoding: string, callback: (error: Error | null) => void): void { if (!Buffer.isBuffer(chunk)) { - callback(new Error('data must be a buffer')); + return callback(new Error('toDecodeStream(): data must be a buffer')); } + // if the decode stream is ready, we just write directly if (this.decodeStream) { - this.decodeStream.write(chunk, callback); // just a forwarder now + this.decodeStream.write(chunk, callback); return; } - this.buffer.push(chunk); - this.bytesBuffered += chunk.length; + // otherwise we need to buffer the data until the stream is ready + this.bufferedChunks.push(chunk); + this.bytesBuffered += chunk.byteLength; // waiting for the decoder to be ready - if (this.decodeStreamConstruction) { - this.decodeStreamConstruction.then(() => callback(), err => callback(err)); + if (this.decodeStreamPromise) { + this.decodeStreamPromise.then(() => callback(null), error => callback(error)); } - // buffered enough data, create stream and forward data + // buffered enough data for encoding detection, create stream and forward data else if (typeof options.minBytesRequiredForDetection === 'number' && this.bytesBuffered >= options.minBytesRequiredForDetection) { this._startDecodeStream(callback); } - // only buffering + // only buffering until enough data for encoding detection is there else { - callback(); + callback(null); } } - _startDecodeStream(callback: Function): void { - this.decodeStreamConstruction = Promise.resolve(detectEncodingFromBuffer({ - buffer: Buffer.concat(this.buffer), + _startDecodeStream(callback: (error: Error | null) => void): void { + + // detect encoding from buffer + this.decodeStreamPromise = Promise.resolve(detectEncodingFromBuffer({ + buffer: Buffer.concat(this.bufferedChunks), bytesRead: this.bytesBuffered }, options.guessEncoding)).then(detected => { - if (options.overwriteEncoding) { - detected.encoding = options.overwriteEncoding(detected.encoding); - } + // ensure to respect overwrite of encoding + detected.encoding = options.overwriteEncoding(detected.encoding); + + // decode and write buffer this.decodeStream = decodeStream(detected.encoding); + this.decodeStream.write(Buffer.concat(this.bufferedChunks), callback); + this.bufferedChunks.length = 0; - for (const buffer of this.buffer) { - this.decodeStream.write(buffer); - } - - callback(); + // signal to the outside our detected encoding + // and final decoder stream resolve({ detected, stream: this.decodeStream }); - }, err => { - this.emit('error', err); - callback(err); + }, error => { + this.emit('error', error); + + callback(error); }); } - _final(callback: (err?: any) => any) { + _final(callback: (error: Error | null) => void) { // normal finish if (this.decodeStream) { this.decodeStream.end(callback); } - // we were still waiting for data... + // we were still waiting for data to do the encoding + // detection. thus, wrap up starting the stream even + // without all the data to get things going else { this._startDecodeStream(() => this.decodeStream.end(callback)); } @@ -149,7 +158,7 @@ function toNodeEncoding(enc: string | null): string { } export function detectEncodingByBOMFromBuffer(buffer: Buffer | VSBuffer | null, bytesRead: number): string | null { - if (!buffer || bytesRead < 2) { + if (!buffer || bytesRead < UTF16be_BOM.length) { return null; } @@ -166,7 +175,7 @@ export function detectEncodingByBOMFromBuffer(buffer: Buffer | VSBuffer | null, return UTF16le; } - if (bytesRead < 3) { + if (bytesRead < UTF8_BOM.length) { return null; } @@ -256,10 +265,6 @@ export function toCanonicalName(enc: string): string { } } -const ZERO_BYTE_DETECTION_BUFFER_MAX_LEN = 512; // number of bytes to look at to decide about a file being binary or not -const NO_GUESS_BUFFER_MAX_LEN = 512; // when not auto guessing the encoding, small number of bytes are enough -const AUTO_GUESS_BUFFER_MAX_LEN = 512 * 8; // with auto guessing we want a lot more content to be read for guessing - export interface IDetectedEncodingResult { encoding: string | null; seemsBinary: boolean; diff --git a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts index 916d4380a9..10bd10799f 100644 --- a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts +++ b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts @@ -16,7 +16,9 @@ export function registerContextMenuListener(): void { y: options ? options.y : undefined, positioningItem: options ? options.positioningItem : undefined, callback: () => { - event.sender.send(CONTEXT_MENU_CLOSE_CHANNEL, contextMenuId); + if (menu) { + event.sender.send(CONTEXT_MENU_CLOSE_CHANNEL, contextMenuId); + } } }); }); diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 681f356fe9..ca5de68c10 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -126,7 +126,8 @@ const enum ProtocolMessageType { Regular = 1, Control = 2, Ack = 3, - KeepAlive = 4 + KeepAlive = 4, + Disconnect = 5 } export const enum ProtocolConstants { @@ -373,6 +374,10 @@ export class Protocol extends Disposable implements IMessagePassingProtocol { return this._socket; } + sendDisconnect(): void { + // Nothing to do... + } + send(buffer: VSBuffer): void { this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.Regular, 0, 0, buffer)); } @@ -393,6 +398,7 @@ export class Client extends IPCClient { dispose(): void { super.dispose(); const socket = this.protocol.getSocket(); + this.protocol.sendDisconnect(); this.protocol.dispose(); socket.end(); } @@ -572,7 +578,6 @@ export class PersistentProtocol { this._socketDisposables.push(this._socketReader); this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg))); this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire())); - this._socketDisposables.push(this._socket.onEnd(() => this._onClose.fire())); if (initialChunk) { this._socketReader.acceptChunk(initialChunk); } @@ -601,6 +606,12 @@ export class PersistentProtocol { this._socketDisposables = dispose(this._socketDisposables); } + sendDisconnect(): void { + const msg = new ProtocolMessage(ProtocolMessageType.Disconnect, 0, 0, getEmptyBuffer()); + this._socketWriter.write(msg); + this._socketWriter.flush(); + } + private _sendKeepAliveCheck(): void { if (this._outgoingKeepAliveTimeout) { // there will be a check in the near future @@ -659,7 +670,6 @@ export class PersistentProtocol { this._socketDisposables.push(this._socketReader); this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg))); this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire())); - this._socketDisposables.push(this._socket.onEnd(() => this._onClose.fire())); this._socketReader.acceptChunk(initialDataChunk); } @@ -703,6 +713,8 @@ export class PersistentProtocol { } } else if (msg.type === ProtocolMessageType.Control) { this._onControlMessage.fire(msg.data); + } else if (msg.type === ProtocolMessageType.Disconnect) { + this._onClose.fire(); } } diff --git a/src/vs/base/test/node/encoding/encoding.test.ts b/src/vs/base/test/node/encoding/encoding.test.ts index da25d14d7d..a6c0399e3f 100644 --- a/src/vs/base/test/node/encoding/encoding.test.ts +++ b/src/vs/base/test/node/encoding/encoding.test.ts @@ -240,7 +240,7 @@ suite('Encoding', () => { } }); - let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 4 }); + let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -260,7 +260,7 @@ suite('Encoding', () => { } }); - let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 64 }); + let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -277,7 +277,7 @@ suite('Encoding', () => { } }); - let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 512 }); + let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 512, guessEncoding: false, overwriteEncoding: detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -292,7 +292,7 @@ suite('Encoding', () => { let path = getPathFromAmdModule(require, './fixtures/some_utf16be.css'); let source = fs.createReadStream(path); - let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 64 }); + let { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: detected => detected || encoding.UTF8 }); assert.equal(detected.encoding, 'utf16be'); assert.equal(detected.seemsBinary, false); @@ -307,7 +307,7 @@ suite('Encoding', () => { let path = getPathFromAmdModule(require, './fixtures/empty.txt'); let source = fs.createReadStream(path); - let { detected, stream } = await encoding.toDecodeStream(source, {}); + let { detected, stream } = await encoding.toDecodeStream(source, { guessEncoding: false, overwriteEncoding: detected => detected || encoding.UTF8 }); let expected = await readAndDecodeFromDisk(path, detected.encoding); let actual = await readAllAsString(stream); diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 9beb31ab1a..c3d909e1bc 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -40,7 +40,7 @@ import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { normalizeGitHubUrl } from 'vs/code/electron-browser/issue/issueReporterUtil'; import { Button } from 'vs/base/browser/ui/button/button'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { SystemInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; const MAX_URL_LENGTH = platform.isWindows ? 2081 : 5400; @@ -940,15 +940,24 @@ export class IssueReporter extends Disposable { `; systemInfo.remoteData.forEach(remote => { - renderedData += ` -
- - - - - - -
Remote${remote.hostName}
OS${remote.machineInfo.os}
CPUs${remote.machineInfo.cpus}
Memory (System)${remote.machineInfo.memory}
VM${remote.machineInfo.vmHint}
`; + if (isRemoteDiagnosticError(remote)) { + renderedData += ` +
+ + + +
Remote${remote.hostName}
${remote.errorMessage}
`; + } else { + renderedData += ` +
+ + + + + + +
Remote${remote.hostName}
OS${remote.machineInfo.os}
CPUs${remote.machineInfo.cpus}
Memory (System)${remote.machineInfo.memory}
VM${remote.machineInfo.vmHint}
`; + } }); target.innerHTML = renderedData; diff --git a/src/vs/code/electron-browser/issue/issueReporterModel.ts b/src/vs/code/electron-browser/issue/issueReporterModel.ts index 4758ebbed4..82be5aa542 100644 --- a/src/vs/code/electron-browser/issue/issueReporterModel.ts +++ b/src/vs/code/electron-browser/issue/issueReporterModel.ts @@ -5,7 +5,7 @@ import { assign } from 'vs/base/common/objects'; import { IssueType, ISettingSearchResult, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; -import { SystemInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; export interface IssueReporterData { issueType: IssueType; @@ -76,7 +76,8 @@ ${this.getInfos()} private getRemoteOSes(): string { if (this._data.systemInfo && this._data.systemInfo.remoteData.length) { - return this._data.systemInfo.remoteData.map(remote => `Remote OS version: ${remote.machineInfo.os}`).join('\n') + '\n'; + return this._data.systemInfo.remoteData + .map(remote => isRemoteDiagnosticError(remote) ? remote.errorMessage : `Remote OS version: ${remote.machineInfo.os}`).join('\n') + '\n'; } return ''; @@ -169,7 +170,10 @@ ${this.getInfos()} |VM|${this._data.systemInfo.vmHint}|`; this._data.systemInfo.remoteData.forEach(remote => { - md += ` + if (isRemoteDiagnosticError(remote)) { + md += `\n\n${remote.errorMessage}`; + } else { + md += ` |Item|Value| |---|---| @@ -178,6 +182,7 @@ ${this.getInfos()} |CPUs|${remote.machineInfo.cpus}| |Memory (System)|${remote.machineInfo.memory}| |VM|${remote.machineInfo.vmHint}|`; + } }); } diff --git a/src/vs/code/electron-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index d51e9b21f9..94ca17281c 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -3,9 +3,7 @@ - + diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 0489dc8f9e..dd75ecd411 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -1190,46 +1190,62 @@ export class WindowsManager implements IWindowsMainService { } } - // Make sure we are not asked to open a workspace or folder that is already opened - if (cliArgs.length && cliArgs.some(path => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, URI.file(path)))) { - cliArgs = []; + if (!Array.isArray(extensionDevelopmentPath)) { + extensionDevelopmentPath = [extensionDevelopmentPath]; } - if (folderUris.length && folderUris.some(uri => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, this.argToUri(uri)))) { - folderUris = []; + let authority = ''; + for (let p of extensionDevelopmentPath) { + if (p.match(/^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/)) { + const url = URI.parse(p); + if (url.scheme === Schemas.vscodeRemote) { + if (authority) { + if (url.authority !== authority) { + this.logService.error('more than one extension development path authority'); + } + } else { + authority = url.authority; + } + } + } } - if (fileUris.length && fileUris.some(uri => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, this.argToUri(uri)))) { - fileUris = []; - } + // Make sure that we do not try to open: + // - a workspace or folder that is already opened + // - a workspace or file that has a different authority as the extension development. + + cliArgs = cliArgs.filter(path => { + const uri = URI.file(path); + if (!!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, uri)) { + return false; + } + return uri.authority === authority; + }); + + folderUris = folderUris.filter(uri => { + const u = this.argToUri(uri); + if (!!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, u)) { + return false; + } + return u ? u.authority === authority : false; + }); + + fileUris = fileUris.filter(uri => { + const u = this.argToUri(uri); + if (!!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, u)) { + return false; + } + return u ? u.authority === authority : false; + }); openConfig.cli._ = cliArgs; openConfig.cli['folder-uri'] = folderUris; openConfig.cli['file-uri'] = fileUris; - if (Array.isArray(extensionDevelopmentPath)) { - let authority: string | undefined = undefined; - for (let p of extensionDevelopmentPath) { - const match = p.match(/^vscode-remote:\/\/([^\/]+)/); - if (match) { - const auth = URI.parse(p).authority; - if (authority) { - if (auth !== authority) { - console.log('more than one authority'); - } - } else { - authority = auth; - } - } - } + // if there are no files or folders cli args left, use the "remote" cli argument + if (!cliArgs.length && !folderUris.length && !fileUris.length) { if (authority) { - openConfig.cli['remote'] = authority; - } - - } else { - const match = extensionDevelopmentPath.match(/^vscode-remote:\/\/([^\/]+)/); - if (match) { - openConfig.cli['remote'] = URI.parse(extensionDevelopmentPath).authority; + openConfig.cli.remote = authority; } } @@ -1581,7 +1597,8 @@ export class WindowsManager implements IWindowsMainService { cli = { ...cli, remote }; } const forceReuseWindow = options && options.reuseWindow; - return this.open({ context, cli, forceEmpty: true, forceReuseWindow }); + const forceNewWindow = !forceReuseWindow; + return this.open({ context, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); } openNewTabbedWindow(context: OpenContext): ICodeWindow[] { diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index e81c07c766..b69abee0ba 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -55,7 +55,7 @@ export class OpenerService implements IOpenerService { if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https) || equalsIgnoreCase(scheme, Schemas.mailto)) { // open http or default mail application - dom.windowOpenNoOpener(resource.toString(true)); + dom.windowOpenNoOpener(encodeURI(resource.toString(true))); return Promise.resolve(true); } else if (equalsIgnoreCase(scheme, Schemas.command)) { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index ac7034a626..4e49af620f 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1288,6 +1288,7 @@ export interface CommentThread2 { onDidChangeRange: Event; onDidChangeLabel: Event; onDidChangeCollasibleState: Event; + isDisposed: boolean; } /** @@ -1312,6 +1313,7 @@ export interface CommentThread { comments: Comment[] | undefined; collapsibleState?: CommentThreadCollapsibleState; reply?: Command; + isDisposed?: boolean; } /** diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index 6e6c79c717..2c729d1dc6 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -399,7 +399,7 @@ export abstract class BaseEditorSimpleWorker { // ---- BEGIN minimal edits --------------------------------------------------------------- - private static readonly _diffLimit = 10000; + private static readonly _diffLimit = 100000; public computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[]): Promise { const model = this._getModel(modelUrl); @@ -432,7 +432,7 @@ export abstract class BaseEditorSimpleWorker { } const original = model.getValueInRange(range); - text = text!.replace(/\r\n|\n|\r/g, model.eol); + text = text.replace(/\r\n|\n|\r/g, model.eol); if (original === text) { // noop diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index 21402c1481..9e945bdc10 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -21,6 +21,8 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { regExpFlags } from 'vs/base/common/strings'; import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { ILogService } from 'vs/platform/log/common/log'; +import { StopWatch } from 'vs/base/common/stopwatch'; /** * Stop syncing a model to the worker if it was not needed for 1 min. @@ -48,14 +50,16 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker private readonly _modelService: IModelService; private readonly _workerManager: WorkerManager; - + private readonly _logService: ILogService; constructor( @IModelService modelService: IModelService, - @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService + @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, + @ILogService logService: ILogService ) { super(); this._modelService = modelService; this._workerManager = this._register(new WorkerManager(this._modelService)); + this._logService = logService; // todo@joh make sure this happens only once this._register(modes.LinkProviderRegistry.register('*', { @@ -96,7 +100,10 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker if (!canSyncModel(this._modelService, resource)) { return Promise.resolve(edits); // File too large } - return this._workerManager.withWorker().then(client => client.computeMoreMinimalEdits(resource, edits)); + const sw = StopWatch.create(true); + const result = this._workerManager.withWorker().then(client => client.computeMoreMinimalEdits(resource, edits)); + result.finally(() => this._logService.trace('FORMAT#computeMoreMinimalEdits', resource.toString(true), sw.elapsed())); + return result; } else { return Promise.resolve(undefined); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 1846a058de..31f52428c6 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -145,8 +145,6 @@ export module StaticServices { export const markerDecorationsService = define(IMarkerDecorationsService, (o) => new MarkerDecorationsService(modelService.get(o), markerService.get(o))); - export const editorWorkerService = define(IEditorWorkerService, (o) => new EditorWorkerServiceImpl(modelService.get(o), resourceConfigurationService.get(o))); - export const standaloneThemeService = define(IStandaloneThemeService, () => new StandaloneThemeServiceImpl()); export const codeEditorService = define(ICodeEditorService, (o) => new StandaloneCodeEditorServiceImpl(standaloneThemeService.get(o))); @@ -157,6 +155,8 @@ export module StaticServices { export const logService = define(ILogService, () => new NullLogService()); + export const editorWorkerService = define(IEditorWorkerService, (o) => new EditorWorkerServiceImpl(modelService.get(o), resourceConfigurationService.get(o), logService.get(o))); + export const suggestMemoryService = define(ISuggestMemoryService, (o) => new SuggestMemoryService(storageService.get(o), configurationService.get(o))); } diff --git a/src/vs/platform/diagnostics/common/diagnosticsService.ts b/src/vs/platform/diagnostics/common/diagnosticsService.ts index d8d2ed7042..21cd8952e1 100644 --- a/src/vs/platform/diagnostics/common/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/common/diagnosticsService.ts @@ -22,7 +22,7 @@ export interface SystemInfo extends IMachineInfo { processArgs: string; gpuStatus: any; screenReader: string; - remoteData: IRemoteDiagnosticInfo[]; + remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]; load?: string; } diff --git a/src/vs/platform/extensions/node/extensionsUtil.ts b/src/vs/platform/extensions/node/extensionsUtil.ts index b25aa663c8..9a6bbfbf4f 100644 --- a/src/vs/platform/extensions/node/extensionsUtil.ts +++ b/src/vs/platform/extensions/node/extensionsUtil.ts @@ -11,14 +11,8 @@ import product from 'vs/platform/product/node/product'; export function isUIExtension(manifest: IExtensionManifest, uiContributions: string[], configurationService: IConfigurationService): boolean { const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); - const { ui, workspace } = configurationService.getValue<{ ui: string[], workspace: string[] }>('extensions.extensionKind') || { ui: [], workspace: [] }; - if (isNonEmptyArray(workspace) && workspace.some(id => areSameExtensions({ id }, { id: extensionId }))) { - return false; - } - if (isNonEmptyArray(ui) && ui.some(id => areSameExtensions({ id }, { id: extensionId }))) { - return true; - } - switch (manifest.extensionKind) { + const extensionKind = getExtensionKind(manifest, configurationService); + switch (extensionKind) { case 'ui': return true; case 'workspace': return false; default: { @@ -38,3 +32,14 @@ export function isUIExtension(manifest: IExtensionManifest, uiContributions: str } } } + +function getExtensionKind(manifest: IExtensionManifest, configurationService: IConfigurationService): string | undefined { + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + const configuredExtensionKinds = configurationService.getValue<{ [key: string]: string }>('remote.extensionKind') || {}; + for (const id of Object.keys(configuredExtensionKinds)) { + if (areSameExtensions({ id: extensionId }, { id })) { + return configuredExtensionKinds[id]; + } + } + return manifest.extensionKind; +} diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/node/commonProperties.ts index e7aa7b8af2..5809b38d9a 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/node/commonProperties.ts @@ -34,8 +34,9 @@ export function resolveCommonProperties(commit: string | undefined, version: str result['common.nodePlatform'] = process.platform; // __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.nodeArch'] = process.arch; - + // __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } // {{SQL CARBON EDIT}} + result['common.product'] = product.nameShort || 'desktop'; result['common.application.name'] = product.nameLong; // dynamic properties which value differs on each call diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 602e030ea5..e0ee7c7410 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1371,4 +1371,43 @@ declare module 'vscode' { group?: string; } //#endregion + + //#region Workspace URI Ben + + export namespace workspace { + + /** + * The location of the workspace file, for example: + * + * `file:///Users/name/Development/myProject.code-workspace` + * + * or + * + * `untitled:1555503116870` + * + * for a workspace that is untitled and not yet saved. + * + * Depending on the workspace that is opened, the value will be: + * * `undefined` when no workspace or a single folder is opened + * * the path of the workspace file as `Uri` otherwise. if the workspace + * is untitled, the returned URI will use the `untitled:` scheme + * + * The location can e.g. be used with the `vscode.openFolder` command to + * open the workspace again after it has been closed. + * + * **Example:** + * ```typescript + * vscode.commands.executeCommand('vscode.openFolder', uriOfWorkspace); + * ``` + * + * **Note:** it is not advised to use `workspace.workspaceFile` to write + * configuration data into the file. You can use `workspace.getConfiguration().update()` + * for that purpose which will work both when a single folder is opened as + * well as an untitled or saved workspace. + */ + export const workspaceFile: Uri | undefined; + } + + //#endregion + } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 3a6de3bf26..adbc91086b 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -184,6 +184,12 @@ export class MainThreadCommentThread implements modes.CommentThread2 { private _onDidChangeCollasibleState = new Emitter(); public onDidChangeCollasibleState = this._onDidChangeCollasibleState.event; + private _isDisposed: boolean; + + get isDisposed(): boolean { + return this._isDisposed; + } + constructor( public commentThreadHandle: number, public controller: MainThreadCommentController, @@ -191,7 +197,9 @@ export class MainThreadCommentThread implements modes.CommentThread2 { public threadId: string, public resource: string, private _range: IRange - ) { } + ) { + this._isDisposed = false; + } batchUpdate( range: IRange, @@ -210,7 +218,16 @@ export class MainThreadCommentThread implements modes.CommentThread2 { this._collapsibleState = collapsibleState; } - dispose() { } + dispose() { + this._isDisposed = true; + this._onDidChangeAcceptInputCommand.dispose(); + this._onDidChangeAdditionalCommands.dispose(); + this._onDidChangeCollasibleState.dispose(); + this._onDidChangeComments.dispose(); + this._onDidChangeInput.dispose(); + this._onDidChangeLabel.dispose(); + this._onDidChangeRange.dispose(); + } toJSON(): any { return { @@ -493,6 +510,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } + console.log('createCommentThread', commentThreadHandle); + return provider.createCommentThread(commentThreadHandle, threadId, resource, range); } diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 4999852c13..94aea4fa74 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -46,23 +46,20 @@ export class MainThreadWindow implements MainThreadWindowShape { } async $openUri(uriComponent: UriComponents, options: IOpenUriOptions): Promise { - const uri = URI.revive(uriComponent); + let uri = URI.revive(uriComponent); if (options.allowTunneling && !!this.environmentService.configuration.remoteAuthority) { if (uri.scheme === 'http' || uri.scheme === 'https') { const port = this.getLocalhostPort(uri); if (typeof port === 'number') { const tunnel = await this.getOrCreateTunnel(port); if (tunnel) { - const tunneledUrl = uri.toString().replace( - new RegExp(`^${uri.scheme}://localhost:${port}/`), - `${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`); - return this.windowsService.openExternal(tunneledUrl); + uri = uri.with({ authority: `localhost:${tunnel.tunnelLocalPort}` }); } } } } - return this.windowsService.openExternal(uri.toString(true)); + return this.windowsService.openExternal(encodeURI(uri.toString(true))); } private getLocalhostPort(uri: URI): number | undefined { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 3fed376eb1..4f4b7e64d0 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -22,6 +22,8 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { isEqualOrParent } from 'vs/base/common/resources'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -40,7 +42,8 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @IStatusbarService private readonly _statusbarService: IStatusbarService, @IWindowService private readonly _windowService: IWindowService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILabelService private readonly _labelService: ILabelService + @ILabelService private readonly _labelService: ILabelService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); this._contextService.getCompleteWorkspace().then(workspace => this._proxy.$initializeWorkspace(this.getWorkspaceData(workspace))); @@ -110,6 +113,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } return { configuration: workspace.configuration || undefined, + isUntitled: workspace.configuration ? isEqualOrParent(workspace.configuration, this._environmentService.untitledWorkspacesHome) : false, folders: workspace.folders, id: workspace.id, name: this._labelService.getWorkspaceLabel(workspace) diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 3634546802..3083ad95a3 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -15,6 +15,7 @@ import { IWindowsService, IOpenSettings, IURIToOpen } from 'vs/platform/windows/ import { IDownloadService } from 'vs/platform/download/common/download'; import { IWorkspacesService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { IRecent } from 'vs/platform/history/common/history'; +import { Schemas } from 'vs/base/common/network'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -51,7 +52,7 @@ export class OpenFolderAPICommand { } const options: IOpenSettings = { forceNewWindow: arg.forceNewWindow, noRecentEntry: arg.noRecentEntry }; uri = URI.revive(uri); - const uriToOpen: IURIToOpen = hasWorkspaceFileExtension(uri.path) ? { workspaceUri: uri } : { folderUri: uri }; + const uriToOpen: IURIToOpen = (hasWorkspaceFileExtension(uri.path) || uri.scheme === Schemas.untitled) ? { workspaceUri: uri } : { folderUri: uri }; return executor.executeCommand('_files.windowOpen', [uriToOpen], options); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c082aefde3..d6d977c1c0 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -68,6 +68,7 @@ export interface IStaticWorkspaceData { id: string; name: string; configuration?: UriComponents | null; + isUntitled?: boolean | null; } export interface IWorkspaceData extends IStaticWorkspaceData { diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 7dfdb861d9..2b9e31e7cf 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { TernarySearchTree } from 'vs/base/common/map'; import { Counter } from 'vs/base/common/numbers'; import { isLinux } from 'vs/base/common/platform'; -import { basenameOrAuthority, dirname, isEqual, relativePath } from 'vs/base/common/resources'; +import { basenameOrAuthority, dirname, isEqual, relativePath, basename } from 'vs/base/common/resources'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -24,6 +24,7 @@ import * as vscode from 'vscode'; import { ExtHostWorkspaceShape, IWorkspaceData, MainThreadMessageServiceShape, MainThreadWorkspaceShape, IMainContext, MainContext, IStaticWorkspaceData } from './extHost.protocol'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Barrier } from 'vs/base/common/async'; +import { Schemas } from 'vs/base/common/network'; export interface IExtHostWorkspaceProvider { getWorkspaceFolder2(uri: vscode.Uri, resolveParent?: boolean): Promise; @@ -67,7 +68,7 @@ class ExtHostWorkspaceImpl extends Workspace { return { workspace: null, added: [], removed: [] }; } - const { id, name, folders } = data; + const { id, name, folders, configuration, isUntitled } = data; const newWorkspaceFolders: vscode.WorkspaceFolder[] = []; // If we have an existing workspace, we try to find the folders that match our @@ -95,7 +96,7 @@ class ExtHostWorkspaceImpl extends Workspace { // make sure to restore sort order based on index newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1); - const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders); + const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders, configuration ? URI.revive(configuration) : null, !!isUntitled); const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri); return { workspace, added, removed }; @@ -115,8 +116,8 @@ class ExtHostWorkspaceImpl extends Workspace { private readonly _workspaceFolders: vscode.WorkspaceFolder[] = []; private readonly _structure = TernarySearchTree.forPaths(); - constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[]) { - super(id, folders.map(f => new WorkspaceFolder(f))); + constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[], configuration: URI | null, private _isUntitled: boolean) { + super(id, folders.map(f => new WorkspaceFolder(f)), configuration); // setup the workspace folder data structure folders.forEach(folder => { @@ -129,6 +130,10 @@ class ExtHostWorkspaceImpl extends Workspace { return this._name; } + get isUntitled(): boolean { + return this._isUntitled; + } + get workspaceFolders(): vscode.WorkspaceFolder[] { return this._workspaceFolders.slice(0); } @@ -175,7 +180,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac this._proxy = mainContext.getProxy(MainContext.MainThreadWorkspace); this._messageService = mainContext.getProxy(MainContext.MainThreadMessageService); - this._confirmedWorkspace = data ? new ExtHostWorkspaceImpl(data.id, data.name, []) : undefined; + this._confirmedWorkspace = data ? new ExtHostWorkspaceImpl(data.id, data.name, [], data.configuration ? URI.revive(data.configuration) : null, !!data.isUntitled) : undefined; } $initializeWorkspace(data: IWorkspaceData): void { @@ -197,6 +202,20 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return this._actualWorkspace ? this._actualWorkspace.name : undefined; } + get workspaceFile(): vscode.Uri | undefined { + if (this._actualWorkspace) { + if (this._actualWorkspace.configuration) { + if (this._actualWorkspace.isUntitled) { + return URI.from({ scheme: Schemas.untitled, path: basename(dirname(this._actualWorkspace.configuration)) }); // Untitled Worspace: return untitled URI + } + + return this._actualWorkspace.configuration; // Workspace: return the configuration location + } + } + + return undefined; + } + private get _actualWorkspace(): ExtHostWorkspaceImpl | undefined { return this._unconfirmedWorkspace || this._confirmedWorkspace; } @@ -365,7 +384,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac id: this._actualWorkspace.id, name: this._actualWorkspace.name, configuration: this._actualWorkspace.configuration, - folders + folders, + isUntitled: this._actualWorkspace.isUntitled } as IWorkspaceData, this._actualWorkspace).workspace || undefined; } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index cdfb064790..6eaf6b5df6 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -116,7 +116,7 @@ export function createApiFactory( const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpcProtocol, extHostDocumentsAndEditors)); const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands)); - const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol, extHostConfiguration, extHostLogService)); + const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol, extHostConfiguration, extHostWorkspace, extHostDocumentsAndEditors, extHostLogService)); // {{SQL CARBON EDIT}} // const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService, extHostDocumentsAndEditors, extHostConfiguration, extHostTerminalService, extHostCommands)); const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostLogService)); @@ -539,6 +539,12 @@ export function createApiFactory( set name(value) { throw errors.readonly(); }, + get workspaceFile() { + return extHostWorkspace.workspaceFile; + }, + set workspaceFile(value) { + throw errors.readonly(); + }, updateWorkspaceFolders: (index, deleteCount, ...workspaceFoldersToAdd) => { return extHostWorkspace.updateWorkspaceFolders(extension, index, deleteCount || 0, ...workspaceFoldersToAdd); }, diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 0467d34002..15d6909c19 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -65,6 +65,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { private readonly _mainThreadExtensionsProxy: MainThreadExtensionServiceShape; private readonly _almostReadyToRunExtensions: Barrier; + private readonly _readyToStartExtensionHost: Barrier; private readonly _readyToRunExtensions: Barrier; private readonly _registry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; @@ -101,6 +102,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { this._mainThreadExtensionsProxy = this._extHostContext.getProxy(MainContext.MainThreadExtensionService); this._almostReadyToRunExtensions = new Barrier(); + this._readyToStartExtensionHost = new Barrier(); this._readyToRunExtensions = new Barrier(); this._registry = new ExtensionDescriptionRegistry(initData.extensions); this._storage = new ExtHostStorage(this._extHostContext); @@ -171,7 +173,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { this._almostReadyToRunExtensions.open(); await this._extHostWorkspace.waitForInitializeCall(); - this._readyToRunExtensions.open(); + this._readyToStartExtensionHost.open(); } catch (err) { errors.onUnexpectedError(err); } @@ -581,7 +583,8 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { } this._started = true; - return this._readyToRunExtensions.wait() + return this._readyToStartExtensionHost.wait() + .then(() => this._readyToRunExtensions.open()) .then(() => this._handleEagerExtensions()) .then(() => this._handleExtensionTests()) .then(() => { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 2d11f98aaa..8fedfa57b4 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -13,10 +13,14 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext, ShellLaunchConfigDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ILogService } from 'vs/platform/log/common/log'; -import { EXT_HOST_CREATION_DELAY, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal'; +import { EXT_HOST_CREATION_DELAY, IShellLaunchConfig, ITerminalEnvironment } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; import { timeout } from 'vs/base/common/async'; -import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +// {{SQL CARBON EDIT}} +// import { ExtHostVariableResolverService } from 'vs/workbench/api/node/extHostDebugService'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; const RENDERER_NO_PROCESS_ID = -1; @@ -288,6 +292,8 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { constructor( mainContext: IMainContext, private _extHostConfiguration: ExtHostConfiguration, + private _extHostWorkspace: ExtHostWorkspace, + private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, private _logService: ILogService, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadTerminalService); @@ -436,6 +442,16 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } } + private _apiInspectConfigToPlain( + config: { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T, workspaceFolderValue?: T } | undefined + ): { user: T | undefined, value: T | undefined, default: T | undefined } { + return { + user: config ? config.globalValue : undefined, + value: config ? config.workspaceValue : undefined, + default: config ? config.defaultValue : undefined, + }; + } + public async $createProcess(id: number, shellLaunchConfigDto: ShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { const shellLaunchConfig: IShellLaunchConfig = { name: shellLaunchConfigDto.name, @@ -447,57 +463,54 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // Merge in shell and args from settings const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + const configProvider = await this._extHostConfiguration.getConfigProvider(); if (!shellLaunchConfig.executable) { const fetchSetting = (key: string) => { const setting = configProvider .getConfiguration(key.substr(0, key.lastIndexOf('.'))) .inspect(key.substr(key.lastIndexOf('.') + 1)); - return { - user: setting ? setting.globalValue : undefined, - value: setting ? setting.workspaceValue : undefined, - default: setting ? setting.defaultValue : undefined, - }; + return this._apiInspectConfigToPlain(setting); }; terminalEnvironment.mergeDefaultShellPathAndArgs(shellLaunchConfig, fetchSetting, isWorkspaceShellAllowed || false); } // Get the initial cwd - const configProvider = await this._extHostConfiguration.getConfigProvider(); const terminalConfig = configProvider.getConfiguration('terminal.integrated'); const activeWorkspaceRootUri = URI.revive(activeWorkspaceRootUriComponents); const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), activeWorkspaceRootUri, terminalConfig.cwd); - // TODO: Pull in and resolve config settings - // // Resolve env vars from config and shell - // const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); - // const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...terminalConfig.env[platformKey] }, lastActiveWorkspaceRoot); - const envFromConfig = { ...terminalConfig.env[platformKey] }; - // const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); - - // Merge process env with the env from config - const env = { ...process.env }; - Object.keys(env).filter(k => env[k] === undefined).forEach(k => { - delete env[k]; - }); - const castedEnv = env as platform.IProcessEnvironment; - terminalEnvironment.mergeEnvironments(castedEnv, envFromConfig); - terminalEnvironment.mergeEnvironments(castedEnv, shellLaunchConfig.env); - - // Sanitize the environment, removing any undesirable VS Code and Electron environment - // variables - sanitizeProcessEnvironment(castedEnv, 'VSCODE_IPC_HOOK_CLI'); - - // Continue env initialization, merging in the env from the launch - // config and adding keys that are needed to create the process - terminalEnvironment.addTerminalEnvironmentKeys(castedEnv, pkg.version, platform.locale, terminalConfig.get('setLocaleVariables') as boolean); + // Get the environment + const apiLastActiveWorkspace = await this._extHostWorkspace.getWorkspaceFolder(activeWorkspaceRootUri); + const lastActiveWorkspace = apiLastActiveWorkspace ? { + uri: apiLastActiveWorkspace.uri, + name: apiLastActiveWorkspace.name, + index: apiLastActiveWorkspace.index, + toResource: () => { + throw new Error('Not implemented'); + } + } as IWorkspaceFolder : null; + const envFromConfig = this._apiInspectConfigToPlain(configProvider.getConfiguration('terminal.integrated').inspect(`env.${platformKey}`)); + const workspaceFolders = await this._extHostWorkspace.getWorkspaceFolders2(); + // {{SQL CARBON EDIT}} + // const variableResolver = workspaceFolders ? new ExtHostVariableResolverService(workspaceFolders, this._extHostDocumentsAndEditors, configProvider) : undefined; + const variableResolver = undefined; + const env = terminalEnvironment.createTerminalEnvironment( + shellLaunchConfig, + lastActiveWorkspace, + envFromConfig, + variableResolver, + isWorkspaceShellAllowed, + pkg.version, + terminalConfig.get('setLocaleVariables', false) + ); // Fork the process and listen for messages - this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, castedEnv); - const p = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, castedEnv, terminalConfig.get('windowsEnableConpty') as boolean); + this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env); + const p = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, terminalConfig.get('windowsEnableConpty') as boolean); p.onProcessIdReady(pid => this._proxy.$sendProcessPid(id, pid)); p.onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); p.onProcessData(data => this._proxy.$sendProcessData(id, data)); - p.onProcessExit((exitCode) => this._onProcessExit(id, exitCode)); + p.onProcessExit(exitCode => this._onProcessExit(id, exitCode)); this._terminalProcesses[id] = p; } @@ -541,7 +554,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // Send exit event to main side this._proxy.$sendProcessExit(id, exitCode); - } private _getTerminalByIdEventually(id: number, retries: number = 5): Promise { diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 8659c8f403..2d81485130 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -145,12 +145,13 @@ export class NoTabsTitleControl extends TitleControl { this.redraw(); } - updateEditorLabel(editor?: IEditorInput): void { - if (!editor) { - editor = withNullAsUndefined(this.group.activeEditor); - } - if (editor) { - this.ifEditorIsActive(editor, () => this.redraw()); + updateEditorLabel(editor: IEditorInput): void { + this.ifEditorIsActive(editor, () => this.redraw()); + } + + updateEditorLabels(): void { + if (this.group.activeEditor) { + this.updateEditorLabel(this.group.activeEditor); // we only have the active one to update } } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 1270a99060..12cce49aa3 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -371,6 +371,12 @@ export class TabsTitleControl extends TitleControl { updateEditorLabel(editor: IEditorInput): void { + // Update all labels to account for changes to tab labels + this.updateEditorLabels(); + } + + updateEditorLabels(): void { + // A change to a label requires to recompute all labels this.computeTabLabels(); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 81cd309f6f..55f67b8cc8 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -93,7 +93,7 @@ export abstract class TitleControl extends Themable { private registerListeners(): void { this._register(this.extensionService.onDidRegisterExtensions(() => this.updateEditorActionsToolbar())); - this._register(this.labelService.onDidChangeFormatters(() => this.updateEditorLabel())); + this._register(this.labelService.onDidChangeFormatters(() => this.updateEditorLabels())); } protected abstract create(parent: HTMLElement): void; @@ -343,7 +343,9 @@ export abstract class TitleControl extends Themable { abstract setActive(isActive: boolean): void; - abstract updateEditorLabel(editor?: IEditorInput): void; + abstract updateEditorLabel(editor: IEditorInput): void; + + abstract updateEditorLabels(): void; abstract updateEditorDirty(editor: IEditorInput): void; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index b1fe710959..c3c5d81248 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -153,7 +153,7 @@ class NotificationMessageRenderer { const anchor = document.createElement('a'); anchor.textContent = link.name; - anchor.title = link.href; + anchor.title = link.title; anchor.href = link.href; if (actionHandler) { diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 070040bf4e..2ee1ceefff 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -10,6 +10,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Action } from 'vs/base/common/actions'; import { isErrorWithActions } from 'vs/base/common/errorsWithActions'; +import { startsWith } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; export interface INotificationsModel { @@ -306,8 +308,9 @@ export class NotificationViewItemProgress extends Disposable implements INotific } export interface IMessageLink { - name: string; href: string; + name: string; + title: string; offset: number; length: number; } @@ -325,7 +328,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie // Example link: "Some message with [link text](http://link.href)." // RegEx: [, anything not ], ], (, http://|https://|command:, no whitespace) - private static LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)\)/gi; + private static LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: "([^"]+)")?\)/gi; private _expanded: boolean; @@ -392,8 +395,17 @@ export class NotificationViewItem extends Disposable implements INotificationVie // Parse Links const links: IMessageLink[] = []; - message.replace(NotificationViewItem.LINK_REGEX, (matchString: string, name: string, href: string, offset: number) => { - links.push({ name, href, offset, length: matchString.length }); + message.replace(NotificationViewItem.LINK_REGEX, (matchString: string, name: string, href: string, title: string, offset: number) => { + let massagedTitle: string; + if (title && title.length > 0) { + massagedTitle = title; + } else if (startsWith(href, 'command:')) { + massagedTitle = localize('executeCommand', "Click to execute command '{0}'", href.substr('command:'.length)); + } else { + massagedTitle = href; + } + + links.push({ name, href, title: massagedTitle, offset, length: matchString.length }); return matchString; }); diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index c0c3cb2ac4..6393220adb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -738,6 +738,7 @@ export class ReviewController implements IEditorContribution { this._commentInfos.forEach(info => { let providerCacheStore = this._pendingCommentCache[info.owner]; + info.threads = info.threads.filter(thread => !thread.isDisposed); info.threads.forEach(thread => { let pendingComment: string | null = null; if (providerCacheStore) { diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index bd9997c144..507cc0682a 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -18,7 +18,7 @@ import { ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IDebugModel, IReplElementSource, IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IReplElement, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { Source, UNKNOWN_SOURCE_LABEL } from 'vs/workbench/contrib/debug/common/debugSource'; import { commonSuffixLength } from 'vs/base/common/strings'; import { posix } from 'vs/base/common/path'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -381,7 +381,10 @@ export class StackFrame implements IStackFrame { } toString(): string { - return `${this.name} (${this.source.inMemory ? this.source.name : this.source.uri.fsPath}:${this.range.startLineNumber})`; + const lineNumberToString = typeof this.range.startLineNumber === 'number' ? `:${this.range.startLineNumber}` : ''; + const sourceToString = `${this.source.inMemory ? this.source.name : this.source.uri.fsPath}${lineNumberToString}`; + + return sourceToString === UNKNOWN_SOURCE_LABEL ? this.name : `${this.name} (${sourceToString})`; } openInEditor(editorService: IEditorService, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { diff --git a/src/vs/workbench/contrib/debug/common/debugSource.ts b/src/vs/workbench/contrib/debug/common/debugSource.ts index e0b20b0cae..bf53f2ca90 100644 --- a/src/vs/workbench/contrib/debug/common/debugSource.ts +++ b/src/vs/workbench/contrib/debug/common/debugSource.ts @@ -13,7 +13,7 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/ import { Schemas } from 'vs/base/common/network'; import { isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; -const UNKNOWN_SOURCE_LABEL = nls.localize('unknownSource', "Unknown Source"); +export const UNKNOWN_SOURCE_LABEL = nls.localize('unknownSource', "Unknown Source"); /** * Debug URI format diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts index cfb4e2b257..18e5259b05 100644 --- a/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts @@ -394,6 +394,22 @@ suite('Debug - Model', () => { assert.equal(secondStackFrame.getSpecificSourceName(), '.../x/c/d/internalModule.js'); }); + test('stack frame toString()', () => { + const session = createMockSession(model); + const thread = new Thread(session, 'mockthread', 1); + const firstSource = new Source({ + name: 'internalModule.js', + path: 'a/b/c/d/internalModule.js', + sourceReference: 10, + }, 'aDebugSessionId'); + const stackFrame = new StackFrame(thread, 1, firstSource, 'app', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); + assert.equal(stackFrame.toString(), 'app (internalModule.js:1)'); + + const secondSource = new Source(undefined, 'aDebugSessionId'); + const stackFrame2 = new StackFrame(thread, 2, secondSource, 'module', 'normal', { startLineNumber: undefined!, startColumn: undefined!, endLineNumber: undefined!, endColumn: undefined! }, 2); + assert.equal(stackFrame2.toString(), 'module'); + }); + test('debug child sessions are added in correct order', () => { const session = createMockSession(model); model.addSession(session); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index b3b072714a..19b1f6890b 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -252,35 +252,6 @@ Registry.as(ConfigurationExtensions.Configuration) scope: ConfigurationScope.APPLICATION, default: ExtensionsPolicy.allowAll }, - 'extensions.extensionKind': { - type: 'object', - description: localize('extensions.extensionKind', "Configure ui or workspace extensions and allow them to run locally or remotely in a remote window."), - properties: { - 'ui': { - type: 'array', - items: { - type: 'string', - pattern: '^([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$', - } - }, - 'workspace': { - type: 'array', - items: { - type: 'string', - pattern: '^([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$', - } - } - }, - default: { - ui: [], - workspace: [] - } - }, - 'extensions.showInstalledExtensionsByDefault': { - type: 'boolean', - description: localize('extensions.showInstalledExtensionsByDefault', "When enabled, extensions view shows installed extensions view by default."), - default: false - } } }); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts index 1def9504ee..3da3e630b4 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts @@ -18,7 +18,7 @@ import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IE import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -176,14 +176,17 @@ export class InstallAction extends ExtensionAction { } update(): void { - if (!this.extension || this.extension.type === ExtensionType.System) { + if (!this.extension || this.extension.type === ExtensionType.System || this.extension.state === ExtensionState.Installed) { this.enabled = false; this.class = InstallAction.Class; this.label = InstallAction.INSTALL_LABEL; return; } - - this.enabled = this.extensionsWorkbenchService.canInstall(this.extension) && !this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, this.extension.identifier)); + this.enabled = false; + if (this.extensionsWorkbenchService.canInstall(this.extension)) { + const local = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier))[0]; + this.enabled = !local || (!!local.local && isLanguagePackExtension(local.local.manifest)); + } this.class = this.extension.state === ExtensionState.Installing ? InstallAction.InstallingClass : InstallAction.Class; this.updateLabel(); } @@ -2590,6 +2593,9 @@ export class DisabledLabelAction extends ExtensionAction { this.class = `${DisabledLabelAction.Class} hide`; this.label = ''; this.enabled = false; + if (this.extension && this.extension.local && isLanguagePackExtension(this.extension.local.manifest)) { + return; + } if (this.warningAction.enabled) { this.enabled = true; this.class = DisabledLabelAction.Class; @@ -2649,6 +2655,9 @@ export class SystemDisabledWarningAction extends ExtensionAction { this.enabled = false; this.class = `${SystemDisabledWarningAction.Class} hide`; this.tooltip = ''; + if (this.extension && this.extension.local && isLanguagePackExtension(this.extension.local.manifest)) { + return; + } if (this.extension && this.extension.local && this.extension.server && this._runningExtensions && this.workbenchEnvironmentService.configuration.remoteAuthority && this.extensionManagementServerService.remoteExtensionManagementServer) { const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier))[0]; const runningExtensionServer = runningExtension ? this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation) : null; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts index fad3dc2a6a..fd4e30b2f5 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts @@ -19,6 +19,7 @@ import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteB import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; export interface IExtensionsViewState { onFocus: Event; @@ -154,7 +155,7 @@ export class Renderer implements IPagedRenderer { const updateEnablement = async () => { const runningExtensions = await this.extensionService.getExtensions(); - if (extension.local) { + if (extension.local && !isLanguagePackExtension(extension.local.manifest)) { const runningExtension = runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, extension.identifier))[0]; const isSameExtensionRunning = runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation); toggleClass(data.root, 'disabled', !isSameExtensionRunning); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts index 19013a1e4d..936c8f01ad 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts @@ -54,6 +54,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { RemoteAuthorityContext } from 'vs/workbench/common/contextkeys'; interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -139,7 +140,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: EnabledExtensionsView }, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), ContextKeyExpr.not('config.extensions.showInstalledExtensionsByDefault')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.isEqualTo('')), weight: 40, canToggleVisibility: true, order: 1 @@ -154,7 +155,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: DisabledExtensionsView }, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), ContextKeyExpr.not('config.extensions.showInstalledExtensionsByDefault')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.isEqualTo('')), weight: 10, canToggleVisibility: true, order: 3, @@ -195,7 +196,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: `extensions.${server.authority}.default`, name: localize('installed', "Installed"), ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), ContextKeyExpr.has('config.extensions.showInstalledExtensionsByDefault')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.notEqualsTo('')), weight: 40, order: 1 }]; @@ -512,7 +513,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private doSearch(): Promise { const value = this.normalizedQuery(); - this.defaultViewsContextKey.set(!value); const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value); this.searchInstalledExtensionsContextKey.set(ExtensionsListView.isInstalledExtensionsQuery(value)); this.searchOutdatedExtensionsContextKey.set(ExtensionsListView.isOutdatedExtensionsQuery(value)); @@ -522,6 +522,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); + this.defaultViewsContextKey.set(!value); return this.progress(Promise.all(this.panels.map(view => (view).show(this.normalizedQuery()) diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts index 9edc0a722e..4f72fa5923 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts @@ -41,7 +41,7 @@ import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAction } from 'vs/base/common/actions'; -import { ExtensionType, ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import product from 'vs/platform/product/node/product'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; @@ -343,6 +343,11 @@ export class ExtensionsListView extends ViewletPanel { if ((isE1Running && isE2Running) || (!isE1Running && !isE2Running)) { return e1.displayName.localeCompare(e2.displayName); } + const isE1LanguagePackExtension = e1.local && isLanguagePackExtension(e1.local.manifest); + const isE2LanguagePackExtension = e2.local && isLanguagePackExtension(e2.local.manifest); + if ((isE1Running && isE2LanguagePackExtension) || (isE2Running && isE1LanguagePackExtension)) { + return e1.displayName.localeCompare(e2.displayName); + } return isE1Running ? -1 : 1; }); } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index ff63c65566..758ca2fae2 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -17,7 +17,7 @@ import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService, AutoSaveConfiguration } from 'vs/platform/files/common/files'; -import { toResource, ITextEditor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -356,64 +356,6 @@ function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean return directories.length > 0 && files.length > 0; } -let pasteShouldMove = false; -// Paste File/Folder -class PasteFileAction extends Action { - - public static readonly ID = 'filesExplorer.paste'; - - constructor( - private element: ExplorerItem, - @IFileService private fileService: IFileService, - @INotificationService private notificationService: INotificationService, - @IEditorService private readonly editorService: IEditorService, - @IExplorerService private readonly explorerService: IExplorerService - ) { - super(PasteFileAction.ID, PASTE_FILE_LABEL); - - if (!this.element) { - this.element = this.explorerService.roots[0]; - } - } - - public run(fileToPaste: URI): Promise { - - // Check if target is ancestor of pasted folder - if (this.element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(this.element.resource, fileToPaste, !isLinux /* ignorecase */)) { - throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder")); - } - - return this.fileService.resolve(fileToPaste).then(fileToPasteStat => { - - // Find target - let target: ExplorerItem; - if (this.element.resource.toString() === fileToPaste.toString()) { - target = this.element.parent!; - } else { - target = this.element.isDirectory ? this.element : this.element.parent!; - } - - const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwirte: pasteShouldMove }); - - // Copy File - const promise = pasteShouldMove ? this.fileService.move(fileToPaste, targetFile) : this.fileService.copy(fileToPaste, targetFile); - return promise.then(stat => { - if (pasteShouldMove) { - // Cut is done. Make sure to clear cut state. - this.explorerService.setToCopy([], false); - } - if (!stat.isDirectory) { - return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } }) - .then(types.withNullAsUndefined); - } - - return undefined; - }, e => onError(this.notificationService, e)); - }, error => { - onError(this.notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile"))); - }); - } -} export function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwirte: boolean }): URI { let name = resources.basenameOrAuthority(fileToPaste.resource); @@ -1083,6 +1025,7 @@ export const deleteFileHandler = (accessor: ServicesAccessor) => { return deleteFiles(accessor, stats, false); }; +let pasteShouldMove = false; export const copyFileHandler = (accessor: ServicesAccessor) => { const listService = accessor.get(IListService); if (!listService.lastFocusedList) { @@ -1112,16 +1055,50 @@ export const cutFileHandler = (accessor: ServicesAccessor) => { }; export const pasteFileHandler = (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); const clipboardService = accessor.get(IClipboardService); - if (!listService.lastFocusedList) { - return Promise.resolve(); - } - const explorerContext = getContext(listService.lastFocusedList); + const explorerService = accessor.get(IExplorerService); + const fileService = accessor.get(IFileService); + const notificationService = accessor.get(INotificationService); + const editorService = accessor.get(IEditorService); - return sequence(resources.distinctParents(clipboardService.readResources(), r => r).map(toCopy => { - const pasteFileAction = instantiationService.createInstance(PasteFileAction, explorerContext.stat); - return () => pasteFileAction.run(toCopy); - })); + if (listService.lastFocusedList) { + const explorerContext = getContext(listService.lastFocusedList); + const toPaste = resources.distinctParents(clipboardService.readResources(), r => r); + const element = explorerContext.stat || explorerService.roots[0]; + + // Check if target is ancestor of pasted folder + sequence(toPaste.map(fileToPaste => () => { + + if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste, !isLinux /* ignorecase */)) { + throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder")); + } + + return fileService.resolve(fileToPaste).then(fileToPasteStat => { + + // Find target + let target: ExplorerItem; + if (element.resource.toString() === fileToPaste.toString()) { + target = element.parent!; + } else { + target = element.isDirectory ? element : element.parent!; + } + + const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwirte: pasteShouldMove }); + + // Copy File + return pasteShouldMove ? fileService.move(fileToPaste, targetFile) : fileService.copy(fileToPaste, targetFile); + }, error => { + onError(notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile"))); + }); + })).then((stat) => { + if (pasteShouldMove) { + // Cut is done. Make sure to clear cut state. + explorerService.setToCopy([], false); + } + if (stat.length === 1 && !stat[0].isDirectory) { + editorService.openEditor({ resource: stat[0].resource, options: { pinned: true, preserveFocus: true } }).then(undefined, onUnexpectedError); + } + }); + } }; diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 5317b725a8..bc457c3c64 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -5,9 +5,9 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -// {{SQL CARBON EDIT}} - Import EditorInput -import { toResource, IEditorCommandsContext, EditorInput, SideBySideEditor } from 'vs/workbench/common/editor'; -import { IWindowsService, IWindowService, IURIToOpen, IOpenSettings, INewWindowOptions } from 'vs/platform/windows/common/windows'; +// {{SQL CARBON EDIT}} import EditorInput +import { toResource, IEditorCommandsContext, SideBySideEditor, EditorInput } from 'vs/workbench/common/editor'; +import { IWindowsService, IWindowService, IURIToOpen, IOpenSettings, INewWindowOptions, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -35,14 +35,15 @@ import { getMultiSelectedEditorContexts } from 'vs/workbench/browser/parts/edito import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -// {{SQL CARBON EDIT}} - Import EditorInput import { IEditorService, SIDE_GROUP, IResourceEditorReplacement } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { basename, toLocalResource } from 'vs/base/common/resources'; +import { basename, toLocalResource, joinPath } from 'vs/base/common/resources'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { UNTITLED_WORKSPACE_NAME } from 'vs/platform/workspaces/common/workspaces'; // {{SQL CARBON EDIT}} import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; @@ -86,6 +87,19 @@ export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace' export const openWindowCommand = (accessor: ServicesAccessor, urisToOpen: IURIToOpen[], options?: IOpenSettings) => { if (Array.isArray(urisToOpen)) { const windowService = accessor.get(IWindowService); + const environmentService = accessor.get(IEnvironmentService); + + // rewrite untitled: workspace URIs to the absolute path on disk + urisToOpen = urisToOpen.map(uriToOpen => { + if (isWorkspaceToOpen(uriToOpen) && uriToOpen.workspaceUri.scheme === Schemas.untitled) { + return { + workspaceUri: joinPath(environmentService.untitledWorkspacesHome, uriToOpen.workspaceUri.path, UNTITLED_WORKSPACE_NAME) + }; + } + + return uriToOpen; + }); + windowService.openWindow(urisToOpen, options); } }; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index baf5dc9651..84c3300b1a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -228,6 +228,10 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { return this.setting.scope === ConfigurationScope.WINDOW || this.setting.scope === ConfigurationScope.RESOURCE; } + if (configTarget === ConfigurationTarget.USER_REMOTE) { + return this.setting.scope === ConfigurationScope.MACHINE || this.setting.scope === ConfigurationScope.WINDOW || this.setting.scope === ConfigurationScope.RESOURCE; + } + return true; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 54412a0e8f..412693c60e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -16,13 +16,11 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { Schemas } from 'vs/base/common/network'; import { REMOTE_HOST_SCHEME, getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; -import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProductService } from 'vs/platform/product/common/product'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { URI } from 'vs/base/common/uri'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -173,53 +171,20 @@ export class TerminalProcessManager implements ITerminalProcessManager { if (!shellLaunchConfig.executable) { this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); } + const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, this._environmentService.userHome, activeWorkspaceRootUri, this._configHelper.config.cwd); - const env = this._createEnvironment(shellLaunchConfig, activeWorkspaceRootUri); + + const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + const lastActiveWorkspace = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : null; + const envFromConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.env.${platformKey}`); + const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(); + const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, lastActiveWorkspace, envFromConfigValue, this._configurationResolverService, isWorkspaceShellAllowed, this._productService.version, this._configHelper.config.setLocaleVariables); this._logService.debug(`Terminal process launching`, shellLaunchConfig, initialCwd, cols, rows, env); return this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, this._configHelper.config.windowsEnableConpty); } - private _createEnvironment(shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined): platform.IProcessEnvironment { - // Create a terminal environment based on settings, launch config and permissions - let env: platform.IProcessEnvironment = {}; - if (shellLaunchConfig.strictEnv) { - // strictEnv is true, only use the requested env (ignoring null entries) - terminalEnvironment.mergeNonNullKeys(env, shellLaunchConfig.env); - } else { - // Merge process env with the env from config and from shellLaunchConfig - terminalEnvironment.mergeNonNullKeys(env, process.env); - - // Determine config env based on workspace shell permissions - const lastActiveWorkspaceRoot = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : null; - const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); - const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(); - const envFromConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.env.${platformKey}`); - const allowedEnvFromConfig = { ...(isWorkspaceShellAllowed ? envFromConfigValue.value : envFromConfigValue.user) }; - - // Resolve env vars from config and shell - if (allowedEnvFromConfig) { - terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, allowedEnvFromConfig, lastActiveWorkspaceRoot); - } - if (shellLaunchConfig.env) { - terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, shellLaunchConfig.env, lastActiveWorkspaceRoot); - } - - // Merge config (settings) and ShellLaunchConfig environments - terminalEnvironment.mergeEnvironments(env, allowedEnvFromConfig); - terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); - - // Sanitize the environment, removing any undesirable VS Code and Electron environment - // variables - sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI'); - - // Adding other env keys necessary to create the process - terminalEnvironment.addTerminalEnvironmentKeys(env, this._productService.version, platform.locale, this._configHelper.config.setLocaleVariables); - } - return env; - } - public setDimensions(cols: number, rows: number): void { if (!this._process) { return; diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 63ffcf0fa1..4af692c014 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -9,6 +9,7 @@ import { URI as Uri } from 'vs/base/common/uri'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; /** * This module contains utility functions related to the environment, cwd and paths. @@ -59,7 +60,7 @@ export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, ve } } -export function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerminalEnvironment | NodeJS.ProcessEnv | undefined) { +function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerminalEnvironment | NodeJS.ProcessEnv | undefined) { if (!other) { return; } @@ -71,7 +72,7 @@ export function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerm } } -export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: ITerminalEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder | null): ITerminalEnvironment { +function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: ITerminalEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder | null): ITerminalEnvironment { Object.keys(env).forEach((key) => { const value = env[key]; if (typeof value === 'string' && lastActiveWorkspaceRoot !== null) { @@ -189,3 +190,49 @@ export function mergeDefaultShellPathAndArgs( shell.executable = shell.executable.replace(/\//g, '\\'); } } + +export function createTerminalEnvironment( + shellLaunchConfig: IShellLaunchConfig, + lastActiveWorkspace: IWorkspaceFolder | null, + envFromConfig: { user: ITerminalEnvironment | undefined, value: ITerminalEnvironment | undefined, default: ITerminalEnvironment | undefined }, + configurationResolverService: IConfigurationResolverService | undefined, + isWorkspaceShellAllowed: boolean, + version: string | undefined, + setLocaleVariables: boolean +): platform.IProcessEnvironment { + // Create a terminal environment based on settings, launch config and permissions + let env: platform.IProcessEnvironment = {}; + if (shellLaunchConfig.strictEnv) { + // strictEnv is true, only use the requested env (ignoring null entries) + mergeNonNullKeys(env, shellLaunchConfig.env); + } else { + // Merge process env with the env from config and from shellLaunchConfig + mergeNonNullKeys(env, process.env); + + // const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + // const envFromConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.env.${platformKey}`); + const allowedEnvFromConfig = { ...(isWorkspaceShellAllowed ? envFromConfig.value : envFromConfig.user) }; + + // Resolve env vars from config and shell + if (configurationResolverService) { + if (allowedEnvFromConfig) { + resolveConfigurationVariables(configurationResolverService, allowedEnvFromConfig, lastActiveWorkspace); + } + if (shellLaunchConfig.env) { + resolveConfigurationVariables(configurationResolverService, shellLaunchConfig.env, lastActiveWorkspace); + } + } + + // Merge config (settings) and ShellLaunchConfig environments + mergeEnvironments(env, allowedEnvFromConfig); + mergeEnvironments(env, shellLaunchConfig.env); + + // Sanitize the environment, removing any undesirable VS Code and Electron environment + // variables + sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI'); + + // Adding other env keys necessary to create the process + addTerminalEnvironmentKeys(env, version, platform.locale, setLocaleVariables); + } + return env; +} \ No newline at end of file diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index 5a8c43eb24..60355932d6 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -38,6 +38,7 @@ import { parseExtensionDevOptions } from '../common/extensionDevOptions'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug'; import { IExtensionHostStarter } from 'vs/workbench/services/extensions/common/extensions'; +import { isEqualOrParent } from 'vs/base/common/resources'; export class ExtensionHostProcessWorker implements IExtensionHostStarter { @@ -400,7 +401,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { configuration: withNullAsUndefined(workspace.configuration), id: workspace.id, - name: this._labelService.getWorkspaceLabel(workspace) + name: this._labelService.getWorkspaceLabel(workspace), + isUntitled: workspace.configuration ? isEqualOrParent(workspace.configuration, this._environmentService.untitledWorkspacesHome) : false }, resolvedExtensions: [], hostExtensions: [], diff --git a/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts b/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts index dd505d4bee..f8724e7457 100644 --- a/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts +++ b/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts @@ -8,7 +8,6 @@ import { IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionManagementServerService, IExtensionManagementServer, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { flatten } from 'vs/base/common/arrays'; import { ExtensionType, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -46,8 +45,10 @@ export class MultiExtensionManagementService extends Disposable implements IExte } getInstalled(type?: ExtensionType): Promise { - return Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type))) - .then(result => flatten(result)); + const installedExtensions: ILocalExtension[] = []; + return Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type).then(extensions => installedExtensions.push(...extensions)))) + .then(_ => installedExtensions) + .catch(e => installedExtensions); } async uninstall(extension: ILocalExtension, force?: boolean): Promise { diff --git a/src/vs/workbench/services/files/common/fileService.ts b/src/vs/workbench/services/files/common/fileService.ts index 8393433c8d..ac741f5021 100644 --- a/src/vs/workbench/services/files/common/fileService.ts +++ b/src/vs/workbench/services/files/common/fileService.ts @@ -17,6 +17,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, bufferToStream, VSBufferReadableStream, writeableBufferStream, VSBufferWriteableStream } from 'vs/base/common/buffer'; import { Queue } from 'vs/base/common/async'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { Schemas } from 'vs/base/common/network'; export class FileService extends Disposable implements IFileService { @@ -101,7 +102,7 @@ export class FileService extends Disposable implements IFileService { // Assert path is absolute if (!isAbsolutePath(resource)) { - throw new FileOperationError(localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)), FileOperationResult.FILE_INVALID_PATH); + throw new FileOperationError(localize('invalidPath', "The path of resource '{0}' must be absolute", this.resourceForError(resource)), FileOperationResult.FILE_INVALID_PATH); } // Activate provider @@ -110,11 +111,11 @@ export class FileService extends Disposable implements IFileService { // Assert provider const provider = this.provider.get(resource.scheme); if (!provider) { - const err = new Error(); - err.name = 'ENOPRO'; - err.message = `No provider found for ${resource.toString()}`; + const error = new Error(); + error.name = 'ENOPRO'; + error.message = localize('noProviderFound', "No file system provider found for {0}", resource.toString()); - throw err; + throw error; } return provider; @@ -150,7 +151,7 @@ export class FileService extends Disposable implements IFileService { // Specially handle file not found case as file operation result if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) { throw new FileOperationError( - localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), + localize('fileNotFoundError', "File not found ({0})", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND ); } @@ -270,7 +271,7 @@ export class FileService extends Disposable implements IFileService { // validate overwrite const overwrite = !!(options && options.overwrite); if (!overwrite && await this.exists(resource)) { - throw new FileOperationError(localize('fileExists', "File to create already exists ({0})", resource.toString(true)), FileOperationResult.FILE_MODIFIED_SINCE, options); + throw new FileOperationError(localize('fileExists', "File to create already exists ({0})", this.resourceForError(resource)), FileOperationResult.FILE_MODIFIED_SINCE, options); } // do write into file (this will create it too) @@ -305,7 +306,7 @@ export class FileService extends Disposable implements IFileService { await this.doWriteUnbuffered(provider, resource, bufferOrReadable); } } catch (error) { - throw new FileOperationError(localize('err.write', "Failed to write file {0}", resource.toString(false)), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.write', "Unable to write file ({0})", error.toString()), toFileOperationResult(error), options); } return this.resolve(resource, { resolveMetadata: true }); @@ -321,7 +322,7 @@ export class FileService extends Disposable implements IFileService { // file cannot be directory if ((stat.type & FileType.Directory) !== 0) { - throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString()), FileOperationResult.FILE_IS_DIRECTORY, options); + throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } // Dirty write prevention: if the file on disk has been changed and does not match our expected @@ -397,7 +398,7 @@ export class FileService extends Disposable implements IFileService { value: fileStream }; } catch (error) { - throw new FileOperationError(localize('err.read', "Failed to read file {0}", resource.toString(false)), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.read', "Unable to read file ({0})", error.toString()), toFileOperationResult(error), options); } } @@ -488,7 +489,7 @@ export class FileService extends Disposable implements IFileService { // Return early if resource is a directory if (stat.isDirectory) { - throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString()), FileOperationResult.FILE_IS_DIRECTORY, options); + throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } // Return early if file not modified since @@ -692,7 +693,7 @@ export class FileService extends Disposable implements IFileService { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { - throw new Error(localize('mkdirExistsError', "{0} exists, but is not a directory", directory.toString())); + throw new Error(localize('mkdirExistsError', "{0} exists, but is not a directory", this.resourceForError(directory))); } break; // we have hit a directory that exists -> good @@ -732,7 +733,7 @@ export class FileService extends Disposable implements IFileService { if (!recursive && await this.exists(resource)) { const stat = await this.resolve(resource); if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) { - throw new Error(localize('deleteFailed', "Failed to delete non-empty folder '{0}'.", resource.toString())); + throw new Error(localize('deleteFailed', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource))); } } @@ -1006,5 +1007,13 @@ export class FileService extends Disposable implements IFileService { return true; } + private resourceForError(resource: URI): string { + if (resource.scheme === Schemas.file) { + return resource.fsPath; + } + + return resource.toString(true); + } + //#endregion } diff --git a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts index 768aada2e0..e985e1e148 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts @@ -13,7 +13,7 @@ import { FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer } from 'vs/base/common/async'; import { normalizeNFC } from 'vs/base/common/normalization'; import { realcaseSync } from 'vs/base/node/extpath'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isLinux } from 'vs/base/common/platform'; import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files/node/watcher/watcher'; import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher'; import { Emitter, Event } from 'vs/base/common/event'; @@ -114,12 +114,21 @@ export class ChokidarWatcherService implements IWatcherService { disableGlobbing: true // fix https://github.com/Microsoft/vscode/issues/4586 }; + const excludes: string[] = []; // if there's only one request, use the built-in ignore-filterering const isSingleFolder = requests.length === 1; if (isSingleFolder) { - watcherOpts.ignored = requests[0].excludes; + excludes.push(...requests[0].excludes); } + if ((isMacintosh || isLinux) && (basePath.length === 0 || basePath === '/')) { + excludes.push('/dev/**'); + if (isLinux) { + excludes.push('/proc/**', '/sys/**'); + } + } + watcherOpts.ignored = excludes; + // Chokidar fails when the basePath does not match case-identical to the path on disk // so we have to find the real casing of the path and do some path massaging to fix this // see https://github.com/paulmillr/chokidar/issues/418 diff --git a/src/vs/workbench/services/files/test/node/diskFileService.test.ts b/src/vs/workbench/services/files/test/node/diskFileService.test.ts index 6c1552a38b..241f7e8f0a 100644 --- a/src/vs/workbench/services/files/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files/test/node/diskFileService.test.ts @@ -1382,6 +1382,10 @@ suite('Disk File Service', () => { }); test('watch - file - multiple writes', done => { + if (isWindows) { + return done(); // not happy + } + const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); @@ -1487,7 +1491,7 @@ suite('Disk File Service', () => { setTimeout(() => mkdirSync(folder.fsPath), 50); }); - test('watch - folder (non recursive) - delete folder', done => { + test.skip('watch - folder (non recursive) - delete folder', done => { const watchDir = URI.file(join(testDir, 'watch7')); mkdirSync(watchDir.fsPath); diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index dca4f68ebd..004a93d3ff 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -160,7 +160,7 @@ export class LabelService implements ILabelService { } getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string { - if (!isWorkspaceIdentifier(workspace) && !isSingleFolderWorkspaceIdentifier(workspace)) { + if (IWorkspace.isIWorkspace(workspace)) { const identifier = toWorkspaceIdentifier(workspace); if (!identifier) { return ''; @@ -176,23 +176,27 @@ export class LabelService implements ILabelService { return this.appendWorkspaceSuffix(label, workspace); } - // Workspace: Untitled - if (isEqualOrParent(workspace.configPath, this.environmentService.untitledWorkspacesHome)) { - return localize('untitledWorkspace', "Untitled (Workspace)"); - } + if (isWorkspaceIdentifier(workspace)) { + // Workspace: Untitled + if (isEqualOrParent(workspace.configPath, this.environmentService.untitledWorkspacesHome)) { + return localize('untitledWorkspace', "Untitled (Workspace)"); + } - // Workspace: Saved - let filename = basename(workspace.configPath); - if (endsWith(filename, WORKSPACE_EXTENSION)) { - filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + // Workspace: Saved + let filename = basename(workspace.configPath); + if (endsWith(filename, WORKSPACE_EXTENSION)) { + filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + } + let label; + if (options && options.verbose) { + label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspace.configPath), filename))); + } else { + label = localize('workspaceName', "{0} (Workspace)", filename); + } + return this.appendWorkspaceSuffix(label, workspace.configPath); } - let label; - if (options && options.verbose) { - label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspace.configPath), filename))); - } else { - label = localize('workspaceName', "{0} (Workspace)", filename); - } - return this.appendWorkspaceSuffix(label, workspace.configPath); + return ''; + } getSeparator(scheme: string, authority?: string): '/' | '\\' { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 119d2d3b0d..3ca04417c6 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -154,7 +154,6 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Model does not exist else { const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined); - model = newModel; modelPromise = model.load(options); // Install state change listener @@ -192,24 +191,24 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE this.mapResourceToPendingModelLoaders.set(resource, modelPromise); try { - const model = await modelPromise; + const resolvedModel = await modelPromise; // Make known to manager (if not already known) - this.add(resource, model); + this.add(resource, resolvedModel); // Model can be dirty if a backup was restored, so we make sure to have this event delivered - if (model.isDirty()) { - this._onModelDirty.fire(new TextFileModelChangeEvent(model, StateChange.DIRTY)); + if (resolvedModel.isDirty()) { + this._onModelDirty.fire(new TextFileModelChangeEvent(resolvedModel, StateChange.DIRTY)); } // Remove from pending loads this.mapResourceToPendingModelLoaders.delete(resource); - return model; + return resolvedModel; } catch (error) { // Free resources of this invalid model - if (model && typeof model.dispose === 'function') { // workaround for https://github.com/Microsoft/vscode/issues/72404 + if (model) { model.dispose(); } diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 4d0c5dc535..1bbdc265f4 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -39,6 +39,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { trim } from 'vs/base/common/strings'; import { VSBuffer } from 'vs/base/common/buffer'; import { ITextSnapshot } from 'vs/editor/common/model'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -85,7 +86,8 @@ export abstract class TextFileService extends Disposable implements ITextFileSer @IContextKeyService contextKeyService: IContextKeyService, @IDialogService private readonly dialogService: IDialogService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService ) { super(); diff --git a/src/vs/workbench/services/textfile/node/textFileService.ts b/src/vs/workbench/services/textfile/node/textFileService.ts index 4354c8a0a3..aa80862877 100644 --- a/src/vs/workbench/services/textfile/node/textFileService.ts +++ b/src/vs/workbench/services/textfile/node/textFileService.ts @@ -17,7 +17,7 @@ import { isMacintosh, isLinux } from 'vs/base/common/platform'; import product from 'vs/platform/product/node/product'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, IDetectedEncodingResult, encodeStream, UTF8_BOM, UTF16be_BOM, UTF16le_BOM, toDecodeStream, IDecodeStreamResult, detectEncodingByBOMFromBuffer } from 'vs/base/node/encoding'; +import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, encodeStream, UTF8_BOM, UTF16be_BOM, UTF16le_BOM, toDecodeStream, IDecodeStreamResult, detectEncodingByBOMFromBuffer } from 'vs/base/node/encoding'; import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { joinPath, extname, isEqualOrParent } from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -70,8 +70,8 @@ export class NodeTextFileService extends TextFileService { // read through encoding library const decoder = await toDecodeStream(this.streamToNodeReadable(bufferStream.value), { - guessEncoding: options && options.autoGuessEncoding, - overwriteEncoding: detected => this.encoding.getReadEncoding(resource, options, { encoding: detected, seemsBinary: false }) + guessEncoding: (options && options.autoGuessEncoding) || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), + overwriteEncoding: detectedEncoding => this.encoding.getReadEncoding(resource, options, detectedEncoding) }); // validate binary @@ -417,7 +417,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { const overwriteEncoding = options && options.overwriteEncoding; if (!overwriteEncoding && encoding === UTF8) { try { - const buffer = (await this.fileService.readFile(resource, { length: 3 })).value; + const buffer = (await this.fileService.readFile(resource, { length: UTF8_BOM.length })).value; if (detectEncodingByBOMFromBuffer(buffer, buffer.byteLength) === UTF8) { return { encoding, addBOM: true }; } @@ -438,12 +438,12 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { }; } - getReadEncoding(resource: URI, options: IReadTextFileOptions | undefined, detected: IDetectedEncodingResult): string { + getReadEncoding(resource: URI, options: IReadTextFileOptions | undefined, detectedEncoding: string | null): string { let preferredEncoding: string | undefined; // Encoding passed in as option if (options && options.encoding) { - if (detected.encoding === UTF8 && options.encoding === UTF8) { + if (detectedEncoding === UTF8 && options.encoding === UTF8) { preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8 } else { preferredEncoding = options.encoding; // give passed in encoding highest priority @@ -451,11 +451,11 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { } // Encoding detected - else if (detected.encoding) { - if (detected.encoding === UTF8) { + else if (detectedEncoding) { + if (detectedEncoding === UTF8) { preferredEncoding = UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM } else { - preferredEncoding = detected.encoding; + preferredEncoding = detectedEncoding; } } diff --git a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts index 481e499a3e..12d985b182 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts @@ -570,6 +570,13 @@ suite('Files - TextFileService i/o', () => { assert.equal(result.encoding, 'utf16be'); }); + test('readStream - autoguessEncoding', async () => { + const resource = URI.file(join(testDir, 'some_cp1252.txt')); + + const result = await service.readStream(resource, { autoGuessEncoding: true }); + assert.equal(result.encoding, 'windows1252'); + }); + test('readStream - FILE_IS_BINARY', async () => { const resource = URI.file(join(testDir, 'binary.txt')); @@ -586,4 +593,21 @@ suite('Files - TextFileService i/o', () => { const result = await service.readStream(URI.file(join(testDir, 'small.txt')), { acceptTextOnly: true }); assert.equal(result.name, 'small.txt'); }); + + test('read - FILE_IS_BINARY', async () => { + const resource = URI.file(join(testDir, 'binary.txt')); + + let error: TextFileOperationError | undefined = undefined; + try { + await service.read(resource, { acceptTextOnly: true }); + } catch (err) { + error = err; + } + + assert.ok(error); + assert.equal(error!.textFileOperationResult, TextFileOperationResult.FILE_IS_BINARY); + + const result = await service.read(URI.file(join(testDir, 'small.txt')), { acceptTextOnly: true }); + assert.equal(result.name, 'small.txt'); + }); }); diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 6595244bed..0e5639dad9 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -6,19 +6,19 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, DETECT_HC_SETTING, HC_THEME_ID } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, DETECT_HC_SETTING, HC_THEME_ID, IColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Registry } from 'vs/platform/registry/common/platform'; import * as errors from 'vs/base/common/errors'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; -import { ColorThemeData } from './colorThemeData'; +import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData'; import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ColorThemeStore } from 'vs/workbench/services/themes/browser/colorThemeStore'; +import { ColorThemeStore } from 'vs/workbench/services/themes/common/colorThemeStore'; import { FileIconThemeStore } from 'vs/workbench/services/themes/common/fileIconThemeStore'; import { FileIconThemeData } from 'vs/workbench/services/themes/common/fileIconThemeData'; import { removeClasses, addClasses } from 'vs/base/browser/dom'; @@ -64,10 +64,6 @@ function validateThemeId(theme: string): string { return theme; } -export interface IColorCustomizations { - [colorIdOrThemeSettingsId: string]: string | IColorCustomizations; -} - export class WorkbenchThemeService implements IWorkbenchThemeService { _serviceBrand: any; diff --git a/src/vs/workbench/services/themes/browser/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts similarity index 98% rename from src/vs/workbench/services/themes/browser/colorThemeData.ts rename to src/vs/workbench/services/themes/common/colorThemeData.ts index 405c4c1069..ad6b3a828a 100644 --- a/src/vs/workbench/services/themes/browser/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -6,8 +6,8 @@ import { basename } from 'vs/base/common/path'; import * as Json from 'vs/base/common/json'; import { Color } from 'vs/base/common/color'; -import { ExtensionData, ITokenColorCustomizations, ITokenColorizationRule, IColorTheme, IColorMap, IThemeExtensionPoint, VS_LIGHT_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { convertSettings } from 'vs/workbench/services/themes/browser/themeCompatibility'; +import { ExtensionData, ITokenColorCustomizations, ITokenColorizationRule, IColorTheme, IColorMap, IThemeExtensionPoint, VS_LIGHT_THEME, VS_HC_THEME, IColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { convertSettings } from 'vs/workbench/services/themes/common/themeCompatibility'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import * as objects from 'vs/base/common/objects'; @@ -15,7 +15,6 @@ import * as resources from 'vs/base/common/resources'; import { Extensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ThemeType } from 'vs/platform/theme/common/themeService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IColorCustomizations } from 'vs/workbench/services/themes/browser/workbenchThemeService'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/src/vs/workbench/services/themes/browser/colorThemeStore.ts b/src/vs/workbench/services/themes/common/colorThemeStore.ts similarity index 98% rename from src/vs/workbench/services/themes/browser/colorThemeStore.ts rename to src/vs/workbench/services/themes/common/colorThemeStore.ts index 8ed91ed6d5..f44d48c350 100644 --- a/src/vs/workbench/services/themes/browser/colorThemeStore.ts +++ b/src/vs/workbench/services/themes/common/colorThemeStore.ts @@ -9,7 +9,7 @@ import * as types from 'vs/base/common/types'; import * as resources from 'vs/base/common/resources'; import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionData, IThemeExtensionPoint, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { ColorThemeData } from 'vs/workbench/services/themes/browser/colorThemeData'; +import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/themes/browser/themeCompatibility.ts b/src/vs/workbench/services/themes/common/themeCompatibility.ts similarity index 100% rename from src/vs/workbench/services/themes/browser/themeCompatibility.ts rename to src/vs/workbench/services/themes/common/themeCompatibility.ts diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 3aa3deb1c3..9567a2144b 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -64,6 +64,10 @@ export interface IWorkbenchThemeService extends IThemeService { onDidFileIconThemeChange: Event; } +export interface IColorCustomizations { + [colorIdOrThemeSettingsId: string]: string | IColorCustomizations; +} + export interface ITokenColorCustomizations { comments?: string | ITokenColorizationSetting; strings?: string | ITokenColorizationSetting; diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 45fe91a26c..95f31fe0b0 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -106,19 +106,27 @@ suite('Notifications', () => { assert.equal(item6.actions.primary!.length, 1); // Links - let item7 = NotificationViewItem.create({ severity: Severity.Info, message: 'Unable to [Link 1](http://link1.com) open [Link 2](https://link2.com) and [Invalid Link3](ftp://link3.com)' })!; + let item7 = NotificationViewItem.create({ severity: Severity.Info, message: 'Unable to [Link 1](http://link1.com) open [Link 2](command:open.me "Open This") and [Link 3](command:without.title) and [Invalid Link4](ftp://link4.com)' })!; const links = item7.message.links; - assert.equal(links.length, 2); + assert.equal(links.length, 3); assert.equal(links[0].name, 'Link 1'); assert.equal(links[0].href, 'http://link1.com'); + assert.equal(links[0].title, 'http://link1.com'); assert.equal(links[0].length, '[Link 1](http://link1.com)'.length); assert.equal(links[0].offset, 'Unable to '.length); assert.equal(links[1].name, 'Link 2'); - assert.equal(links[1].href, 'https://link2.com'); - assert.equal(links[1].length, '[Link 2](https://link2.com)'.length); + assert.equal(links[1].href, 'command:open.me'); + assert.equal(links[1].title, 'Open This'); + assert.equal(links[1].length, '[Link 2](command:open.me "Open This")'.length); assert.equal(links[1].offset, 'Unable to [Link 1](http://link1.com) open '.length); + + assert.equal(links[2].name, 'Link 3'); + assert.equal(links[2].href, 'command:without.title'); + assert.equal(links[2].title, 'Click to execute command \'without.title\''); + assert.equal(links[2].length, '[Link 3](command:without.title)'.length); + assert.equal(links[2].offset, 'Unable to [Link 1](http://link1.com) open [Link 2](command:open.me "Open This") and '.length); }); test('Model', () => { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index db07671026..f9a00ac5be 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -200,7 +200,8 @@ export class TestTextFileService extends BrowserTextFileService { @IContextKeyService contextKeyService: IContextKeyService, @IDialogService dialogService: IDialogService, @IFileDialogService fileDialogService: IFileDialogService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService ) { super( contextService, @@ -219,7 +220,8 @@ export class TestTextFileService extends BrowserTextFileService { contextKeyService, dialogService, fileDialogService, - editorService + editorService, + textResourceConfigurationService ); }