Merge from vscode 966b87dd40
(#4696)
|
@ -75,6 +75,9 @@
|
|||
"linux": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/sql.sh"
|
||||
},
|
||||
"env": {
|
||||
"VSCODE_EXTHOST_WILL_SEND_SOCKET": null
|
||||
},
|
||||
"breakOnLoad": false,
|
||||
"urlFilter": "*workbench.html*",
|
||||
"runtimeArgs": [
|
||||
|
|
|
@ -362,7 +362,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
|||
.pipe(util.cleanNodeModule('vscode-nsfw', ['binding.gyp', 'build/**', 'src/**', 'openpa/**', 'includes/**'], ['**/*.node', '**/*.a']))
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
.pipe(util.cleanNodeModule('vsda', ['binding.gyp', 'README.md', 'build/**', '*.bat', '*.sh', '*.cpp', '*.h'], ['build/Release/vsda.node']))
|
||||
.pipe(util.cleanNodeModule('win-ca-lib', ['**/*'], ['package.json', '**/*.node']))
|
||||
.pipe(util.cleanNodeModule('vscode-windows-ca-certs', ['**/*'], ['package.json', '**/*.node']))
|
||||
.pipe(util.cleanNodeModule('node-addon-api', ['**/*']))
|
||||
.pipe(createAsar(path.join(process.cwd(), 'node_modules'), ['**/*.node', '**/vscode-ripgrep/bin/*', '**/node-pty/build/Release/*'], 'app/node_modules.asar'));
|
||||
|
||||
|
|
|
@ -21,8 +21,8 @@ function log(message, ...rest) {
|
|||
fancyLog(ansiColors.green('[i18n]'), message, ...rest);
|
||||
}
|
||||
exports.defaultLanguages = [
|
||||
{ id: 'zh-tw', folderName: 'cht', transifexId: 'zh-hant' },
|
||||
{ id: 'zh-cn', folderName: 'chs', transifexId: 'zh-hans' },
|
||||
{ id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' },
|
||||
{ id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' },
|
||||
{ id: 'ja', folderName: 'jpn' },
|
||||
{ id: 'ko', folderName: 'kor' },
|
||||
{ id: 'de', folderName: 'deu' },
|
||||
|
@ -372,7 +372,11 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) {
|
|||
}
|
||||
});
|
||||
});
|
||||
let languageDirectory = path.join(__dirname, '..', '..', 'i18n');
|
||||
let languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n');
|
||||
if (!fs.existsSync(languageDirectory)) {
|
||||
log(`No VS Code localization repository found. Looking at ${languageDirectory}`);
|
||||
log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`);
|
||||
}
|
||||
let sortedLanguages = sortLanguages(languages);
|
||||
sortedLanguages.forEach((language) => {
|
||||
if (process.env['VSCODE_BUILD_VERBOSE']) {
|
||||
|
@ -380,22 +384,25 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) {
|
|||
}
|
||||
statistics[language.id] = 0;
|
||||
let localizedModules = Object.create(null);
|
||||
let languageFolderName = language.folderName || language.id;
|
||||
let cwd = path.join(languageDirectory, languageFolderName, 'src');
|
||||
let languageFolderName = language.translationId || language.id;
|
||||
let i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json');
|
||||
let allMessages;
|
||||
if (fs.existsSync(i18nFile)) {
|
||||
let content = stripComments(fs.readFileSync(i18nFile, 'utf8'));
|
||||
allMessages = JSON.parse(content);
|
||||
}
|
||||
modules.forEach((module) => {
|
||||
let order = keysSection[module];
|
||||
let i18nFile = path.join(cwd, module) + '.i18n.json';
|
||||
let messages = null;
|
||||
if (fs.existsSync(i18nFile)) {
|
||||
let content = stripComments(fs.readFileSync(i18nFile, 'utf8'));
|
||||
messages = JSON.parse(content);
|
||||
let moduleMessage;
|
||||
if (allMessages) {
|
||||
moduleMessage = allMessages.contents[module];
|
||||
}
|
||||
else {
|
||||
if (!moduleMessage) {
|
||||
if (process.env['VSCODE_BUILD_VERBOSE']) {
|
||||
log(`No localized messages found for module ${module}. Using default messages.`);
|
||||
}
|
||||
messages = defaultMessages[module];
|
||||
statistics[language.id] = statistics[language.id] + Object.keys(messages).length;
|
||||
moduleMessage = defaultMessages[module];
|
||||
statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length;
|
||||
}
|
||||
let localizedMessages = [];
|
||||
order.forEach((keyInfo) => {
|
||||
|
@ -406,7 +413,7 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) {
|
|||
else {
|
||||
key = keyInfo.key;
|
||||
}
|
||||
let message = messages[key];
|
||||
let message = moduleMessage[key];
|
||||
if (!message) {
|
||||
if (process.env['VSCODE_BUILD_VERBOSE']) {
|
||||
log(`No localized message found for key ${key} in module ${module}. Using default message.`);
|
||||
|
@ -956,7 +963,7 @@ function retrieveResource(language, resource, apiHostname, credentials) {
|
|||
return limiter.queue(() => new Promise((resolve, reject) => {
|
||||
const slug = resource.name.replace(/\//g, '_');
|
||||
const project = resource.project;
|
||||
let transifexLanguageId = language.id === 'ps' ? 'en' : language.transifexId || language.id;
|
||||
let transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id;
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`,
|
||||
|
|
|
@ -25,7 +25,7 @@ function log(message: any, ...rest: any[]): void {
|
|||
|
||||
export interface Language {
|
||||
id: string; // laguage id, e.g. zh-tw, de
|
||||
transifexId?: string; // language id used in transifex, e.g zh-hant, de (optional, if not set, the id is used)
|
||||
translationId?: string; // language id used in translation tools, e.g zh-hant, de (optional, if not set, the id is used)
|
||||
folderName?: string; // language specific folder name, e.g. cht, deu (optional, if not set, the id is used)
|
||||
}
|
||||
|
||||
|
@ -38,8 +38,8 @@ export interface InnoSetup {
|
|||
}
|
||||
|
||||
export const defaultLanguages: Language[] = [
|
||||
{ id: 'zh-tw', folderName: 'cht', transifexId: 'zh-hant' },
|
||||
{ id: 'zh-cn', folderName: 'chs', transifexId: 'zh-hans' },
|
||||
{ id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' },
|
||||
{ id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' },
|
||||
{ id: 'ja', folderName: 'jpn' },
|
||||
{ id: 'ko', folderName: 'kor' },
|
||||
{ id: 'de', folderName: 'deu' },
|
||||
|
@ -144,6 +144,15 @@ interface BundledExtensionFormat {
|
|||
};
|
||||
}
|
||||
|
||||
interface I18nFormat {
|
||||
version: string;
|
||||
contents: {
|
||||
[module: string]: {
|
||||
[messageKey: string]: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class Line {
|
||||
private buffer: string[] = [];
|
||||
|
||||
|
@ -486,7 +495,11 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json
|
|||
});
|
||||
});
|
||||
|
||||
let languageDirectory = path.join(__dirname, '..', '..', 'i18n');
|
||||
let languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n');
|
||||
if (!fs.existsSync(languageDirectory)) {
|
||||
log(`No VS Code localization repository found. Looking at ${languageDirectory}`);
|
||||
log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`);
|
||||
}
|
||||
let sortedLanguages = sortLanguages(languages);
|
||||
sortedLanguages.forEach((language) => {
|
||||
if (process.env['VSCODE_BUILD_VERBOSE']) {
|
||||
|
@ -495,21 +508,25 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json
|
|||
|
||||
statistics[language.id] = 0;
|
||||
let localizedModules: Map<string[]> = Object.create(null);
|
||||
let languageFolderName = language.folderName || language.id;
|
||||
let cwd = path.join(languageDirectory, languageFolderName, 'src');
|
||||
let languageFolderName = language.translationId || language.id;
|
||||
let i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json');
|
||||
let allMessages: I18nFormat | undefined;
|
||||
if (fs.existsSync(i18nFile)) {
|
||||
let content = stripComments(fs.readFileSync(i18nFile, 'utf8'));
|
||||
allMessages = JSON.parse(content);
|
||||
}
|
||||
modules.forEach((module) => {
|
||||
let order = keysSection[module];
|
||||
let i18nFile = path.join(cwd, module) + '.i18n.json';
|
||||
let messages: Map<string> | null = null;
|
||||
if (fs.existsSync(i18nFile)) {
|
||||
let content = stripComments(fs.readFileSync(i18nFile, 'utf8'));
|
||||
messages = JSON.parse(content);
|
||||
} else {
|
||||
let moduleMessage: { [messageKey: string]: string } | undefined;
|
||||
if (allMessages) {
|
||||
moduleMessage = allMessages.contents[module];
|
||||
}
|
||||
if (!moduleMessage) {
|
||||
if (process.env['VSCODE_BUILD_VERBOSE']) {
|
||||
log(`No localized messages found for module ${module}. Using default messages.`);
|
||||
}
|
||||
messages = defaultMessages[module];
|
||||
statistics[language.id] = statistics[language.id] + Object.keys(messages).length;
|
||||
moduleMessage = defaultMessages[module];
|
||||
statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length;
|
||||
}
|
||||
let localizedMessages: string[] = [];
|
||||
order.forEach((keyInfo) => {
|
||||
|
@ -519,7 +536,7 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json
|
|||
} else {
|
||||
key = keyInfo.key;
|
||||
}
|
||||
let message: string = messages![key];
|
||||
let message: string = moduleMessage![key];
|
||||
if (!message) {
|
||||
if (process.env['VSCODE_BUILD_VERBOSE']) {
|
||||
log(`No localized message found for key ${key} in module ${module}. Using default message.`);
|
||||
|
@ -1093,7 +1110,7 @@ function retrieveResource(language: Language, resource: Resource, apiHostname: s
|
|||
return limiter.queue(() => new Promise<File | null>((resolve, reject) => {
|
||||
const slug = resource.name.replace(/\//g, '_');
|
||||
const project = resource.project;
|
||||
let transifexLanguageId = language.id === 'ps' ? 'en' : language.transifexId || language.id;
|
||||
let transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id;
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`,
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
"use strict";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const ts = require("typescript");
|
||||
const Lint = require("tslint");
|
||||
const path_1 = require("path");
|
||||
class Rule extends Lint.Rules.AbstractRule {
|
||||
apply(sourceFile) {
|
||||
if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.api/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.main/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.worker/.test(sourceFile.fileName)) {
|
||||
return this.applyWithWalker(new NoNlsInStandaloneEditorRuleWalker(sourceFile, this.getOptions()));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
exports.Rule = Rule;
|
||||
class NoNlsInStandaloneEditorRuleWalker extends Lint.RuleWalker {
|
||||
constructor(file, opts) {
|
||||
super(file, opts);
|
||||
}
|
||||
visitImportEqualsDeclaration(node) {
|
||||
if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) {
|
||||
this._validateImport(node.moduleReference.expression.getText(), node);
|
||||
}
|
||||
}
|
||||
visitImportDeclaration(node) {
|
||||
this._validateImport(node.moduleSpecifier.getText(), node);
|
||||
}
|
||||
visitCallExpression(node) {
|
||||
super.visitCallExpression(node);
|
||||
// import('foo') statements inside the code
|
||||
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
||||
const [path] = node.arguments;
|
||||
this._validateImport(path.getText(), node);
|
||||
}
|
||||
}
|
||||
_validateImport(path, node) {
|
||||
// remove quotes
|
||||
path = path.slice(1, -1);
|
||||
// resolve relative paths
|
||||
if (path[0] === '.') {
|
||||
path = path_1.join(this.getSourceFile().fileName, path);
|
||||
}
|
||||
if (/vs(\/|\\)nls/.test(path)) {
|
||||
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts`));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import * as Lint from 'tslint';
|
||||
import { join } from 'path';
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
if (
|
||||
/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.api/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.main/.test(sourceFile.fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.worker/.test(sourceFile.fileName)
|
||||
) {
|
||||
return this.applyWithWalker(new NoNlsInStandaloneEditorRuleWalker(sourceFile, this.getOptions()));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class NoNlsInStandaloneEditorRuleWalker extends Lint.RuleWalker {
|
||||
|
||||
constructor(file: ts.SourceFile, opts: Lint.IOptions) {
|
||||
super(file, opts);
|
||||
}
|
||||
|
||||
protected visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): void {
|
||||
if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) {
|
||||
this._validateImport(node.moduleReference.expression.getText(), node);
|
||||
}
|
||||
}
|
||||
|
||||
protected visitImportDeclaration(node: ts.ImportDeclaration): void {
|
||||
this._validateImport(node.moduleSpecifier.getText(), node);
|
||||
}
|
||||
|
||||
protected visitCallExpression(node: ts.CallExpression): void {
|
||||
super.visitCallExpression(node);
|
||||
|
||||
// import('foo') statements inside the code
|
||||
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
||||
const [path] = node.arguments;
|
||||
this._validateImport(path.getText(), node);
|
||||
}
|
||||
}
|
||||
|
||||
private _validateImport(path: string, node: ts.Node): void {
|
||||
// remove quotes
|
||||
path = path.slice(1, -1);
|
||||
|
||||
// resolve relative paths
|
||||
if (path[0] === '.') {
|
||||
path = join(this.getSourceFile().fileName, path);
|
||||
}
|
||||
|
||||
if (
|
||||
/vs(\/|\\)nls/.test(path)
|
||||
) {
|
||||
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts`));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,8 +45,8 @@ class NoStandaloneEditorRuleWalker extends Lint.RuleWalker {
|
|||
if (path[0] === '.') {
|
||||
path = path_1.join(this.getSourceFile().fileName, path);
|
||||
}
|
||||
if (/vs(\/|\\)editor(\/|\\)standalone/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone/.test(path)
|
||||
if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.api/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.main/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.worker/.test(path)) {
|
||||
|
|
|
@ -54,8 +54,8 @@ class NoStandaloneEditorRuleWalker extends Lint.RuleWalker {
|
|||
}
|
||||
|
||||
if (
|
||||
/vs(\/|\\)editor(\/|\\)standalone/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone/.test(path)
|
||||
/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.api/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.main/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.worker/.test(path)
|
||||
|
|
|
@ -703,89 +703,94 @@
|
|||
"group": "1_sync",
|
||||
"when": "scmProvider == git && config.git.allowForcePush"
|
||||
},
|
||||
{
|
||||
"command": "git.checkout",
|
||||
"group": "2_branch",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.publish",
|
||||
"group": "2_publish",
|
||||
"group": "2_branch",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStaged",
|
||||
"group": "3_commit",
|
||||
"group": "4_commit",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStagedSigned",
|
||||
"group": "3_commit",
|
||||
"group": "4_commit",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStagedAmend",
|
||||
"group": "3_commit",
|
||||
"group": "4_commit",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAll",
|
||||
"group": "3_commit",
|
||||
"group": "4_commit",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAllSigned",
|
||||
"group": "3_commit",
|
||||
"group": "4_commit",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAllAmend",
|
||||
"group": "3_commit",
|
||||
"group": "4_commit",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.undoCommit",
|
||||
"group": "3_commit",
|
||||
"group": "4_commit",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.stageAll",
|
||||
"group": "4_stage",
|
||||
"group": "5_stage",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.unstageAll",
|
||||
"group": "4_stage",
|
||||
"group": "5_stage",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.cleanAll",
|
||||
"group": "4_stage",
|
||||
"group": "5_stage",
|
||||
"when": "scmProvider == git && !gitFreshRepository"
|
||||
},
|
||||
{
|
||||
"command": "git.stashIncludeUntracked",
|
||||
"group": "5_stash",
|
||||
"group": "6_stash",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.stash",
|
||||
"group": "5_stash",
|
||||
"group": "6_stash",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.stashPop",
|
||||
"group": "5_stash",
|
||||
"group": "6_stash",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.stashPopLatest",
|
||||
"group": "5_stash",
|
||||
"group": "6_stash",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.stashApply",
|
||||
"group": "5_stash",
|
||||
"group": "6_stash",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "git.stashApplyLatest",
|
||||
"group": "5_stash",
|
||||
"group": "6_stash",
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
"vscode-ripgrep": "^1.2.5",
|
||||
"vscode-sqlite3": "4.0.7",
|
||||
"vscode-textmate": "^4.0.1",
|
||||
"vscode-xterm": "3.13.0-beta1",
|
||||
"vscode-xterm": "3.13.0-beta2",
|
||||
"yauzl": "^2.9.1",
|
||||
"yazl": "^2.4.3",
|
||||
"zone.js": "^0.8.4"
|
||||
|
@ -177,7 +177,7 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"vscode-windows-registry": "1.0.1",
|
||||
"win-ca-lib": "https://github.com/chrmarti/win-ca/releases/download/v2.4.1-lib-test/win-ca-lib-2.4.1.tgz",
|
||||
"vscode-windows-ca-certs": "0.1.0",
|
||||
"windows-foreground-love": "0.1.0",
|
||||
"windows-mutex": "0.2.1",
|
||||
"windows-process-tree": "0.2.3"
|
||||
|
|
|
@ -1,3 +1,34 @@
|
|||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec "$@" --executed-from="$(pwd)" --pid=$$
|
||||
# On Fedora $SNAP is under /var and there is some magic to map it to /snap.
|
||||
# We need to handle that case and reset $SNAP
|
||||
SNAP=$(echo $SNAP | sed -e "s|/var/lib/snapd||g")
|
||||
|
||||
if [ "$SNAP_ARCH" == "amd64" ]; then
|
||||
ARCH="x86_64-linux-gnu"
|
||||
elif [ "$SNAP_ARCH" == "armhf" ]; then
|
||||
ARCH="arm-linux-gnueabihf"
|
||||
elif [ "$SNAP_ARCH" == "arm64" ]; then
|
||||
ARCH="aarch64-linux-gnu"
|
||||
else
|
||||
ARCH="$SNAP_ARCH-linux-gnu"
|
||||
fi
|
||||
|
||||
export XDG_CACHE_HOME=$SNAP_USER_COMMON/.cache
|
||||
if [[ -d $SNAP_USER_DATA/.cache && ! -e $XDG_CACHE_HOME ]]; then
|
||||
# the .cache directory used to be stored under $SNAP_USER_DATA, migrate it
|
||||
mv $SNAP_USER_DATA/.cache $SNAP_USER_COMMON/
|
||||
fi
|
||||
mkdir -p $XDG_CACHE_HOME
|
||||
|
||||
# Gdk-pixbuf loaders
|
||||
export GDK_PIXBUF_MODULE_FILE=$XDG_CACHE_HOME/gdk-pixbuf-loaders.cache
|
||||
export GDK_PIXBUF_MODULEDIR=$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders
|
||||
if [ -f $SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders ]; then
|
||||
$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders > $GDK_PIXBUF_MODULE_FILE
|
||||
fi
|
||||
|
||||
# Create $XDG_RUNTIME_DIR if not exists (to be removed when https://pad.lv/1656340 is fixed)
|
||||
[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p $XDG_RUNTIME_DIR -m 700
|
||||
|
||||
exec "$@"
|
||||
|
|
|
@ -10,33 +10,54 @@ grade: stable
|
|||
confinement: classic
|
||||
|
||||
parts:
|
||||
gnome:
|
||||
plugin: nil
|
||||
build-packages:
|
||||
- software-properties-common
|
||||
override-pull: |
|
||||
add-apt-repository -y ppa:ubuntu-desktop/gnome-3-26
|
||||
apt -y update
|
||||
|
||||
code:
|
||||
after:
|
||||
- gnome
|
||||
plugin: dump
|
||||
source: .
|
||||
stage-packages:
|
||||
- fcitx-frontend-gtk3
|
||||
- gvfs-libs
|
||||
- libasound2
|
||||
- libc++1
|
||||
- libgconf-2-4
|
||||
- libglib2.0-bin
|
||||
- libgnome-keyring0
|
||||
- libgtk-3-0
|
||||
- libnotify4
|
||||
- libnspr4
|
||||
- libnss3
|
||||
- libpcre3
|
||||
- libpulse0
|
||||
- libsecret-1-0
|
||||
- libxss1
|
||||
- libxtst6
|
||||
# desktop-gtk2 deps below
|
||||
- libxkbcommon0
|
||||
- libgtk2.0-0
|
||||
# - unity-gtk2-module
|
||||
- libappindicator1
|
||||
- zlib1g
|
||||
prime:
|
||||
- -usr/share/dh-python
|
||||
electron-launch:
|
||||
plugin: dump
|
||||
source: .
|
||||
organize:
|
||||
electron-launch: bin/electron-launch
|
||||
- -usr/share/doc
|
||||
- -usr/share/fonts
|
||||
- -usr/share/icons
|
||||
- -usr/share/lintian
|
||||
- -usr/share/man
|
||||
|
||||
apps:
|
||||
@@NAME@@:
|
||||
command: bin/electron-launch ${SNAP}/usr/share/@@NAME@@/bin/@@NAME@@
|
||||
desktop: usr/share/applications/@@NAME@@.desktop
|
||||
command: electron-launch $SNAP/usr/share/@@NAME@@/bin/@@NAME@@
|
||||
desktop: usr/share/applications/@@NAME@@.desktop
|
||||
environment:
|
||||
DISABLE_WAYLAND: 1
|
||||
GSETTINGS_SCHEMA_DIR: $SNAP/usr/share/glib-2.0/schemas
|
||||
|
||||
url-handler:
|
||||
command: electron-launch $SNAP/usr/share/@@NAME@@/bin/@@NAME@@ --open-url
|
||||
desktop: usr/share/applications/@@NAME@@-url-handler.desktop
|
||||
environment:
|
||||
DISABLE_WAYLAND: 1
|
||||
GSETTINGS_SCHEMA_DIR: $SNAP/usr/share/glib-2.0/schemas
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Pass in a version like ./scripts/generate-vscode-dts.sh 1.30."
|
||||
echo "Failed to generate index.d.ts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
header="// Type definitions for Visual Studio Code ${1}
|
||||
// Project: https://github.com/microsoft/vscode
|
||||
// Definitions by: Visual Studio Code Team, Microsoft <https://github.com/Microsoft>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Type Definition for Visual Studio Code ${1} Extension API
|
||||
* See https://code.visualstudio.com/api for more information
|
||||
*/"
|
||||
|
||||
if [ -f ./src/vs/vscode.d.ts ]; then
|
||||
echo "$header" > index.d.ts
|
||||
sed "1,4d" ./src/vs/vscode.d.ts >> index.d.ts
|
||||
echo "Generated index.d.ts for version ${1}."
|
||||
else
|
||||
echo "Can't find ./src/vs/vscode.d.ts. Run this script at vscode root."
|
||||
fi
|
|
@ -31,7 +31,6 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
|
|||
import { TreeNodeContextKey } from 'sql/parts/objectExplorer/viewlet/treeNodeContextKey';
|
||||
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
|
||||
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
|
||||
import * as constants from 'sql/common/constants';
|
||||
import { ServerInfoContextKey } from 'sql/parts/connection/common/serverInfoContextKey';
|
||||
|
||||
/**
|
||||
|
@ -101,7 +100,7 @@ export class ServerTreeActionProvider extends ContributableActionProvider {
|
|||
let actions = getDefaultActions(context);
|
||||
let options = { arg: undefined, shouldForwardArgs: true };
|
||||
const groups = menu.getActions(options);
|
||||
fillInActions(groups, actions, this.contextMenuService);
|
||||
fillInActions(groups, actions, false);
|
||||
|
||||
// Cleanup
|
||||
scopedContextService.dispose();
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"alwaysStrict": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictNullChecks": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"vs/*": [
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
|
||||
|
||||
export type EventHandler = HTMLElement | HTMLDocument | Window;
|
||||
|
||||
export interface IDomEvent {
|
||||
<K extends keyof HTMLElementEventMap>(element: EventHandler, type: K, useCapture?: boolean): Event<HTMLElementEventMap[K]>;
|
||||
(element: EventHandler, type: string, useCapture?: boolean): Event<any>;
|
||||
<K extends keyof HTMLElementEventMap>(element: EventHandler, type: K, useCapture?: boolean): BaseEvent<HTMLElementEventMap[K]>;
|
||||
(element: EventHandler, type: string, useCapture?: boolean): BaseEvent<any>;
|
||||
}
|
||||
|
||||
export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => {
|
||||
const fn = e => emitter.fire(e);
|
||||
const emitter = new Emitter<any>({
|
||||
const fn = (e: Event) => emitter.fire(e);
|
||||
const emitter = new Emitter<Event>({
|
||||
onFirstListenerAdd: () => {
|
||||
element.addEventListener(type, fn, useCapture);
|
||||
},
|
||||
|
@ -27,12 +27,12 @@ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapt
|
|||
};
|
||||
|
||||
export interface CancellableEvent {
|
||||
preventDefault();
|
||||
stopPropagation();
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export function stop<T extends CancellableEvent>(event: Event<T>): Event<T> {
|
||||
return Event.map(event, e => {
|
||||
export function stop<T extends CancellableEvent>(event: BaseEvent<T>): BaseEvent<T> {
|
||||
return BaseEvent.map(event, e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return e;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.784 8L13 11.217 11.215 13 8.001 9.786 4.785 13 3 11.216l3.214-3.215L3 4.785 4.784 3 8 6.216 11.216 3 13 4.785 9.784 8.001z" fill="#C5C5C5"/></svg>
|
После Ширина: | Высота: | Размер: 253 B |
|
@ -0,0 +1 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.784 8L13 11.217 11.215 13 8.001 9.786 4.785 13 3 11.216l3.214-3.215L3 4.785 4.784 3 8 6.216 11.216 3 13 4.785 9.784 8.001z" fill="#424242"/></svg>
|
После Ширина: | Высота: | Размер: 253 B |
|
@ -0,0 +1,154 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/** Dialog: Modal Block */
|
||||
.monaco-workbench .dialog-modal-block {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left:0;
|
||||
top:0;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
/** Dialog: Container */
|
||||
.monaco-workbench .dialog-box {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
top: 200px;
|
||||
left: 50%;
|
||||
margin-left: -250px;
|
||||
width: 500px;
|
||||
min-height: 75px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/** Dialog: Title Actions Row */
|
||||
.monaco-workbench .dialog-box .dialog-toolbar-row {
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.monaco-workbench .dialog-box .action-label {
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
background-size: 16px;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
|
||||
.monaco-workbench .dialog-box .dialog-close-action {
|
||||
background: url('close.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .dialog-box .dialog-close-action,
|
||||
.hc-black .monaco-workbench .dialog-box .dialog-close-action {
|
||||
background: url('close-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
/** Dialog: Message Row */
|
||||
.monaco-workbench .dialog-box .dialog-message-row {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: 10px 15px 20px;
|
||||
}
|
||||
|
||||
.monaco-workbench .dialog-box .dialog-message-row .dialog-icon {
|
||||
flex: 0 0 30px;
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info {
|
||||
background-image: url('info.svg');
|
||||
}
|
||||
|
||||
.vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-warning {
|
||||
background-image: url('warning.svg');
|
||||
}
|
||||
|
||||
.vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-error {
|
||||
background-image: url('error.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info,
|
||||
.hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info {
|
||||
background-image: url('info-inverse.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-warning,
|
||||
.hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-warning {
|
||||
background-image: url('warning-inverse.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-error,
|
||||
.hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-error {
|
||||
background-image: url('error-inverse.svg');
|
||||
}
|
||||
|
||||
/** Dialog: Message Container */
|
||||
.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 20px;
|
||||
user-select: text;
|
||||
word-wrap: break-word; /* never overflow long words, but break to next line */
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/** Dialog: Message */
|
||||
.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container .dialog-message {
|
||||
line-height: 22px;
|
||||
font-size: 18px;
|
||||
flex: 1; /* let the message always grow */
|
||||
white-space: normal;
|
||||
word-wrap: break-word; /* never overflow long words, but break to next line */
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/** Dialog: Details */
|
||||
.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container .dialog-message-detail {
|
||||
line-height: 22px;
|
||||
flex: 1; /* let the message always grow */
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container .dialog-message a:focus {
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
/** Dialog: Buttons Row */
|
||||
.monaco-workbench .dialog-box > .dialog-buttons-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 1px;
|
||||
overflow: hidden; /* buttons row should never overflow */
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-workbench .dialog-box > .dialog-buttons-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/** Dialog: Buttons */
|
||||
.monaco-workbench .monaco-workbench .dialog-box > .dialog-buttons-row > .dialog-buttons {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-workbench .dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button {
|
||||
max-width: fit-content;
|
||||
padding: 5px 10px;
|
||||
margin: 4px 5px; /* allows button focus outline to be visible */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dialog';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { $, hide, show, EventHelper, clearNode, removeClasses, addClass, removeNode } from 'vs/base/browser/dom';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ButtonGroup, IButtonStyles } from 'vs/base/browser/ui/button/button';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
|
||||
export interface IDialogOptions {
|
||||
cancelId?: number;
|
||||
detail?: string;
|
||||
type?: 'none' | 'info' | 'error' | 'question' | 'warning';
|
||||
}
|
||||
|
||||
export interface IDialogStyles extends IButtonStyles {
|
||||
dialogForeground?: Color;
|
||||
dialogBackground?: Color;
|
||||
dialogShadow?: Color;
|
||||
}
|
||||
|
||||
export class Dialog extends Disposable {
|
||||
private element: HTMLElement | undefined;
|
||||
private modal: HTMLElement | undefined;
|
||||
private buttonsContainer: HTMLElement | undefined;
|
||||
private iconElement: HTMLElement | undefined;
|
||||
private toolbarContainer: HTMLElement | undefined;
|
||||
private buttonGroup: ButtonGroup | undefined;
|
||||
private styles: IDialogStyles | undefined;
|
||||
|
||||
constructor(private container: HTMLElement, private message: string, private buttons: string[], private options: IDialogOptions) {
|
||||
super();
|
||||
this.modal = this.container.appendChild($('.dialog-modal-block'));
|
||||
this.element = this.modal.appendChild($('.dialog-box'));
|
||||
hide(this.element);
|
||||
|
||||
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
|
||||
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
|
||||
|
||||
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
|
||||
this.iconElement = messageRowElement.appendChild($('.dialog-icon'));
|
||||
const messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
|
||||
const messageElement = messageContainer.appendChild($('.dialog-message'));
|
||||
messageElement.innerText = this.message;
|
||||
if (this.options.detail) {
|
||||
const messageDetailElement = messageContainer.appendChild($('.dialog-message-detail'));
|
||||
messageDetailElement.innerText = this.options.detail;
|
||||
}
|
||||
|
||||
const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row'));
|
||||
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
|
||||
}
|
||||
|
||||
async show(): Promise<number> {
|
||||
return new Promise<number>((resolve) => {
|
||||
if (!this.element || !this.buttonsContainer || !this.iconElement || !this.toolbarContainer) {
|
||||
resolve(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal) {
|
||||
this._register(domEvent(this.modal, 'mousedown')(e => {
|
||||
// Used to stop focusing of modal with mouse
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
}
|
||||
|
||||
clearNode(this.buttonsContainer);
|
||||
|
||||
let focusedButton = 0;
|
||||
this.buttonGroup = new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true });
|
||||
this.buttonGroup.buttons.forEach((button, index) => {
|
||||
button.label = mnemonicButtonLabel(this.buttons[index], true);
|
||||
|
||||
this._register(button.onDidClick(e => {
|
||||
EventHelper.stop(e);
|
||||
resolve(index);
|
||||
}));
|
||||
});
|
||||
|
||||
this._register(domEvent(this.element, 'keydown', true)((e: KeyboardEvent) => {
|
||||
const evt = new StandardKeyboardEvent(e);
|
||||
if (evt.equals(KeyCode.Enter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.buttonGroup) {
|
||||
if ((evt.shiftKey && evt.equals(KeyCode.Tab)) || evt.equals(KeyCode.LeftArrow)) {
|
||||
focusedButton = focusedButton + this.buttonGroup.buttons.length - 1;
|
||||
focusedButton = focusedButton % this.buttonGroup.buttons.length;
|
||||
this.buttonGroup.buttons[focusedButton].focus();
|
||||
} else if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
|
||||
focusedButton++;
|
||||
focusedButton = focusedButton % this.buttonGroup.buttons.length;
|
||||
this.buttonGroup.buttons[focusedButton].focus();
|
||||
}
|
||||
}
|
||||
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
|
||||
this._register(domEvent(this.element, 'keyup', true)((e: KeyboardEvent) => {
|
||||
EventHelper.stop(e, true);
|
||||
const evt = new StandardKeyboardEvent(e);
|
||||
|
||||
if (evt.equals(KeyCode.Escape)) {
|
||||
resolve(this.options.cancelId || 0);
|
||||
}
|
||||
}));
|
||||
|
||||
removeClasses(this.iconElement, 'icon-error', 'icon-warning', 'icon-info');
|
||||
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
addClass(this.iconElement, 'icon-error');
|
||||
break;
|
||||
case 'warning':
|
||||
addClass(this.iconElement, 'icon-warning');
|
||||
break;
|
||||
case 'none':
|
||||
case 'info':
|
||||
case 'question':
|
||||
default:
|
||||
addClass(this.iconElement, 'icon-info');
|
||||
break;
|
||||
}
|
||||
|
||||
const actionBar = new ActionBar(this.toolbarContainer, {});
|
||||
|
||||
const action = new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), 'dialog-close-action', true, () => {
|
||||
resolve(this.options.cancelId || 0);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
actionBar.push(action, { icon: true, label: false, });
|
||||
|
||||
this.applyStyles();
|
||||
|
||||
show(this.element);
|
||||
|
||||
// Focus first element
|
||||
this.buttonGroup.buttons[focusedButton].focus();
|
||||
});
|
||||
}
|
||||
|
||||
private applyStyles() {
|
||||
if (this.styles) {
|
||||
const style = this.styles;
|
||||
|
||||
const fgColor = style.dialogForeground ? `${style.dialogForeground}` : null;
|
||||
const bgColor = style.dialogBackground ? `${style.dialogBackground}` : null;
|
||||
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : null;
|
||||
|
||||
if (this.element) {
|
||||
this.element.style.color = fgColor;
|
||||
this.element.style.backgroundColor = bgColor;
|
||||
this.element.style.boxShadow = shadowColor;
|
||||
|
||||
if (this.buttonGroup) {
|
||||
this.buttonGroup.buttons.forEach(button => button.style(style));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style(style: IDialogStyles): void {
|
||||
this.styles = style;
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
if (this.modal) {
|
||||
removeNode(this.modal);
|
||||
this.modal = undefined;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#CACACC;}
|
||||
.st1{fill:#E51400;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
.st3{fill:#F6F6F6;fill-opacity:0;}
|
||||
.st4{fill:#1A1A1A;}
|
||||
</style>
|
||||
<path id="outline_2_" class="st0" d="M-169.7-71.2c0,1.4-1.2,2.6-2.6,2.6c-1.4,0-2.6-1.2-2.6-2.6s1.2-2.6,2.6-2.6
|
||||
C-170.9-73.8-169.7-72.6-169.7-71.2z"/>
|
||||
<path id="iconBg_2_" class="st1" d="M-172.3-73.5c-1.3,0-2.3,1-2.3,2.3s1,2.3,2.3,2.3s2.3-1,2.3-2.3S-171.1-73.5-172.3-73.5z
|
||||
M-170.9-70.2l-0.5,0.5l-1-1l-1,1l-0.5-0.5l1-1l-1-1l0.5-0.5l1,1l1-1l0.5,0.5l-1,1L-170.9-70.2z"/>
|
||||
<g id="iconFg_2_">
|
||||
<path class="st2" d="M-171.9-71.2l1,1l-0.5,0.5l-1-1l-1,1l-0.5-0.5l1-1l-1-1l0.5-0.5l1,1l1-1l0.5,0.5L-171.9-71.2z"/>
|
||||
</g>
|
||||
<path id="canvas_1_" class="st3" d="M16,16H0V0h16V16z"/>
|
||||
<path id="outline_1_" class="st4" d="M16,8c0,4.4-3.6,8-8,8s-8-3.6-8-8s3.6-8,8-8S16,3.6,16,8z"/>
|
||||
<path id="iconBg_1_" class="st1" d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7s7-3.1,7-7S11.9,1,8,1z M12.4,11L11,12.4l-3-3l-3,3L3.6,11
|
||||
l3-3l-3-3L5,3.6l3,3l3-3L12.4,5l-3,3L12.4,11z"/>
|
||||
<g id="iconFg_1_">
|
||||
<path class="st2" d="M9.4,8l3,3L11,12.4l-3-3l-3,3L3.6,11l3-3l-3-3L5,3.6l3,3l3-3L12.4,5L9.4,8z"/>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.4 KiB |
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#CACACC;}
|
||||
.st1{fill:#E51400;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
.st3{fill:#F6F6F6;fill-opacity:0;}
|
||||
</style>
|
||||
<path id="outline" class="st0" d="M16,8c0,4.4-3.6,8-8,8s-8-3.6-8-8s3.6-8,8-8S16,3.6,16,8z"/>
|
||||
<path id="iconBg" class="st1" d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7s7-3.1,7-7S11.9,1,8,1z M12.4,11L11,12.4l-3-3l-3,3L3.6,11
|
||||
l3-3l-3-3L5,3.6l3,3l3-3L12.4,5l-3,3L12.4,11z"/>
|
||||
<g id="iconFg">
|
||||
<path class="st2" d="M9.4,8l3,3L11,12.4l-3-3l-3,3L3.6,11l3-3l-3-3L5,3.6l3,3l3-3L12.4,5L9.4,8z"/>
|
||||
</g>
|
||||
<path id="canvas_4_" class="st3" d="M16,16H0V0h16V16z"/>
|
||||
<path id="outline_2_" class="st0" d="M-192.7-71.2c0,1.4-1.2,2.6-2.6,2.6s-2.6-1.2-2.6-2.6s1.2-2.6,2.6-2.6S-192.7-72.6-192.7-71.2z
|
||||
"/>
|
||||
<path id="iconBg_2_" class="st1" d="M-195.4-73.5c-1.3,0-2.3,1-2.3,2.3s1,2.3,2.3,2.3c1.3,0,2.3-1,2.3-2.3S-194.1-73.5-195.4-73.5z
|
||||
M-193.9-70.2l-0.5,0.5l-1-1l-1,1l-0.5-0.5l1-1l-1-1l0.5-0.5l1,1l1-1l0.5,0.5l-1,1L-193.9-70.2z"/>
|
||||
<g id="iconFg_2_">
|
||||
<path class="st2" d="M-194.9-71.2l1,1l-0.5,0.5l-1-1l-1,1l-0.5-0.5l1-1l-1-1l0.5-0.5l1,1l1-1l0.5,0.5L-194.9-71.2z"/>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.4 KiB |
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F6F6F6;fill-opacity:0;}
|
||||
.st1{fill:#1A1A1A;}
|
||||
.st2{fill:#1BA1E2;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path id="canvas_1_" class="st0" d="M16,16H0V0h16V16z"/>
|
||||
<path id="outline_1_" class="st1" d="M0,8c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8S0,12.4,0,8z"/>
|
||||
<path id="iconBg_1_" class="st2" d="M8,1C4.1,1,1,4.1,1,8s3.1,7,7,7s7-3.1,7-7S11.9,1,8,1z M9,13H7V6h2V13z M9,5H7V3h2V5z"/>
|
||||
<g id="iconFg_1_">
|
||||
<path class="st3" d="M7,6h2v7H7V6z M7,5h2V3H7V5z"/>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 834 B |
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F6F6F6;fill-opacity:0;}
|
||||
.st1{fill:#CACACC;}
|
||||
.st2{fill:#1BA1E2;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path id="canvas" class="st0" d="M16,16H0V0h16V16z"/>
|
||||
<path id="outline" class="st1" d="M0,8c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8S0,12.4,0,8z"/>
|
||||
<path id="iconBg" class="st2" d="M8,1C4.1,1,1,4.1,1,8s3.1,7,7,7s7-3.1,7-7S11.8,1,8,1z M9,13H7V6h2V13z M9,5H7V3h2V5z"/>
|
||||
<g id="iconFg">
|
||||
<path class="st3" d="M7,6h2v7H7V6z M7,5h2V3H7V5z"/>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 822 B |
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F6F6F6;fill-opacity:0;}
|
||||
.st1{fill:#1A1A1A;}
|
||||
.st2{fill:#FFCC00;}
|
||||
</style>
|
||||
<title>StatusWarning_16x</title>
|
||||
<path class="st0" d="M16,0v16H0V0H16z"/>
|
||||
<path class="st1" d="M16,14l-2,2H2l-2-2L7,0h2L16,14z"/>
|
||||
<path class="st2" d="M8.4,1H7.6L1.2,13.8L2.5,15h11l1.3-1.2L8.4,1z M9,13H7v-2h2V13z M9,10H7V5h2V10z"/>
|
||||
<path d="M7,11h2v2H7V11z M7,5v5h2V5H7z"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 737 B |
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F6F6F6;fill-opacity:0;}
|
||||
.st1{fill:#CACACC;}
|
||||
.st2{fill:#FFCC00;}
|
||||
</style>
|
||||
<title>StatusWarning_16x</title>
|
||||
<path class="st0" d="M16,0v16H0V0H16z"/>
|
||||
<path class="st1" d="M16,14l-2,2H2l-2-2L7,0h2L16,14z"/>
|
||||
<path class="st2" d="M8.4,1H7.6L1.2,13.8L2.5,15h11l1.3-1.2L8.4,1z M9,13H7v-2h2V13z M9,10H7V5h2V10z"/>
|
||||
<path d="M7,11h2v2H7V11z M7,5v5h2V5H7z"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 737 B |
|
@ -7,7 +7,7 @@ import 'vs/css!./list';
|
|||
import { localize } from 'vs/nls';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
import { range, firstIndex } from 'vs/base/common/arrays';
|
||||
import { range, firstIndex, binarySearch } from 'vs/base/common/arrays';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
@ -107,10 +107,8 @@ class TraitRenderer<T> implements IListRenderer<T, ITraitTemplateData>
|
|||
|
||||
class Trait<T> implements ISpliceable<boolean>, IDisposable {
|
||||
|
||||
/**
|
||||
* Sorted indexes which have this trait.
|
||||
*/
|
||||
private indexes: number[];
|
||||
private indexes: number[] = [];
|
||||
private sortedIndexes: number[] = [];
|
||||
|
||||
private _onChange = new Emitter<ITraitChangeEvent>();
|
||||
get onChange(): Event<ITraitChangeEvent> { return this._onChange.event; }
|
||||
|
@ -122,21 +120,19 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
|
|||
return new TraitRenderer<T>(this);
|
||||
}
|
||||
|
||||
constructor(private _trait: string) {
|
||||
this.indexes = [];
|
||||
}
|
||||
constructor(private _trait: string) { }
|
||||
|
||||
splice(start: number, deleteCount: number, elements: boolean[]): void {
|
||||
const diff = elements.length - deleteCount;
|
||||
const end = start + deleteCount;
|
||||
const indexes = [
|
||||
...this.indexes.filter(i => i < start),
|
||||
...this.sortedIndexes.filter(i => i < start),
|
||||
...elements.map((hasTrait, i) => hasTrait ? i + start : -1).filter(i => i !== -1),
|
||||
...this.indexes.filter(i => i >= end).map(i => i + diff)
|
||||
...this.sortedIndexes.filter(i => i >= end).map(i => i + diff)
|
||||
];
|
||||
|
||||
this.renderer.splice(start, deleteCount, elements.length);
|
||||
this.set(indexes);
|
||||
this._set(indexes, indexes);
|
||||
}
|
||||
|
||||
renderIndex(index: number, container: HTMLElement): void {
|
||||
|
@ -154,10 +150,17 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
|
|||
* @return The old indexes which had this trait.
|
||||
*/
|
||||
set(indexes: number[], browserEvent?: UIEvent): number[] {
|
||||
const result = this.indexes;
|
||||
this.indexes = indexes;
|
||||
return this._set(indexes, [...indexes].sort(numericSort), browserEvent);
|
||||
}
|
||||
|
||||
const toRender = disjunction(result, indexes);
|
||||
private _set(indexes: number[], sortedIndexes: number[], browserEvent?: UIEvent): number[] {
|
||||
const result = this.indexes;
|
||||
const sortedResult = this.sortedIndexes;
|
||||
|
||||
this.indexes = indexes;
|
||||
this.sortedIndexes = sortedIndexes;
|
||||
|
||||
const toRender = disjunction(sortedResult, indexes);
|
||||
this.renderer.renderIndexes(toRender);
|
||||
|
||||
this._onChange.fire({ indexes, browserEvent });
|
||||
|
@ -169,7 +172,7 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
|
|||
}
|
||||
|
||||
contains(index: number): boolean {
|
||||
return this.indexes.some(i => i === index);
|
||||
return binarySearch(this.sortedIndexes, index, numericSort) >= 0;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
@ -1334,7 +1337,6 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
|||
}
|
||||
}
|
||||
|
||||
indexes = indexes.sort(numericSort);
|
||||
this.selection.set(indexes, browserEvent);
|
||||
}
|
||||
|
||||
|
@ -1353,7 +1355,6 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
|||
}
|
||||
}
|
||||
|
||||
indexes = indexes.sort(numericSort);
|
||||
this.focus.set(indexes, browserEvent);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
|
|||
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd';
|
||||
import { range, equals } from 'vs/base/common/arrays';
|
||||
import { range, equals, distinctES6 } from 'vs/base/common/arrays';
|
||||
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
|
||||
|
@ -924,7 +924,7 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
|
|||
const additionalSelection: number[] = [];
|
||||
|
||||
elements.forEach((node, index) => {
|
||||
if (this.selectionTrait.has(node)) {
|
||||
if (this.focusTrait.has(node)) {
|
||||
additionalFocus.push(start + index);
|
||||
}
|
||||
|
||||
|
@ -934,11 +934,11 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
|
|||
});
|
||||
|
||||
if (additionalFocus.length > 0) {
|
||||
super.setFocus([...super.getFocus(), ...additionalFocus]);
|
||||
super.setFocus(distinctES6([...super.getFocus(), ...additionalFocus]));
|
||||
}
|
||||
|
||||
if (additionalSelection.length > 0) {
|
||||
super.setSelection([...super.getSelection(), ...additionalSelection]);
|
||||
super.setSelection(distinctES6([...super.getSelection(), ...additionalSelection]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -364,6 +364,18 @@ export function distinct<T>(array: ReadonlyArray<T>, keyFn?: (t: T) => string):
|
|||
});
|
||||
}
|
||||
|
||||
export function distinctES6<T>(array: ReadonlyArray<T>): T[] {
|
||||
const seen = new Set<T>();
|
||||
return array.filter(element => {
|
||||
if (seen.has(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(element);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
|
||||
const seen: { [key: string]: boolean; } = Object.create(null);
|
||||
|
||||
|
|
|
@ -765,3 +765,19 @@ export class IdleValue<T> {
|
|||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await task();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
await timeout(delay);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(lastError);
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare var Buffer: any;
|
||||
const hasBuffer = (typeof Buffer !== 'undefined');
|
||||
|
||||
export class VSBuffer {
|
||||
|
||||
public static alloc(byteLength: number): VSBuffer {
|
||||
if (hasBuffer) {
|
||||
return new VSBuffer(Buffer.allocUnsafe(byteLength));
|
||||
} else {
|
||||
return new VSBuffer(new Uint8Array(byteLength));
|
||||
}
|
||||
}
|
||||
|
||||
public static wrap(actual: Uint8Array): VSBuffer {
|
||||
return new VSBuffer(actual);
|
||||
}
|
||||
|
||||
public static fromString(source: string): VSBuffer {
|
||||
return new VSBuffer(Buffer.from(source));
|
||||
}
|
||||
|
||||
public static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer {
|
||||
if (typeof totalLength === 'undefined') {
|
||||
totalLength = 0;
|
||||
for (let i = 0, len = buffers.length; i < len; i++) {
|
||||
totalLength += buffers[i].byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
const ret = VSBuffer.alloc(totalLength);
|
||||
let offset = 0;
|
||||
for (let i = 0, len = buffers.length; i < len; i++) {
|
||||
const element = buffers[i];
|
||||
ret.set(element, offset);
|
||||
offset += element.byteLength;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public readonly buffer: Uint8Array;
|
||||
public readonly byteLength: number;
|
||||
|
||||
private constructor(buffer: Uint8Array) {
|
||||
this.buffer = buffer;
|
||||
this.byteLength = this.buffer.byteLength;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.buffer.toString();
|
||||
}
|
||||
|
||||
public slice(start?: number, end?: number): VSBuffer {
|
||||
return new VSBuffer(this.buffer.slice(start, end));
|
||||
}
|
||||
|
||||
public set(array: VSBuffer, offset?: number): void {
|
||||
this.buffer.set(array.buffer, offset);
|
||||
}
|
||||
|
||||
public readUint32BE(offset: number): number {
|
||||
return readUint32BE(this.buffer, offset);
|
||||
}
|
||||
|
||||
public writeUint32BE(value: number, offset: number): void {
|
||||
writeUint32BE(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
public readUint8(offset: number): number {
|
||||
return readUint8(this.buffer, offset);
|
||||
}
|
||||
|
||||
public writeUint8(value: number, offset: number): void {
|
||||
writeUint8(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function readUint32BE(source: Uint8Array, offset: number): number {
|
||||
return (
|
||||
source[offset] * 2 ** 24
|
||||
+ source[offset + 1] * 2 ** 16
|
||||
+ source[offset + 2] * 2 ** 8
|
||||
+ source[offset + 3]
|
||||
);
|
||||
}
|
||||
|
||||
function writeUint32BE(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset + 3] = value;
|
||||
value = value >>> 8;
|
||||
destination[offset + 2] = value;
|
||||
value = value >>> 8;
|
||||
destination[offset + 1] = value;
|
||||
value = value >>> 8;
|
||||
destination[offset] = value;
|
||||
}
|
||||
|
||||
function readUint8(source: Uint8Array, offset: number): number {
|
||||
return source[offset];
|
||||
}
|
||||
|
||||
function writeUint8(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset] = value;
|
||||
}
|
|
@ -369,8 +369,8 @@ export function mnemonicMenuLabel(label: string, forceDisableMnemonics?: boolean
|
|||
* - Linux: Supported via _ character (replace && with _)
|
||||
* - macOS: Unsupported (replace && with empty string)
|
||||
*/
|
||||
export function mnemonicButtonLabel(label: string): string {
|
||||
if (isMacintosh) {
|
||||
export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: boolean): string {
|
||||
if (isMacintosh || forceDisableMnemonics) {
|
||||
return label.replace(/\(&&\w\)|&&/g, '');
|
||||
}
|
||||
|
||||
|
|
|
@ -371,10 +371,10 @@ export interface IWriteFileOptions {
|
|||
}
|
||||
|
||||
let canFlush = true;
|
||||
export function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream, options: IWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
export function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
options = ensureOptions(options);
|
||||
|
||||
if (typeof data === 'string' || Buffer.isBuffer(data)) {
|
||||
if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
||||
doWriteFileAndFlush(path, data, options, callback);
|
||||
} else {
|
||||
doWriteFileStreamAndFlush(path, data, options, callback);
|
||||
|
@ -472,9 +472,9 @@ function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream,
|
|||
// not in some cache.
|
||||
//
|
||||
// See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194
|
||||
function doWriteFileAndFlush(path: string, data: string | Buffer, options: IWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
if (options.encoding) {
|
||||
data = encode(data, options.encoding.charset, { addBOM: options.encoding.addBOM });
|
||||
data = encode(data instanceof Uint8Array ? Buffer.from(data) : data, options.encoding.charset, { addBOM: options.encoding.addBOM });
|
||||
}
|
||||
|
||||
if (!canFlush) {
|
||||
|
|
|
@ -694,7 +694,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
|||
private prepare(connection: IDatabaseConnection, sql: string, runCallback: (stmt: Statement) => void, errorDetails: () => string): void {
|
||||
const stmt = connection.db.prepare(sql);
|
||||
|
||||
const statementErrorListener = error => {
|
||||
const statementErrorListener = (error: Error) => {
|
||||
this.handleSQLiteError(connection, error, `[storage ${this.name}] prepare(): ${error} (${sql}). Details: ${errorDetails()}`);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,774 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
||||
declare var process: any;
|
||||
|
||||
export interface ISocket {
|
||||
onData(listener: (e: VSBuffer) => void): IDisposable;
|
||||
onClose(listener: () => void): IDisposable;
|
||||
onEnd(listener: () => void): IDisposable;
|
||||
write(buffer: VSBuffer): void;
|
||||
end(): void;
|
||||
}
|
||||
|
||||
let emptyBuffer: VSBuffer | null = null;
|
||||
function getEmptyBuffer(): VSBuffer {
|
||||
if (!emptyBuffer) {
|
||||
emptyBuffer = VSBuffer.alloc(0);
|
||||
}
|
||||
return emptyBuffer;
|
||||
}
|
||||
|
||||
class ChunkStream {
|
||||
|
||||
private _chunks: VSBuffer[];
|
||||
private _totalLength: number;
|
||||
|
||||
public get byteLength() {
|
||||
return this._totalLength;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._chunks = [];
|
||||
this._totalLength = 0;
|
||||
}
|
||||
|
||||
public acceptChunk(buff: VSBuffer) {
|
||||
this._chunks.push(buff);
|
||||
this._totalLength += buff.byteLength;
|
||||
}
|
||||
|
||||
public read(byteCount: number): VSBuffer {
|
||||
if (byteCount === 0) {
|
||||
return getEmptyBuffer();
|
||||
}
|
||||
|
||||
if (byteCount > this._totalLength) {
|
||||
throw new Error(`Cannot read so many bytes!`);
|
||||
}
|
||||
|
||||
if (this._chunks[0].byteLength === byteCount) {
|
||||
// super fast path, precisely first chunk must be returned
|
||||
const result = this._chunks.shift()!;
|
||||
this._totalLength -= byteCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (this._chunks[0].byteLength > byteCount) {
|
||||
// fast path, the reading is entirely within the first chunk
|
||||
const result = this._chunks[0].slice(0, byteCount);
|
||||
this._chunks[0] = this._chunks[0].slice(byteCount);
|
||||
this._totalLength -= byteCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
let result = VSBuffer.alloc(byteCount);
|
||||
let resultOffset = 0;
|
||||
while (byteCount > 0) {
|
||||
const chunk = this._chunks[0];
|
||||
if (chunk.byteLength > byteCount) {
|
||||
// this chunk will survive
|
||||
this._chunks[0] = chunk.slice(byteCount);
|
||||
|
||||
const chunkPart = chunk.slice(0, byteCount);
|
||||
result.set(chunkPart, resultOffset);
|
||||
resultOffset += byteCount;
|
||||
this._totalLength -= byteCount;
|
||||
byteCount -= byteCount;
|
||||
} else {
|
||||
// this chunk will be entirely read
|
||||
this._chunks.shift();
|
||||
|
||||
result.set(chunk, resultOffset);
|
||||
resultOffset += chunk.byteLength;
|
||||
this._totalLength -= chunk.byteLength;
|
||||
byteCount -= chunk.byteLength;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const enum ProtocolMessageType {
|
||||
None = 0,
|
||||
Regular = 1,
|
||||
Control = 2,
|
||||
Ack = 3,
|
||||
KeepAlive = 4
|
||||
}
|
||||
|
||||
export const enum ProtocolConstants {
|
||||
HeaderLength = 13,
|
||||
/**
|
||||
* Send an Acknowledge message at most 2 seconds later...
|
||||
*/
|
||||
AcknowledgeTime = 2000, // 2 seconds
|
||||
/**
|
||||
* If there is a message that has been unacknowledged for 10 seconds, consider the connection closed...
|
||||
*/
|
||||
AcknowledgeTimeoutTime = 10000, // 10 seconds
|
||||
/**
|
||||
* Send at least a message every 30s for keep alive reasons.
|
||||
*/
|
||||
KeepAliveTime = 30000, // 30 seconds
|
||||
/**
|
||||
* If there is no message received for 60 seconds, consider the connection closed...
|
||||
*/
|
||||
KeepAliveTimeoutTime = 60000, // 60 seconds
|
||||
/**
|
||||
* If there is no reconnection within this time-frame, consider the connection permanently closed...
|
||||
*/
|
||||
ReconnectionGraceTime = 60 * 60 * 1000, // 1hr
|
||||
}
|
||||
|
||||
class ProtocolMessage {
|
||||
|
||||
public writtenTime: number;
|
||||
|
||||
constructor(
|
||||
public readonly type: ProtocolMessageType,
|
||||
public readonly id: number,
|
||||
public readonly ack: number,
|
||||
public readonly data: VSBuffer
|
||||
) {
|
||||
this.writtenTime = 0;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.data.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
class ProtocolReader extends Disposable {
|
||||
|
||||
private readonly _socket: ISocket;
|
||||
private _isDisposed: boolean;
|
||||
private readonly _incomingData: ChunkStream;
|
||||
public lastReadTime: number;
|
||||
|
||||
private readonly _onMessage = new Emitter<ProtocolMessage>();
|
||||
public readonly onMessage: Event<ProtocolMessage> = this._onMessage.event;
|
||||
|
||||
private readonly _state = {
|
||||
readHead: true,
|
||||
readLen: ProtocolConstants.HeaderLength,
|
||||
messageType: ProtocolMessageType.None,
|
||||
id: 0,
|
||||
ack: 0
|
||||
};
|
||||
|
||||
constructor(socket: ISocket) {
|
||||
super();
|
||||
this._socket = socket;
|
||||
this._isDisposed = false;
|
||||
this._incomingData = new ChunkStream();
|
||||
this._register(this._socket.onData(data => this.acceptChunk(data)));
|
||||
this.lastReadTime = Date.now();
|
||||
}
|
||||
|
||||
public acceptChunk(data: VSBuffer | null): void {
|
||||
if (!data || data.byteLength === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastReadTime = Date.now();
|
||||
|
||||
this._incomingData.acceptChunk(data);
|
||||
|
||||
while (this._incomingData.byteLength >= this._state.readLen) {
|
||||
|
||||
const buff = this._incomingData.read(this._state.readLen);
|
||||
|
||||
if (this._state.readHead) {
|
||||
// buff is the header
|
||||
|
||||
// save new state => next time will read the body
|
||||
this._state.readHead = false;
|
||||
this._state.readLen = buff.readUint32BE(9);
|
||||
this._state.messageType = <ProtocolMessageType>buff.readUint8(0);
|
||||
this._state.id = buff.readUint32BE(1);
|
||||
this._state.ack = buff.readUint32BE(5);
|
||||
} else {
|
||||
// buff is the body
|
||||
const messageType = this._state.messageType;
|
||||
const id = this._state.id;
|
||||
const ack = this._state.ack;
|
||||
|
||||
// save new state => next time will read the header
|
||||
this._state.readHead = true;
|
||||
this._state.readLen = ProtocolConstants.HeaderLength;
|
||||
this._state.messageType = ProtocolMessageType.None;
|
||||
this._state.id = 0;
|
||||
this._state.ack = 0;
|
||||
|
||||
this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff));
|
||||
|
||||
if (this._isDisposed) {
|
||||
// check if an event listener lead to our disposal
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readEntireBuffer(): VSBuffer {
|
||||
return this._incomingData.read(this._incomingData.byteLength);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ProtocolWriter {
|
||||
|
||||
private _isDisposed: boolean;
|
||||
private readonly _socket: ISocket;
|
||||
private _data: VSBuffer[];
|
||||
private _totalLength: number;
|
||||
public lastWriteTime: number;
|
||||
|
||||
constructor(socket: ISocket) {
|
||||
this._isDisposed = false;
|
||||
this._socket = socket;
|
||||
this._data = [];
|
||||
this._totalLength = 0;
|
||||
this.lastWriteTime = 0;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.flush();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
// flush
|
||||
this._writeNow();
|
||||
}
|
||||
|
||||
public write(msg: ProtocolMessage) {
|
||||
if (this._isDisposed) {
|
||||
console.warn(`Cannot write message in a disposed ProtocolWriter`);
|
||||
console.warn(msg);
|
||||
return;
|
||||
}
|
||||
msg.writtenTime = Date.now();
|
||||
this.lastWriteTime = Date.now();
|
||||
const header = VSBuffer.alloc(ProtocolConstants.HeaderLength);
|
||||
header.writeUint8(msg.type, 0);
|
||||
header.writeUint32BE(msg.id, 1);
|
||||
header.writeUint32BE(msg.ack, 5);
|
||||
header.writeUint32BE(msg.data.byteLength, 9);
|
||||
this._writeSoon(header, msg.data);
|
||||
}
|
||||
|
||||
private _bufferAdd(head: VSBuffer, body: VSBuffer): boolean {
|
||||
const wasEmpty = this._totalLength === 0;
|
||||
this._data.push(head, body);
|
||||
this._totalLength += head.byteLength + body.byteLength;
|
||||
return wasEmpty;
|
||||
}
|
||||
|
||||
private _bufferTake(): VSBuffer {
|
||||
const ret = VSBuffer.concat(this._data, this._totalLength);
|
||||
this._data.length = 0;
|
||||
this._totalLength = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
private _writeSoon(header: VSBuffer, data: VSBuffer): void {
|
||||
if (this._bufferAdd(header, data)) {
|
||||
platform.setImmediate(() => {
|
||||
this._writeNow();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _writeNow(): void {
|
||||
if (this._totalLength === 0) {
|
||||
return;
|
||||
}
|
||||
this._socket.write(this._bufferTake());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A message has the following format:
|
||||
* ```
|
||||
* /-------------------------------|------\
|
||||
* | HEADER | |
|
||||
* |-------------------------------| DATA |
|
||||
* | TYPE | ID | ACK | DATA_LENGTH | |
|
||||
* \-------------------------------|------/
|
||||
* ```
|
||||
* The header is 9 bytes and consists of:
|
||||
* - TYPE is 1 byte (ProtocolMessageType) - the message type
|
||||
* - ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored)
|
||||
* - ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored)
|
||||
* - DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA
|
||||
*
|
||||
* Only Regular messages are counted, other messages are not counted, nor acknowledged.
|
||||
*/
|
||||
export class Protocol extends Disposable implements IMessagePassingProtocol {
|
||||
|
||||
private _socket: ISocket;
|
||||
private _socketWriter: ProtocolWriter;
|
||||
private _socketReader: ProtocolReader;
|
||||
|
||||
private _onMessage = new Emitter<VSBuffer>();
|
||||
readonly onMessage: Event<VSBuffer> = this._onMessage.event;
|
||||
|
||||
private _onClose = new Emitter<void>();
|
||||
readonly onClose: Event<void> = this._onClose.event;
|
||||
|
||||
constructor(socket: ISocket) {
|
||||
super();
|
||||
this._socket = socket;
|
||||
this._socketWriter = this._register(new ProtocolWriter(this._socket));
|
||||
this._socketReader = this._register(new ProtocolReader(this._socket));
|
||||
|
||||
this._register(this._socketReader.onMessage((msg) => {
|
||||
if (msg.type === ProtocolMessageType.Regular) {
|
||||
this._onMessage.fire(msg.data);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._socket.onClose(() => this._onClose.fire()));
|
||||
}
|
||||
|
||||
getSocket(): ISocket {
|
||||
return this._socket;
|
||||
}
|
||||
|
||||
send(buffer: VSBuffer): void {
|
||||
this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.Regular, 0, 0, buffer));
|
||||
}
|
||||
}
|
||||
|
||||
export class Client<TContext = string> extends IPCClient<TContext> {
|
||||
|
||||
static fromSocket<TContext = string>(socket: ISocket, id: TContext): Client<TContext> {
|
||||
return new Client(new Protocol(socket), id);
|
||||
}
|
||||
|
||||
get onClose(): Event<void> { return this.protocol.onClose; }
|
||||
|
||||
constructor(private protocol: Protocol | PersistentProtocol, id: TContext) {
|
||||
super(protocol, id);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
const socket = this.protocol.getSocket();
|
||||
this.protocol.dispose();
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will ensure no messages are lost if there are no event listeners.
|
||||
*/
|
||||
function createBufferedEvent<T>(source: Event<T>): Event<T> {
|
||||
let emitter: Emitter<T>;
|
||||
let hasListeners = false;
|
||||
let isDeliveringMessages = false;
|
||||
let bufferedMessages: T[] = [];
|
||||
|
||||
const deliverMessages = () => {
|
||||
if (isDeliveringMessages) {
|
||||
return;
|
||||
}
|
||||
isDeliveringMessages = true;
|
||||
while (hasListeners && bufferedMessages.length > 0) {
|
||||
emitter.fire(bufferedMessages.shift()!);
|
||||
}
|
||||
isDeliveringMessages = false;
|
||||
};
|
||||
|
||||
source((e: T) => {
|
||||
bufferedMessages.push(e);
|
||||
deliverMessages();
|
||||
});
|
||||
|
||||
emitter = new Emitter<T>({
|
||||
onFirstListenerAdd: () => {
|
||||
hasListeners = true;
|
||||
// it is important to deliver these messages after this call, but before
|
||||
// other messages have a chance to be received (to guarantee in order delivery)
|
||||
// that's why we're using here nextTick and not other types of timeouts
|
||||
if (typeof process !== 'undefined') {
|
||||
process.nextTick(deliverMessages);
|
||||
} else {
|
||||
platform.setImmediate(deliverMessages);
|
||||
}
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
hasListeners = false;
|
||||
}
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
class QueueElement<T> {
|
||||
public readonly data: T;
|
||||
public next: QueueElement<T> | null;
|
||||
|
||||
constructor(data: T) {
|
||||
this.data = data;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Queue<T> {
|
||||
|
||||
private _first: QueueElement<T> | null;
|
||||
private _last: QueueElement<T> | null;
|
||||
|
||||
constructor() {
|
||||
this._first = null;
|
||||
this._last = null;
|
||||
}
|
||||
|
||||
public peek(): T | null {
|
||||
if (!this._first) {
|
||||
return null;
|
||||
}
|
||||
return this._first.data;
|
||||
}
|
||||
|
||||
public toArray(): T[] {
|
||||
let result: T[] = [], resultLen = 0;
|
||||
let it = this._first;
|
||||
while (it) {
|
||||
result[resultLen++] = it.data;
|
||||
it = it.next;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public pop(): void {
|
||||
if (!this._first) {
|
||||
return;
|
||||
}
|
||||
if (this._first === this._last) {
|
||||
this._first = null;
|
||||
this._last = null;
|
||||
return;
|
||||
}
|
||||
this._first = this._first.next;
|
||||
}
|
||||
|
||||
public push(item: T): void {
|
||||
const element = new QueueElement(item);
|
||||
if (!this._first) {
|
||||
this._first = element;
|
||||
this._last = element;
|
||||
return;
|
||||
}
|
||||
this._last!.next = element;
|
||||
this._last = element;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as Protocol, but will actually track messages and acks.
|
||||
* Moreover, it will ensure no messages are lost if there are no event listeners.
|
||||
*/
|
||||
export class PersistentProtocol {
|
||||
|
||||
private _isReconnecting: boolean;
|
||||
|
||||
private _outgoingUnackMsg: Queue<ProtocolMessage>;
|
||||
private _outgoingMsgId: number;
|
||||
private _outgoingAckId: number;
|
||||
private _outgoingAckTimeout: any | null;
|
||||
|
||||
private _incomingMsgId: number;
|
||||
private _incomingAckId: number;
|
||||
private _incomingMsgLastTime: number;
|
||||
private _incomingAckTimeout: any | null;
|
||||
|
||||
private _outgoingKeepAliveTimeout: any | null;
|
||||
private _incomingKeepAliveTimeout: any | null;
|
||||
|
||||
private _socket: ISocket;
|
||||
private _socketWriter: ProtocolWriter;
|
||||
private _socketReader: ProtocolReader;
|
||||
private _socketDisposables: IDisposable[];
|
||||
|
||||
private _onControlMessage = new Emitter<VSBuffer>();
|
||||
readonly onControlMessage: Event<VSBuffer> = createBufferedEvent(this._onControlMessage.event);
|
||||
|
||||
private _onMessage = new Emitter<VSBuffer>();
|
||||
readonly onMessage: Event<VSBuffer> = createBufferedEvent(this._onMessage.event);
|
||||
|
||||
private _onClose = new Emitter<void>();
|
||||
readonly onClose: Event<void> = createBufferedEvent(this._onClose.event);
|
||||
|
||||
private _onSocketClose = new Emitter<void>();
|
||||
readonly onSocketClose: Event<void> = createBufferedEvent(this._onSocketClose.event);
|
||||
|
||||
private _onSocketTimeout = new Emitter<void>();
|
||||
readonly onSocketTimeout: Event<void> = createBufferedEvent(this._onSocketTimeout.event);
|
||||
|
||||
public get unacknowledgedCount(): number {
|
||||
return this._outgoingMsgId - this._outgoingAckId;
|
||||
}
|
||||
|
||||
constructor(socket: ISocket, initialChunk: VSBuffer | null = null) {
|
||||
this._isReconnecting = false;
|
||||
this._outgoingUnackMsg = new Queue<ProtocolMessage>();
|
||||
this._outgoingMsgId = 0;
|
||||
this._outgoingAckId = 0;
|
||||
this._outgoingAckTimeout = null;
|
||||
|
||||
this._incomingMsgId = 0;
|
||||
this._incomingAckId = 0;
|
||||
this._incomingMsgLastTime = 0;
|
||||
this._incomingAckTimeout = null;
|
||||
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
|
||||
this._socketDisposables = [];
|
||||
this._socket = socket;
|
||||
this._socketWriter = new ProtocolWriter(this._socket);
|
||||
this._socketDisposables.push(this._socketWriter);
|
||||
this._socketReader = new ProtocolReader(this._socket);
|
||||
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);
|
||||
}
|
||||
|
||||
this._sendKeepAliveCheck();
|
||||
this._recvKeepAliveCheck();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._outgoingAckTimeout) {
|
||||
clearTimeout(this._outgoingAckTimeout);
|
||||
this._outgoingAckTimeout = null;
|
||||
}
|
||||
if (this._incomingAckTimeout) {
|
||||
clearTimeout(this._incomingAckTimeout);
|
||||
this._incomingAckTimeout = null;
|
||||
}
|
||||
if (this._outgoingKeepAliveTimeout) {
|
||||
clearTimeout(this._outgoingKeepAliveTimeout);
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
}
|
||||
if (this._incomingKeepAliveTimeout) {
|
||||
clearTimeout(this._incomingKeepAliveTimeout);
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
}
|
||||
this._socketDisposables = dispose(this._socketDisposables);
|
||||
}
|
||||
|
||||
private _sendKeepAliveCheck(): void {
|
||||
if (this._outgoingKeepAliveTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastOutgoingMsg = Date.now() - this._socketWriter.lastWriteTime;
|
||||
if (timeSinceLastOutgoingMsg >= ProtocolConstants.KeepAliveTime) {
|
||||
// sufficient time has passed since last message was written,
|
||||
// and no message from our side needed to be sent in the meantime,
|
||||
// so we will send a message containing only a keep alive.
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.KeepAlive, 0, 0, getEmptyBuffer());
|
||||
this._socketWriter.write(msg);
|
||||
this._sendKeepAliveCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this._outgoingKeepAliveTimeout = setTimeout(() => {
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
this._sendKeepAliveCheck();
|
||||
}, ProtocolConstants.KeepAliveTime - timeSinceLastOutgoingMsg + 5);
|
||||
}
|
||||
|
||||
private _recvKeepAliveCheck(): void {
|
||||
if (this._incomingKeepAliveTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastIncomingMsg = Date.now() - this._socketReader.lastReadTime;
|
||||
if (timeSinceLastIncomingMsg >= ProtocolConstants.KeepAliveTimeoutTime) {
|
||||
// Trash the socket
|
||||
this._onSocketTimeout.fire(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingKeepAliveTimeout = setTimeout(() => {
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
this._recvKeepAliveCheck();
|
||||
}, ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg + 5);
|
||||
}
|
||||
|
||||
public getSocket(): ISocket {
|
||||
return this._socket;
|
||||
}
|
||||
|
||||
public beginAcceptReconnection(socket: ISocket, initialDataChunk: VSBuffer | null): void {
|
||||
this._isReconnecting = true;
|
||||
|
||||
this._socketDisposables = dispose(this._socketDisposables);
|
||||
|
||||
this._socket = socket;
|
||||
this._socketWriter = new ProtocolWriter(this._socket);
|
||||
this._socketDisposables.push(this._socketWriter);
|
||||
this._socketReader = new ProtocolReader(this._socket);
|
||||
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);
|
||||
}
|
||||
|
||||
public endAcceptReconnection(): void {
|
||||
this._isReconnecting = false;
|
||||
|
||||
// Send again all unacknowledged messages
|
||||
const toSend = this._outgoingUnackMsg.toArray();
|
||||
for (let i = 0, len = toSend.length; i < len; i++) {
|
||||
this._socketWriter.write(toSend[i]);
|
||||
}
|
||||
this._recvAckCheck();
|
||||
|
||||
this._sendKeepAliveCheck();
|
||||
this._recvKeepAliveCheck();
|
||||
}
|
||||
|
||||
private _receiveMessage(msg: ProtocolMessage): void {
|
||||
if (msg.ack > this._outgoingAckId) {
|
||||
this._outgoingAckId = msg.ack;
|
||||
do {
|
||||
const first = this._outgoingUnackMsg.peek();
|
||||
if (first && first.id <= msg.ack) {
|
||||
// this message has been confirmed, remove it
|
||||
this._outgoingUnackMsg.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
if (msg.type === ProtocolMessageType.Regular) {
|
||||
if (msg.id > this._incomingMsgId) {
|
||||
if (msg.id !== this._incomingMsgId + 1) {
|
||||
console.error(`PROTOCOL CORRUPTION, LAST SAW MSG ${this._incomingMsgId} AND HAVE NOW RECEIVED MSG ${msg.id}`);
|
||||
}
|
||||
this._incomingMsgId = msg.id;
|
||||
this._incomingMsgLastTime = Date.now();
|
||||
this._sendAckCheck();
|
||||
this._onMessage.fire(msg.data);
|
||||
}
|
||||
} else if (msg.type === ProtocolMessageType.Control) {
|
||||
this._onControlMessage.fire(msg.data);
|
||||
}
|
||||
}
|
||||
|
||||
readEntireBuffer(): VSBuffer {
|
||||
return this._socketReader.readEntireBuffer();
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._socketWriter.flush();
|
||||
}
|
||||
|
||||
send(buffer: VSBuffer): void {
|
||||
const myId = ++this._outgoingMsgId;
|
||||
this._incomingAckId = this._incomingMsgId;
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Regular, myId, this._incomingAckId, buffer);
|
||||
this._outgoingUnackMsg.push(msg);
|
||||
if (!this._isReconnecting) {
|
||||
this._socketWriter.write(msg);
|
||||
this._recvAckCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message which will not be part of the regular acknowledge flow.
|
||||
* Use this for early control messages which are repeated in case of reconnection.
|
||||
*/
|
||||
sendControl(buffer: VSBuffer): void {
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Control, 0, 0, buffer);
|
||||
this._socketWriter.write(msg);
|
||||
}
|
||||
|
||||
private _sendAckCheck(): void {
|
||||
if (this._incomingMsgId <= this._incomingAckId) {
|
||||
// nothink to acknowledge
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._incomingAckTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastIncomingMsg = Date.now() - this._incomingMsgLastTime;
|
||||
if (timeSinceLastIncomingMsg >= ProtocolConstants.AcknowledgeTime) {
|
||||
// sufficient time has passed since this message has been received,
|
||||
// and no message from our side needed to be sent in the meantime,
|
||||
// so we will send a message containing only an ack.
|
||||
this._sendAck();
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingAckTimeout = setTimeout(() => {
|
||||
this._incomingAckTimeout = null;
|
||||
this._sendAckCheck();
|
||||
}, ProtocolConstants.AcknowledgeTime - timeSinceLastIncomingMsg + 5);
|
||||
}
|
||||
|
||||
private _recvAckCheck(): void {
|
||||
if (this._outgoingMsgId <= this._outgoingAckId) {
|
||||
// everything has been acknowledged
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._outgoingAckTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!;
|
||||
const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime;
|
||||
if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) {
|
||||
// Trash the socket
|
||||
this._onSocketTimeout.fire(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this._outgoingAckTimeout = setTimeout(() => {
|
||||
this._outgoingAckTimeout = null;
|
||||
this._recvAckCheck();
|
||||
}, ProtocolConstants.AcknowledgeTimeoutTime - timeSinceOldestUnacknowledgedMsg + 5);
|
||||
}
|
||||
|
||||
private _sendAck(): void {
|
||||
if (this._incomingMsgId <= this._incomingAckId) {
|
||||
// nothink to acknowledge
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingAckId = this._incomingMsgId;
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Ack, 0, this._incomingAckId, getEmptyBuffer());
|
||||
this._socketWriter.write(msg);
|
||||
}
|
||||
}
|
|
@ -3,8 +3,13 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Event, Emitter, Relay } from 'vs/base/common/event';
|
||||
import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
/**
|
||||
* An `IChannel` is an abstraction over a collection of commands.
|
||||
|
@ -26,3 +31,743 @@ export interface IServerChannel<TContext = string> {
|
|||
call<T>(ctx: TContext, command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
|
||||
listen<T>(ctx: TContext, event: string, arg?: any): Event<T>;
|
||||
}
|
||||
|
||||
|
||||
export const enum RequestType {
|
||||
Promise = 100,
|
||||
PromiseCancel = 101,
|
||||
EventListen = 102,
|
||||
EventDispose = 103
|
||||
}
|
||||
|
||||
type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; };
|
||||
type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number };
|
||||
type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; };
|
||||
type IRawEventDisposeRequest = { type: RequestType.EventDispose, id: number };
|
||||
type IRawRequest = IRawPromiseRequest | IRawPromiseCancelRequest | IRawEventListenRequest | IRawEventDisposeRequest;
|
||||
|
||||
export const enum ResponseType {
|
||||
Initialize = 200,
|
||||
PromiseSuccess = 201,
|
||||
PromiseError = 202,
|
||||
PromiseErrorObj = 203,
|
||||
EventFire = 204
|
||||
}
|
||||
|
||||
type IRawInitializeResponse = { type: ResponseType.Initialize };
|
||||
type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any };
|
||||
type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } };
|
||||
type IRawPromiseErrorObjResponse = { type: ResponseType.PromiseErrorObj; id: number; data: any };
|
||||
type IRawEventFireResponse = { type: ResponseType.EventFire; id: number; data: any };
|
||||
type IRawResponse = IRawInitializeResponse | IRawPromiseSuccessResponse | IRawPromiseErrorResponse | IRawPromiseErrorObjResponse | IRawEventFireResponse;
|
||||
|
||||
interface IHandler {
|
||||
(response: IRawResponse): void;
|
||||
}
|
||||
|
||||
export interface IMessagePassingProtocol {
|
||||
send(buffer: VSBuffer): void;
|
||||
onMessage: Event<VSBuffer>;
|
||||
}
|
||||
|
||||
enum State {
|
||||
Uninitialized,
|
||||
Idle
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelServer` hosts a collection of channels. You are
|
||||
* able to register channels onto it, provided a channel name.
|
||||
*/
|
||||
export interface IChannelServer<TContext = string> {
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelClient` has access to a collection of channels. You
|
||||
* are able to get those channels, given their channel name.
|
||||
*/
|
||||
export interface IChannelClient {
|
||||
getChannel<T extends IChannel>(channelName: string): T;
|
||||
}
|
||||
|
||||
export interface Client<TContext> {
|
||||
readonly ctx: TContext;
|
||||
}
|
||||
|
||||
export interface IConnectionHub<TContext> {
|
||||
readonly connections: Connection<TContext>[];
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IClientRouter` is responsible for routing calls to specific
|
||||
* channels, in scenarios in which there are multiple possible
|
||||
* channels (each from a separate client) to pick from.
|
||||
*/
|
||||
export interface IClientRouter<TContext = string> {
|
||||
routeCall(hub: IConnectionHub<TContext>, command: string, arg?: any, cancellationToken?: CancellationToken): Promise<Client<TContext>>;
|
||||
routeEvent(hub: IConnectionHub<TContext>, event: string, arg?: any): Promise<Client<TContext>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to the `IChannelClient`, you can get channels from this
|
||||
* collection of channels. The difference being that in the
|
||||
* `IRoutingChannelClient`, there are multiple clients providing
|
||||
* the same channel. You'll need to pass in an `IClientRouter` in
|
||||
* order to pick the right one.
|
||||
*/
|
||||
export interface IRoutingChannelClient<TContext = string> {
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T;
|
||||
}
|
||||
|
||||
interface IReader {
|
||||
read(bytes: number): VSBuffer;
|
||||
}
|
||||
|
||||
interface IWriter {
|
||||
write(buffer: VSBuffer): void;
|
||||
}
|
||||
|
||||
class BufferReader implements IReader {
|
||||
|
||||
private pos = 0;
|
||||
|
||||
constructor(private buffer: VSBuffer) { }
|
||||
|
||||
read(bytes: number): VSBuffer {
|
||||
const result = this.buffer.slice(this.pos, this.pos + bytes);
|
||||
this.pos += result.byteLength;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class BufferWriter implements IWriter {
|
||||
|
||||
private buffers: VSBuffer[] = [];
|
||||
|
||||
get buffer(): VSBuffer {
|
||||
return VSBuffer.concat(this.buffers);
|
||||
}
|
||||
|
||||
write(buffer: VSBuffer): void {
|
||||
this.buffers.push(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
enum DataType {
|
||||
Undefined = 0,
|
||||
String = 1,
|
||||
Buffer = 2,
|
||||
VSBuffer = 3,
|
||||
Array = 4,
|
||||
Object = 5
|
||||
}
|
||||
|
||||
function createSizeBuffer(size: number): VSBuffer {
|
||||
const result = VSBuffer.alloc(4);
|
||||
result.writeUint32BE(size, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function readSizeBuffer(reader: IReader): number {
|
||||
return reader.read(4).readUint32BE(0);
|
||||
}
|
||||
|
||||
function createOneByteBuffer(value: number): VSBuffer {
|
||||
const result = VSBuffer.alloc(1);
|
||||
result.writeUint8(value, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
const BufferPresets = {
|
||||
Undefined: createOneByteBuffer(DataType.Undefined),
|
||||
String: createOneByteBuffer(DataType.String),
|
||||
Buffer: createOneByteBuffer(DataType.Buffer),
|
||||
VSBuffer: createOneByteBuffer(DataType.VSBuffer),
|
||||
Array: createOneByteBuffer(DataType.Array),
|
||||
Object: createOneByteBuffer(DataType.Object),
|
||||
};
|
||||
|
||||
declare var Buffer: any;
|
||||
const hasBuffer = (typeof Buffer !== 'undefined');
|
||||
|
||||
function serialize(writer: IWriter, data: any): void {
|
||||
if (typeof data === 'undefined') {
|
||||
writer.write(BufferPresets.Undefined);
|
||||
} else if (typeof data === 'string') {
|
||||
const buffer = VSBuffer.fromString(data);
|
||||
writer.write(BufferPresets.String);
|
||||
writer.write(createSizeBuffer(buffer.byteLength));
|
||||
writer.write(buffer);
|
||||
} else if (hasBuffer && Buffer.isBuffer(data)) {
|
||||
const buffer = VSBuffer.wrap(data);
|
||||
writer.write(BufferPresets.Buffer);
|
||||
writer.write(createSizeBuffer(buffer.byteLength));
|
||||
writer.write(buffer);
|
||||
} else if (data instanceof VSBuffer) {
|
||||
writer.write(BufferPresets.VSBuffer);
|
||||
writer.write(createSizeBuffer(data.byteLength));
|
||||
writer.write(data);
|
||||
} else if (Array.isArray(data)) {
|
||||
writer.write(BufferPresets.Array);
|
||||
writer.write(createSizeBuffer(data.length));
|
||||
|
||||
for (const el of data) {
|
||||
serialize(writer, el);
|
||||
}
|
||||
} else {
|
||||
const buffer = VSBuffer.fromString(JSON.stringify(data));
|
||||
writer.write(BufferPresets.Object);
|
||||
writer.write(createSizeBuffer(buffer.byteLength));
|
||||
writer.write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(reader: IReader): any {
|
||||
const type = reader.read(1).readUint8(0);
|
||||
|
||||
switch (type) {
|
||||
case DataType.Undefined: return undefined;
|
||||
case DataType.String: return reader.read(readSizeBuffer(reader)).toString();
|
||||
case DataType.Buffer: return reader.read(readSizeBuffer(reader)).buffer;
|
||||
case DataType.VSBuffer: return reader.read(readSizeBuffer(reader));
|
||||
case DataType.Array: {
|
||||
const length = readSizeBuffer(reader);
|
||||
const result: any[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(deserialize(reader));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case DataType.Object: return JSON.parse(reader.read(readSizeBuffer(reader)).toString());
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelServer<TContext = string> implements IChannelServer<TContext>, IDisposable {
|
||||
|
||||
private channels = new Map<string, IServerChannel<TContext>>();
|
||||
private activeRequests = new Map<number, IDisposable>();
|
||||
private protocolListener: IDisposable | null;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol, private ctx: TContext) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg));
|
||||
this.sendResponse({ type: ResponseType.Initialize });
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channels.set(channelName, channel);
|
||||
}
|
||||
|
||||
private sendResponse(response: IRawResponse): void {
|
||||
switch (response.type) {
|
||||
case ResponseType.Initialize:
|
||||
return this.send([response.type]);
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return this.send([response.type, response.id], response.data);
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: VSBuffer): void {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private onRawMessage(message: VSBuffer): void {
|
||||
const reader = new BufferReader(message);
|
||||
const header = deserialize(reader);
|
||||
const body = deserialize(reader);
|
||||
const type = header[0] as RequestType;
|
||||
|
||||
switch (type) {
|
||||
case RequestType.Promise:
|
||||
return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.EventListen:
|
||||
return this.onEventListen({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.PromiseCancel:
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
case RequestType.EventDispose:
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
}
|
||||
}
|
||||
|
||||
private onPromise(request: IRawPromiseRequest): void {
|
||||
const channel = this.channels.get(request.channelName);
|
||||
if (!channel) {
|
||||
throw new Error('Unknown channel');
|
||||
}
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
let promise: Promise<any>;
|
||||
|
||||
try {
|
||||
promise = channel.call(this.ctx, request.name, request.arg, cancellationTokenSource.token);
|
||||
} catch (err) {
|
||||
promise = Promise.reject(err);
|
||||
}
|
||||
|
||||
const id = request.id;
|
||||
|
||||
promise.then(data => {
|
||||
this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.PromiseSuccess });
|
||||
this.activeRequests.delete(request.id);
|
||||
}, err => {
|
||||
if (err instanceof Error) {
|
||||
this.sendResponse(<IRawResponse>{
|
||||
id, data: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack ? (err.stack.split ? err.stack.split('\n') : err.stack) : undefined
|
||||
}, type: ResponseType.PromiseError
|
||||
});
|
||||
} else {
|
||||
this.sendResponse(<IRawResponse>{ id, data: err, type: ResponseType.PromiseErrorObj });
|
||||
}
|
||||
|
||||
this.activeRequests.delete(request.id);
|
||||
});
|
||||
|
||||
const disposable = toDisposable(() => cancellationTokenSource.cancel());
|
||||
this.activeRequests.set(request.id, disposable);
|
||||
}
|
||||
|
||||
private onEventListen(request: IRawEventListenRequest): void {
|
||||
const channel = this.channels.get(request.channelName);
|
||||
if (!channel) {
|
||||
throw new Error('Unknown channel');
|
||||
}
|
||||
|
||||
const id = request.id;
|
||||
const event = channel.listen(this.ctx, request.name, request.arg);
|
||||
const disposable = event(data => this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.EventFire }));
|
||||
|
||||
this.activeRequests.set(request.id, disposable);
|
||||
}
|
||||
|
||||
private disposeActiveRequest(request: IRawRequest): void {
|
||||
const disposable = this.activeRequests.get(request.id);
|
||||
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
this.activeRequests.delete(request.id);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.protocolListener) {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
}
|
||||
this.activeRequests.forEach(d => d.dispose());
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelClient implements IChannelClient, IDisposable {
|
||||
|
||||
private state: State = State.Uninitialized;
|
||||
private activeRequests = new Set<IDisposable>();
|
||||
private handlers = new Map<number, IHandler>();
|
||||
private lastRequestId: number = 0;
|
||||
private protocolListener: IDisposable | null;
|
||||
|
||||
private _onDidInitialize = new Emitter<void>();
|
||||
readonly onDidInitialize = this._onDidInitialize.event;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg));
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
|
||||
return that.requestPromise(channelName, command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg: any) {
|
||||
return that.requestEvent(channelName, event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
private requestPromise(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = RequestType.Promise;
|
||||
const request: IRawRequest = { id, type, channelName, name, arg };
|
||||
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(errors.canceled());
|
||||
}
|
||||
|
||||
let disposable: IDisposable;
|
||||
|
||||
const result = new Promise((c, e) => {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return e(errors.canceled());
|
||||
}
|
||||
|
||||
let uninitializedPromise: CancelablePromise<void> | null = createCancelablePromise(_ => this.whenInitialized());
|
||||
uninitializedPromise.then(() => {
|
||||
uninitializedPromise = null;
|
||||
|
||||
const handler: IHandler = response => {
|
||||
switch (response.type) {
|
||||
case ResponseType.PromiseSuccess:
|
||||
this.handlers.delete(id);
|
||||
c(response.data);
|
||||
break;
|
||||
|
||||
case ResponseType.PromiseError:
|
||||
this.handlers.delete(id);
|
||||
const error = new Error(response.data.message);
|
||||
(<any>error).stack = response.data.stack;
|
||||
error.name = response.data.name;
|
||||
e(error);
|
||||
break;
|
||||
|
||||
case ResponseType.PromiseErrorObj:
|
||||
this.handlers.delete(id);
|
||||
e(response.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.handlers.set(id, handler);
|
||||
this.sendRequest(request);
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
if (uninitializedPromise) {
|
||||
uninitializedPromise.cancel();
|
||||
uninitializedPromise = null;
|
||||
} else {
|
||||
this.sendRequest({ id, type: RequestType.PromiseCancel });
|
||||
}
|
||||
|
||||
e(errors.canceled());
|
||||
};
|
||||
|
||||
const cancellationTokenListener = cancellationToken.onCancellationRequested(cancel);
|
||||
disposable = combinedDisposable([toDisposable(cancel), cancellationTokenListener]);
|
||||
this.activeRequests.add(disposable);
|
||||
});
|
||||
|
||||
return result.finally(() => this.activeRequests.delete(disposable));
|
||||
}
|
||||
|
||||
private requestEvent(channelName: string, name: string, arg?: any): Event<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = RequestType.EventListen;
|
||||
const request: IRawRequest = { id, type, channelName, name, arg };
|
||||
|
||||
let uninitializedPromise: CancelablePromise<void> | null = null;
|
||||
|
||||
const emitter = new Emitter<any>({
|
||||
onFirstListenerAdd: () => {
|
||||
uninitializedPromise = createCancelablePromise(_ => this.whenInitialized());
|
||||
uninitializedPromise.then(() => {
|
||||
uninitializedPromise = null;
|
||||
this.activeRequests.add(emitter);
|
||||
this.sendRequest(request);
|
||||
});
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
if (uninitializedPromise) {
|
||||
uninitializedPromise.cancel();
|
||||
uninitializedPromise = null;
|
||||
} else {
|
||||
this.activeRequests.delete(emitter);
|
||||
this.sendRequest({ id, type: RequestType.EventDispose });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handler: IHandler = (res: IRawEventFireResponse) => emitter.fire(res.data);
|
||||
this.handlers.set(id, handler);
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
private sendRequest(request: IRawRequest): void {
|
||||
switch (request.type) {
|
||||
case RequestType.Promise:
|
||||
case RequestType.EventListen:
|
||||
return this.send([request.type, request.id, request.channelName, request.name], request.arg);
|
||||
|
||||
case RequestType.PromiseCancel:
|
||||
case RequestType.EventDispose:
|
||||
return this.send([request.type, request.id]);
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: VSBuffer): void {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private onBuffer(message: VSBuffer): void {
|
||||
const reader = new BufferReader(message);
|
||||
const header = deserialize(reader);
|
||||
const body = deserialize(reader);
|
||||
const type: ResponseType = header[0];
|
||||
|
||||
switch (type) {
|
||||
case ResponseType.Initialize:
|
||||
return this.onResponse({ type: header[0] });
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return this.onResponse({ type: header[0], id: header[1], data: body });
|
||||
}
|
||||
}
|
||||
|
||||
private onResponse(response: IRawResponse): void {
|
||||
if (response.type === ResponseType.Initialize) {
|
||||
this.state = State.Idle;
|
||||
this._onDidInitialize.fire();
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = this.handlers.get(response.id);
|
||||
|
||||
if (handler) {
|
||||
handler(response);
|
||||
}
|
||||
}
|
||||
|
||||
private whenInitialized(): Promise<void> {
|
||||
if (this.state === State.Idle) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Event.toPromise(this.onDidInitialize);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.protocolListener) {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
}
|
||||
this.activeRequests.forEach(p => p.dispose());
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientConnectionEvent {
|
||||
protocol: IMessagePassingProtocol;
|
||||
onDidClientDisconnect: Event<void>;
|
||||
}
|
||||
|
||||
interface Connection<TContext> extends Client<TContext> {
|
||||
readonly channelClient: ChannelClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCServer` is both a channel server and a routing channel
|
||||
* client.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCServer<TContext = string> implements IChannelServer<TContext>, IRoutingChannelClient<TContext>, IConnectionHub<TContext>, IDisposable {
|
||||
|
||||
private channels = new Map<string, IServerChannel<TContext>>();
|
||||
private _connections = new Set<Connection<TContext>>();
|
||||
|
||||
private _onDidChangeConnections = new Emitter<Connection<TContext>>();
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>> = this._onDidChangeConnections.event;
|
||||
|
||||
get connections(): Connection<TContext>[] {
|
||||
const result: Connection<TContext>[] = [];
|
||||
this._connections.forEach(ctx => result.push(ctx));
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
|
||||
onDidClientConnect(({ protocol, onDidClientDisconnect }) => {
|
||||
const onFirstMessage = Event.once(protocol.onMessage);
|
||||
|
||||
onFirstMessage(msg => {
|
||||
const reader = new BufferReader(msg);
|
||||
const ctx = deserialize(reader) as TContext;
|
||||
|
||||
const channelServer = new ChannelServer(protocol, ctx);
|
||||
const channelClient = new ChannelClient(protocol);
|
||||
|
||||
this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel));
|
||||
|
||||
const connection: Connection<TContext> = { channelClient, ctx };
|
||||
this._connections.add(connection);
|
||||
this._onDidChangeConnections.fire(connection);
|
||||
|
||||
onDidClientDisconnect(() => {
|
||||
channelServer.dispose();
|
||||
channelClient.dispose();
|
||||
this._connections.delete(connection);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
|
||||
const channelPromise = router.routeCall(that, command, arg)
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.call(command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg: any) {
|
||||
const channelPromise = router.routeEvent(that, event, arg)
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.listen(event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channels.set(channelName, channel);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channels.clear();
|
||||
this._connections.clear();
|
||||
this._onDidChangeConnections.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCClient` is both a channel client and a channel server.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCClient<TContext = string> implements IChannelClient, IChannelServer<TContext>, IDisposable {
|
||||
|
||||
private channelClient: ChannelClient;
|
||||
private channelServer: ChannelServer<TContext>;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, ctx: TContext) {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, ctx);
|
||||
protocol.send(writer.buffer);
|
||||
|
||||
this.channelClient = new ChannelClient(protocol);
|
||||
this.channelServer = new ChannelServer(protocol, ctx);
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
return this.channelClient.getChannel(channelName) as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channelServer.registerChannel(channelName, channel);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channelClient.dispose();
|
||||
this.channelServer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function getDelayedChannel<T extends IChannel>(promise: Promise<T>): T {
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
return promise.then(c => c.call<T>(command, arg, cancellationToken));
|
||||
},
|
||||
|
||||
listen<T>(event: string, arg?: any): Event<T> {
|
||||
const relay = new Relay<any>();
|
||||
promise.then(c => relay.input = c.listen(event, arg));
|
||||
return relay.event;
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export function getNextTickChannel<T extends IChannel>(channel: T): T {
|
||||
let didTick = false;
|
||||
|
||||
return {
|
||||
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
if (didTick) {
|
||||
return channel.call(command, arg, cancellationToken);
|
||||
}
|
||||
|
||||
return timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => channel.call<T>(command, arg, cancellationToken));
|
||||
},
|
||||
listen<T>(event: string, arg?: any): Event<T> {
|
||||
if (didTick) {
|
||||
return channel.listen<T>(event, arg);
|
||||
}
|
||||
|
||||
const relay = new Relay<T>();
|
||||
|
||||
timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => relay.input = channel.listen<T>(event, arg));
|
||||
|
||||
return relay.event;
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export class StaticRouter<TContext = string> implements IClientRouter<TContext> {
|
||||
|
||||
constructor(private fn: (ctx: TContext) => boolean | Promise<boolean>) { }
|
||||
|
||||
routeCall(hub: IConnectionHub<TContext>): Promise<Client<TContext>> {
|
||||
return this.route(hub);
|
||||
}
|
||||
|
||||
routeEvent(hub: IConnectionHub<TContext>): Promise<Client<TContext>> {
|
||||
return this.route(hub);
|
||||
}
|
||||
|
||||
private async route(hub: IConnectionHub<TContext>): Promise<Client<TContext>> {
|
||||
for (const connection of hub.connections) {
|
||||
if (await Promise.resolve(this.fn(connection.ctx))) {
|
||||
return Promise.resolve(connection);
|
||||
}
|
||||
}
|
||||
|
||||
await Event.toPromise(hub.onDidChangeConnections);
|
||||
return await this.route(hub);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,18 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IPCClient } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
export class Client extends IPCClient implements IDisposable {
|
||||
|
||||
private protocol: Protocol;
|
||||
|
||||
private static createProtocol(): Protocol {
|
||||
const onMessage = Event.fromNodeEventEmitter<Buffer>(ipcRenderer, 'ipc:message', (_, message: Buffer) => message);
|
||||
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(ipcRenderer, 'ipc:message', (_, message: Buffer) => VSBuffer.wrap(message));
|
||||
ipcRenderer.send('ipc:hello');
|
||||
return new Protocol(ipcRenderer, onMessage);
|
||||
}
|
||||
|
|
|
@ -4,20 +4,22 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron';
|
||||
import { ipcMain } from 'electron';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
interface IIPCEvent {
|
||||
event: { sender: Electron.WebContents; };
|
||||
message: Buffer | null;
|
||||
}
|
||||
|
||||
function createScopedOnMessageEvent(senderId: number, eventName: string): Event<Buffer | null> {
|
||||
function createScopedOnMessageEvent(senderId: number, eventName: string): Event<VSBuffer | null> {
|
||||
const onMessage = Event.fromNodeEventEmitter<IIPCEvent>(ipcMain, eventName, (event, message) => ({ event, message }));
|
||||
const onMessageFromSender = Event.filter(onMessage, ({ event }) => event.sender.id === senderId);
|
||||
return Event.map(onMessageFromSender, ({ message }) => message);
|
||||
// {{SQL CARBON EDIT}} cast message as null since typescript isn't saying its always null
|
||||
return Event.map(onMessageFromSender, ({ message }) => message ? VSBuffer.wrap(message) : message as null);
|
||||
}
|
||||
|
||||
export class Server extends IPCServer {
|
||||
|
@ -38,7 +40,7 @@ export class Server extends IPCServer {
|
|||
const onDidClientReconnect = new Emitter<void>();
|
||||
Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire()));
|
||||
|
||||
const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event<Buffer>;
|
||||
const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event<VSBuffer>;
|
||||
const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')), onDidClientReconnect.event);
|
||||
const protocol = new Protocol(webContents, onMessage);
|
||||
|
||||
|
@ -49,4 +51,4 @@ export class Server extends IPCServer {
|
|||
constructor() {
|
||||
super(Server.getOnDidClientConnect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,11 @@ import { Delayer, createCancelablePromise } from 'vs/base/common/async';
|
|||
import { deepClone, assign } from 'vs/base/common/objects';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { createQueuedSender } from 'vs/base/node/processes';
|
||||
import { ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IChannel, ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { isRemoteConsoleLog, log } from 'vs/base/common/console';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
/**
|
||||
* This implementation doesn't perform well since it uses base64 encoding for buffers.
|
||||
|
@ -26,11 +26,11 @@ export class Server<TContext extends string> extends IPCServer<TContext> {
|
|||
send: r => {
|
||||
try {
|
||||
if (process.send) {
|
||||
process.send(r.toString('base64'));
|
||||
process.send((<Buffer>r.buffer).toString('base64'));
|
||||
}
|
||||
} catch (e) { /* not much to do */ }
|
||||
},
|
||||
onMessage: Event.fromNodeEventEmitter(process, 'message', msg => Buffer.from(msg, 'base64'))
|
||||
onMessage: Event.fromNodeEventEmitter(process, 'message', msg => VSBuffer.wrap(Buffer.from(msg, 'base64')))
|
||||
}, ctx);
|
||||
|
||||
process.once('disconnect', () => this.dispose());
|
||||
|
@ -199,7 +199,7 @@ export class Client implements IChannelClient, IDisposable {
|
|||
|
||||
this.child = fork(this.modulePath, args, forkOpts);
|
||||
|
||||
const onMessageEmitter = new Emitter<Buffer>();
|
||||
const onMessageEmitter = new Emitter<VSBuffer>();
|
||||
const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', msg => msg);
|
||||
|
||||
onRawMessage(msg => {
|
||||
|
@ -211,11 +211,11 @@ export class Client implements IChannelClient, IDisposable {
|
|||
}
|
||||
|
||||
// Anything else goes to the outside
|
||||
onMessageEmitter.fire(Buffer.from(msg, 'base64'));
|
||||
onMessageEmitter.fire(VSBuffer.wrap(Buffer.from(msg, 'base64')));
|
||||
});
|
||||
|
||||
const sender = this.options.useQueue ? createQueuedSender(this.child) : this.child;
|
||||
const send = (r: Buffer) => this.child && this.child.connected && sender.send(r.toString('base64'));
|
||||
const send = (r: VSBuffer) => this.child && this.child.connected && sender.send((<Buffer>r.buffer).toString('base64'));
|
||||
const onMessage = onMessageEmitter.event;
|
||||
const protocol = { send, onMessage };
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
export interface Sender {
|
||||
send(channel: string, msg: Buffer | null): void;
|
||||
|
@ -12,11 +13,11 @@ export interface Sender {
|
|||
|
||||
export class Protocol implements IMessagePassingProtocol {
|
||||
|
||||
constructor(private sender: Sender, readonly onMessage: Event<Buffer>) { }
|
||||
constructor(private sender: Sender, readonly onMessage: Event<VSBuffer>) { }
|
||||
|
||||
send(message: Buffer): void {
|
||||
send(message: VSBuffer): void {
|
||||
try {
|
||||
this.sender.send('ipc:message', message);
|
||||
this.sender.send('ipc:message', (<Buffer>message.buffer));
|
||||
} catch (e) {
|
||||
// systems are going down
|
||||
}
|
||||
|
|
|
@ -4,13 +4,62 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Socket, Server as NetServer, createConnection, createServer } from 'net';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol, ClientConnectionEvent, IPCServer, IPCClient } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { tmpdir } from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { ISocket, Protocol, Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
|
||||
export class NodeSocket implements ISocket {
|
||||
public readonly socket: Socket;
|
||||
|
||||
constructor(socket: Socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public onData(_listener: (e: VSBuffer) => void): IDisposable {
|
||||
const listener = (buff: Buffer) => _listener(VSBuffer.wrap(buff));
|
||||
this.socket.on('data', listener);
|
||||
return {
|
||||
dispose: () => this.socket.off('data', listener)
|
||||
};
|
||||
}
|
||||
|
||||
public onClose(listener: () => void): IDisposable {
|
||||
this.socket.on('close', listener);
|
||||
return {
|
||||
dispose: () => this.socket.off('close', listener)
|
||||
};
|
||||
}
|
||||
|
||||
public onEnd(listener: () => void): IDisposable {
|
||||
this.socket.on('end', listener);
|
||||
return {
|
||||
dispose: () => this.socket.off('end', listener)
|
||||
};
|
||||
}
|
||||
|
||||
public write(buffer: VSBuffer): void {
|
||||
// return early if socket has been destroyed in the meantime
|
||||
if (this.socket.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we ignore the returned value from `write` because we would have to cached the data
|
||||
// anyways and nodejs is already doing that for us:
|
||||
// > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback
|
||||
// > However, the false return value is only advisory and the writable stream will unconditionally
|
||||
// > accept and buffer chunk even if it has not not been allowed to drain.
|
||||
this.socket.write(<Buffer>buffer.buffer);
|
||||
}
|
||||
|
||||
public end(): void {
|
||||
this.socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRandomPipeName(): string {
|
||||
const randomSuffix = generateUuid();
|
||||
|
@ -22,386 +71,13 @@ export function generateRandomPipeName(): string {
|
|||
}
|
||||
}
|
||||
|
||||
function log(fd: number, msg: string, data?: Buffer): void {
|
||||
const date = new Date();
|
||||
fs.writeSync(fd, `[${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}] ${msg}\n`);
|
||||
if (data) {
|
||||
fs.writeSync(fd, data);
|
||||
fs.writeSync(fd, `\n---------------------------------------------------------------------------------------------------------\n`);
|
||||
}
|
||||
fs.fdatasyncSync(fd);
|
||||
}
|
||||
|
||||
const EMPTY_BUFFER = Buffer.allocUnsafe(0);
|
||||
|
||||
class ChunkStream {
|
||||
|
||||
private _chunks: Buffer[];
|
||||
private _totalLength: number;
|
||||
|
||||
public get byteLength() {
|
||||
return this._totalLength;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._chunks = [];
|
||||
this._totalLength = 0;
|
||||
}
|
||||
|
||||
public acceptChunk(buff: Buffer) {
|
||||
this._chunks.push(buff);
|
||||
this._totalLength += buff.byteLength;
|
||||
}
|
||||
|
||||
public read(byteCount: number): Buffer {
|
||||
if (byteCount === 0) {
|
||||
return EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
if (byteCount > this._totalLength) {
|
||||
throw new Error(`Cannot read so many bytes!`);
|
||||
}
|
||||
|
||||
if (this._chunks[0].byteLength === byteCount) {
|
||||
// super fast path, precisely first chunk must be returned
|
||||
const result = this._chunks.shift()!;
|
||||
this._totalLength -= byteCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (this._chunks[0].byteLength > byteCount) {
|
||||
// fast path, the reading is entirely within the first chunk
|
||||
const result = this._chunks[0].slice(0, byteCount);
|
||||
this._chunks[0] = this._chunks[0].slice(byteCount);
|
||||
this._totalLength -= byteCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
let result = Buffer.allocUnsafe(byteCount);
|
||||
let resultOffset = 0;
|
||||
while (byteCount > 0) {
|
||||
const chunk = this._chunks[0];
|
||||
if (chunk.byteLength > byteCount) {
|
||||
// this chunk will survive
|
||||
this._chunks[0] = chunk.slice(byteCount);
|
||||
|
||||
chunk.copy(result, resultOffset, 0, byteCount);
|
||||
resultOffset += byteCount;
|
||||
this._totalLength -= byteCount;
|
||||
byteCount -= byteCount;
|
||||
} else {
|
||||
// this chunk will be entirely read
|
||||
this._chunks.shift();
|
||||
|
||||
chunk.copy(result, resultOffset, 0, chunk.byteLength);
|
||||
resultOffset += chunk.byteLength;
|
||||
this._totalLength -= chunk.byteLength;
|
||||
byteCount -= chunk.byteLength;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const enum ProtocolMessageType {
|
||||
None = 0,
|
||||
Regular = 1,
|
||||
Control = 2,
|
||||
Ack = 3,
|
||||
KeepAlive = 4
|
||||
}
|
||||
|
||||
function ProtocolMessageTypeToString(type: ProtocolMessageType): string {
|
||||
switch (type) {
|
||||
case ProtocolMessageType.None: return 'None';
|
||||
case ProtocolMessageType.Regular: return 'Regular';
|
||||
case ProtocolMessageType.Control: return 'Control';
|
||||
case ProtocolMessageType.Ack: return 'Ack';
|
||||
case ProtocolMessageType.KeepAlive: return 'KeepAlive';
|
||||
}
|
||||
}
|
||||
|
||||
export const enum ProtocolConstants {
|
||||
HeaderLength = 13,
|
||||
/**
|
||||
* Send an Acknowledge message at most 2 seconds later...
|
||||
*/
|
||||
AcknowledgeTime = 2000, // 2 seconds
|
||||
/**
|
||||
* If there is a message that has been unacknowledged for 10 seconds, consider the connection closed...
|
||||
*/
|
||||
AcknowledgeTimeoutTime = 10000, // 10 seconds
|
||||
/**
|
||||
* Send at least a message every 30s for keep alive reasons.
|
||||
*/
|
||||
KeepAliveTime = 30000, // 30 seconds
|
||||
/**
|
||||
* If there is no message received for 60 seconds, consider the connection closed...
|
||||
*/
|
||||
KeepAliveTimeoutTime = 60000, // 60 seconds
|
||||
/**
|
||||
* If there is no reconnection within this time-frame, consider the connection permanently closed...
|
||||
*/
|
||||
ReconnectionGraceTime = 60 * 60 * 1000, // 1hr
|
||||
}
|
||||
|
||||
class ProtocolMessage {
|
||||
|
||||
public writtenTime: number;
|
||||
|
||||
constructor(
|
||||
public readonly type: ProtocolMessageType,
|
||||
public readonly id: number,
|
||||
public readonly ack: number,
|
||||
public readonly data: Buffer
|
||||
) {
|
||||
this.writtenTime = 0;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.data.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
class ProtocolReader {
|
||||
|
||||
private readonly _socket: Socket;
|
||||
private _isDisposed: boolean;
|
||||
private readonly _incomingData: ChunkStream;
|
||||
private readonly _socketDataListener: (data: Buffer) => void;
|
||||
public lastReadTime: number;
|
||||
|
||||
private readonly _onMessage = new Emitter<ProtocolMessage>();
|
||||
public readonly onMessage: Event<ProtocolMessage> = this._onMessage.event;
|
||||
|
||||
private readonly _state = {
|
||||
readHead: true,
|
||||
readLen: ProtocolConstants.HeaderLength,
|
||||
messageType: ProtocolMessageType.None,
|
||||
id: 0,
|
||||
ack: 0
|
||||
};
|
||||
|
||||
constructor(socket: Socket) {
|
||||
this._socket = socket;
|
||||
this._isDisposed = false;
|
||||
this._incomingData = new ChunkStream();
|
||||
this._socketDataListener = (data: Buffer) => this.acceptChunk(data);
|
||||
this._socket.on('data', this._socketDataListener);
|
||||
this.lastReadTime = Date.now();
|
||||
}
|
||||
|
||||
public acceptChunk(data: Buffer | null): void {
|
||||
if (!data || data.byteLength === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastReadTime = Date.now();
|
||||
|
||||
this._incomingData.acceptChunk(data);
|
||||
|
||||
while (this._incomingData.byteLength >= this._state.readLen) {
|
||||
|
||||
const buff = this._incomingData.read(this._state.readLen);
|
||||
|
||||
if (this._state.readHead) {
|
||||
// buff is the header
|
||||
|
||||
// save new state => next time will read the body
|
||||
this._state.readHead = false;
|
||||
this._state.readLen = buff.readUInt32BE(9, true);
|
||||
this._state.messageType = <ProtocolMessageType>buff.readUInt8(0, true);
|
||||
this._state.id = buff.readUInt32BE(1, true);
|
||||
this._state.ack = buff.readUInt32BE(5, true);
|
||||
} else {
|
||||
// buff is the body
|
||||
const messageType = this._state.messageType;
|
||||
const id = this._state.id;
|
||||
const ack = this._state.ack;
|
||||
|
||||
// save new state => next time will read the header
|
||||
this._state.readHead = true;
|
||||
this._state.readLen = ProtocolConstants.HeaderLength;
|
||||
this._state.messageType = ProtocolMessageType.None;
|
||||
this._state.id = 0;
|
||||
this._state.ack = 0;
|
||||
|
||||
this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff));
|
||||
|
||||
if (this._isDisposed) {
|
||||
// check if an event listener lead to our disposal
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readEntireBuffer(): Buffer {
|
||||
return this._incomingData.read(this._incomingData.byteLength);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._socket.removeListener('data', this._socketDataListener);
|
||||
}
|
||||
}
|
||||
|
||||
class ProtocolWriter {
|
||||
|
||||
private _isDisposed: boolean;
|
||||
private readonly _socket: Socket;
|
||||
private readonly _logFile: number;
|
||||
private _data: Buffer[];
|
||||
private _totalLength;
|
||||
public lastWriteTime: number;
|
||||
|
||||
constructor(socket: Socket, logFile: number) {
|
||||
this._isDisposed = false;
|
||||
this._socket = socket;
|
||||
this._logFile = logFile;
|
||||
this._data = [];
|
||||
this._totalLength = 0;
|
||||
this.lastWriteTime = 0;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.flush();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
// flush
|
||||
this._writeNow();
|
||||
}
|
||||
|
||||
public write(msg: ProtocolMessage) {
|
||||
if (this._isDisposed) {
|
||||
console.warn(`Cannot write message in a disposed ProtocolWriter`);
|
||||
console.warn(msg);
|
||||
return;
|
||||
}
|
||||
if (this._logFile) {
|
||||
log(this._logFile, `send-${ProtocolMessageTypeToString(msg.type)}-${msg.id}-${msg.ack}-`, msg.data);
|
||||
}
|
||||
msg.writtenTime = Date.now();
|
||||
this.lastWriteTime = Date.now();
|
||||
const header = Buffer.allocUnsafe(ProtocolConstants.HeaderLength);
|
||||
header.writeUInt8(msg.type, 0, true);
|
||||
header.writeUInt32BE(msg.id, 1, true);
|
||||
header.writeUInt32BE(msg.ack, 5, true);
|
||||
header.writeUInt32BE(msg.data.length, 9, true);
|
||||
this._writeSoon(header, msg.data);
|
||||
}
|
||||
|
||||
private _bufferAdd(head: Buffer, body: Buffer): boolean {
|
||||
const wasEmpty = this._totalLength === 0;
|
||||
this._data.push(head, body);
|
||||
this._totalLength += head.length + body.length;
|
||||
return wasEmpty;
|
||||
}
|
||||
|
||||
private _bufferTake(): Buffer {
|
||||
const ret = Buffer.concat(this._data, this._totalLength);
|
||||
this._data.length = 0;
|
||||
this._totalLength = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
private _writeSoon(header: Buffer, data: Buffer): void {
|
||||
if (this._bufferAdd(header, data)) {
|
||||
setImmediate(() => {
|
||||
this._writeNow();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _writeNow(): void {
|
||||
if (this._totalLength === 0) {
|
||||
return;
|
||||
}
|
||||
// return early if socket has been destroyed in the meantime
|
||||
if (this._socket.destroyed) {
|
||||
return;
|
||||
}
|
||||
// we ignore the returned value from `write` because we would have to cached the data
|
||||
// anyways and nodejs is already doing that for us:
|
||||
// > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback
|
||||
// > However, the false return value is only advisory and the writable stream will unconditionally
|
||||
// > accept and buffer chunk even if it has not not been allowed to drain.
|
||||
this._socket.write(this._bufferTake());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A message has the following format:
|
||||
* ```
|
||||
* /-------------------------------|------\
|
||||
* | HEADER | |
|
||||
* |-------------------------------| DATA |
|
||||
* | TYPE | ID | ACK | DATA_LENGTH | |
|
||||
* \-------------------------------|------/
|
||||
* ```
|
||||
* The header is 9 bytes and consists of:
|
||||
* - TYPE is 1 byte (ProtocolMessageType) - the message type
|
||||
* - ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored)
|
||||
* - ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored)
|
||||
* - DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA
|
||||
*
|
||||
* Only Regular messages are counted, other messages are not counted, nor acknowledged.
|
||||
*/
|
||||
export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
|
||||
private _socket: Socket;
|
||||
private _socketWriter: ProtocolWriter;
|
||||
private _socketReader: ProtocolReader;
|
||||
|
||||
private _socketCloseListener: () => void;
|
||||
|
||||
private _onMessage = new Emitter<Buffer>();
|
||||
readonly onMessage: Event<Buffer> = this._onMessage.event;
|
||||
|
||||
private _onClose = new Emitter<void>();
|
||||
readonly onClose: Event<void> = this._onClose.event;
|
||||
|
||||
constructor(socket: Socket) {
|
||||
this._socket = socket;
|
||||
this._socketWriter = new ProtocolWriter(this._socket, 0);
|
||||
this._socketReader = new ProtocolReader(this._socket);
|
||||
|
||||
this._socketReader.onMessage((msg) => {
|
||||
if (msg.type === ProtocolMessageType.Regular) {
|
||||
this._onMessage.fire(msg.data);
|
||||
}
|
||||
});
|
||||
|
||||
this._socketCloseListener = () => {
|
||||
this._onClose.fire();
|
||||
};
|
||||
this._socket.once('close', this._socketCloseListener);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._socketWriter.dispose();
|
||||
this._socketReader.dispose();
|
||||
this._socket.removeListener('close', this._socketCloseListener);
|
||||
}
|
||||
|
||||
getSocket(): Socket {
|
||||
return this._socket;
|
||||
}
|
||||
|
||||
send(buffer: Buffer): void {
|
||||
this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.Regular, 0, 0, buffer));
|
||||
}
|
||||
}
|
||||
|
||||
export class Server extends IPCServer {
|
||||
|
||||
private static toClientConnectionEvent(server: NetServer): Event<ClientConnectionEvent> {
|
||||
const onConnection = Event.fromNodeEventEmitter<Socket>(server, 'connection');
|
||||
|
||||
return Event.map(onConnection, socket => ({
|
||||
protocol: new Protocol(socket),
|
||||
protocol: new Protocol(new NodeSocket(socket)),
|
||||
onDidClientDisconnect: Event.once(Event.fromNodeEventEmitter<void>(socket, 'close'))
|
||||
}));
|
||||
}
|
||||
|
@ -422,26 +98,6 @@ export class Server extends IPCServer {
|
|||
}
|
||||
}
|
||||
|
||||
export class Client<TContext = string> extends IPCClient<TContext> {
|
||||
|
||||
static fromSocket<TContext = string>(socket: Socket, id: TContext): Client<TContext> {
|
||||
return new Client(new Protocol(socket), id);
|
||||
}
|
||||
|
||||
get onClose(): Event<void> { return this.protocol.onClose; }
|
||||
|
||||
constructor(private protocol: Protocol | PersistentProtocol, id: TContext) {
|
||||
super(protocol, id);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
const socket = this.protocol.getSocket();
|
||||
this.protocol.dispose();
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
export function serve(port: number): Promise<Server>;
|
||||
export function serve(namedPipe: string): Promise<Server>;
|
||||
export function serve(hook: any): Promise<Server> {
|
||||
|
@ -463,440 +119,9 @@ export function connect(hook: any, clientId: string): Promise<Client> {
|
|||
return new Promise<Client>((c, e) => {
|
||||
const socket = createConnection(hook, () => {
|
||||
socket.removeListener('error', e);
|
||||
c(Client.fromSocket(socket, clientId));
|
||||
c(Client.fromSocket(new NodeSocket(socket), clientId));
|
||||
});
|
||||
|
||||
socket.once('error', e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will ensure no messages are lost if there are no event listeners.
|
||||
*/
|
||||
function createBufferedEvent<T>(source: Event<T>): Event<T> {
|
||||
let emitter: Emitter<T>;
|
||||
let hasListeners = false;
|
||||
let isDeliveringMessages = false;
|
||||
let bufferedMessages: T[] = [];
|
||||
|
||||
const deliverMessages = () => {
|
||||
if (isDeliveringMessages) {
|
||||
return;
|
||||
}
|
||||
isDeliveringMessages = true;
|
||||
while (hasListeners && bufferedMessages.length > 0) {
|
||||
emitter.fire(bufferedMessages.shift()!);
|
||||
}
|
||||
isDeliveringMessages = false;
|
||||
};
|
||||
|
||||
source((e: T) => {
|
||||
bufferedMessages.push(e);
|
||||
deliverMessages();
|
||||
});
|
||||
|
||||
emitter = new Emitter<T>({
|
||||
onFirstListenerAdd: () => {
|
||||
hasListeners = true;
|
||||
// it is important to deliver these messages after this call, but before
|
||||
// other messages have a chance to be received (to guarantee in order delivery)
|
||||
// that's why we're using here nextTick and not other types of timeouts
|
||||
process.nextTick(deliverMessages);
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
hasListeners = false;
|
||||
}
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
class QueueElement<T> {
|
||||
public readonly data: T;
|
||||
public next: QueueElement<T> | null;
|
||||
|
||||
constructor(data: T) {
|
||||
this.data = data;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Queue<T> {
|
||||
|
||||
private _first: QueueElement<T> | null;
|
||||
private _last: QueueElement<T> | null;
|
||||
|
||||
constructor() {
|
||||
this._first = null;
|
||||
this._last = null;
|
||||
}
|
||||
|
||||
public peek(): T | null {
|
||||
if (!this._first) {
|
||||
return null;
|
||||
}
|
||||
return this._first.data;
|
||||
}
|
||||
|
||||
public toArray(): T[] {
|
||||
let result: T[] = [], resultLen = 0;
|
||||
let it = this._first;
|
||||
while (it) {
|
||||
result[resultLen++] = it.data;
|
||||
it = it.next;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public pop(): void {
|
||||
if (!this._first) {
|
||||
return;
|
||||
}
|
||||
if (this._first === this._last) {
|
||||
this._first = null;
|
||||
this._last = null;
|
||||
return;
|
||||
}
|
||||
this._first = this._first.next;
|
||||
}
|
||||
|
||||
public push(item: T): void {
|
||||
const element = new QueueElement(item);
|
||||
if (!this._first) {
|
||||
this._first = element;
|
||||
this._last = element;
|
||||
return;
|
||||
}
|
||||
this._last!.next = element;
|
||||
this._last = element;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as Protocol, but will actually track messages and acks.
|
||||
* Moreover, it will ensure no messages are lost if there are no event listeners.
|
||||
*/
|
||||
export class PersistentProtocol {
|
||||
|
||||
private _logFile: number;
|
||||
private _isReconnecting: boolean;
|
||||
|
||||
private _outgoingUnackMsg: Queue<ProtocolMessage>;
|
||||
private _outgoingMsgId: number;
|
||||
private _outgoingAckId: number;
|
||||
private _outgoingAckTimeout: NodeJS.Timeout | null;
|
||||
|
||||
private _incomingMsgId: number;
|
||||
private _incomingAckId: number;
|
||||
private _incomingMsgLastTime: number;
|
||||
private _incomingAckTimeout: NodeJS.Timeout | null;
|
||||
|
||||
private _outgoingKeepAliveTimeout: NodeJS.Timeout | null;
|
||||
private _incomingKeepAliveTimeout: NodeJS.Timeout | null;
|
||||
|
||||
private _socket: Socket;
|
||||
private _socketWriter: ProtocolWriter;
|
||||
private _socketReader: ProtocolReader;
|
||||
private _socketReaderListener: IDisposable;
|
||||
|
||||
private readonly _socketCloseListener: () => void;
|
||||
private readonly _socketEndListener: () => void;
|
||||
private readonly _socketErrorListener: (err: any) => void;
|
||||
|
||||
private _onControlMessage = new Emitter<Buffer>();
|
||||
readonly onControlMessage: Event<Buffer> = createBufferedEvent(this._onControlMessage.event);
|
||||
|
||||
private _onMessage = new Emitter<Buffer>();
|
||||
readonly onMessage: Event<Buffer> = createBufferedEvent(this._onMessage.event);
|
||||
|
||||
private _onClose = new Emitter<void>();
|
||||
readonly onClose: Event<void> = createBufferedEvent(this._onClose.event);
|
||||
|
||||
private _onSocketClose = new Emitter<void>();
|
||||
readonly onSocketClose: Event<void> = createBufferedEvent(this._onSocketClose.event);
|
||||
|
||||
private _onSocketTimeout = new Emitter<void>();
|
||||
readonly onSocketTimeout: Event<void> = createBufferedEvent(this._onSocketTimeout.event);
|
||||
|
||||
public get unacknowledgedCount(): number {
|
||||
return this._outgoingMsgId - this._outgoingAckId;
|
||||
}
|
||||
|
||||
constructor(socket: Socket, initialChunk: Buffer | null = null, logFileName: string | null = null) {
|
||||
this._logFile = 0;
|
||||
this._isReconnecting = false;
|
||||
if (logFileName) {
|
||||
console.log(`PersistentProtocol log file: ${logFileName}`);
|
||||
this._logFile = fs.openSync(logFileName, 'a');
|
||||
}
|
||||
this._outgoingUnackMsg = new Queue<ProtocolMessage>();
|
||||
this._outgoingMsgId = 0;
|
||||
this._outgoingAckId = 0;
|
||||
this._outgoingAckTimeout = null;
|
||||
|
||||
this._incomingMsgId = 0;
|
||||
this._incomingAckId = 0;
|
||||
this._incomingMsgLastTime = 0;
|
||||
this._incomingAckTimeout = null;
|
||||
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
|
||||
this._socketCloseListener = () => {
|
||||
console.log(`socket triggered close event!`);
|
||||
this._onSocketClose.fire();
|
||||
};
|
||||
this._socketEndListener = () => {
|
||||
// received FIN
|
||||
this._onClose.fire();
|
||||
};
|
||||
this._socketErrorListener = (err) => {
|
||||
console.log(`socket had an error: `, err);
|
||||
};
|
||||
|
||||
this._socket = socket;
|
||||
this._socketWriter = new ProtocolWriter(this._socket, this._logFile);
|
||||
this._socketReader = new ProtocolReader(this._socket);
|
||||
this._socketReaderListener = this._socketReader.onMessage(msg => this._receiveMessage(msg));
|
||||
this._socket.on('close', this._socketCloseListener);
|
||||
this._socket.on('end', this._socketEndListener);
|
||||
this._socket.on('error', this._socketErrorListener);
|
||||
if (initialChunk) {
|
||||
this._socketReader.acceptChunk(initialChunk);
|
||||
}
|
||||
|
||||
this._sendKeepAliveCheck();
|
||||
this._recvKeepAliveCheck();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._outgoingAckTimeout) {
|
||||
clearTimeout(this._outgoingAckTimeout);
|
||||
this._outgoingAckTimeout = null;
|
||||
}
|
||||
if (this._incomingAckTimeout) {
|
||||
clearTimeout(this._incomingAckTimeout);
|
||||
this._incomingAckTimeout = null;
|
||||
}
|
||||
if (this._outgoingKeepAliveTimeout) {
|
||||
clearTimeout(this._outgoingKeepAliveTimeout);
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
}
|
||||
if (this._incomingKeepAliveTimeout) {
|
||||
clearTimeout(this._incomingKeepAliveTimeout);
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
}
|
||||
if (this._logFile) {
|
||||
fs.closeSync(this._logFile);
|
||||
}
|
||||
this._socketWriter.dispose();
|
||||
this._socketReader.dispose();
|
||||
this._socketReaderListener.dispose();
|
||||
this._socket.removeListener('close', this._socketCloseListener);
|
||||
this._socket.removeListener('end', this._socketEndListener);
|
||||
this._socket.removeListener('error', this._socketErrorListener);
|
||||
}
|
||||
|
||||
private _sendKeepAliveCheck(): void {
|
||||
if (this._outgoingKeepAliveTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastOutgoingMsg = Date.now() - this._socketWriter.lastWriteTime;
|
||||
if (timeSinceLastOutgoingMsg >= ProtocolConstants.KeepAliveTime) {
|
||||
// sufficient time has passed since last message was written,
|
||||
// and no message from our side needed to be sent in the meantime,
|
||||
// so we will send a message containing only a keep alive.
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.KeepAlive, 0, 0, EMPTY_BUFFER);
|
||||
this._socketWriter.write(msg);
|
||||
this._sendKeepAliveCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this._outgoingKeepAliveTimeout = setTimeout(() => {
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
this._sendKeepAliveCheck();
|
||||
}, ProtocolConstants.KeepAliveTime - timeSinceLastOutgoingMsg + 5);
|
||||
}
|
||||
|
||||
private _recvKeepAliveCheck(): void {
|
||||
if (this._incomingKeepAliveTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastIncomingMsg = Date.now() - this._socketReader.lastReadTime;
|
||||
if (timeSinceLastIncomingMsg >= ProtocolConstants.KeepAliveTimeoutTime) {
|
||||
// Trash the socket
|
||||
this._onSocketTimeout.fire(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingKeepAliveTimeout = setTimeout(() => {
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
this._recvKeepAliveCheck();
|
||||
}, ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg + 5);
|
||||
}
|
||||
|
||||
public getSocket(): Socket {
|
||||
return this._socket;
|
||||
}
|
||||
|
||||
public beginAcceptReconnection(socket: Socket, initialDataChunk: Buffer | null): void {
|
||||
this._isReconnecting = true;
|
||||
|
||||
this._socketWriter.dispose();
|
||||
this._socketReader.dispose();
|
||||
this._socketReaderListener.dispose();
|
||||
this._socket.removeListener('close', this._socketCloseListener);
|
||||
this._socket.removeListener('end', this._socketEndListener);
|
||||
this._socket.removeListener('error', this._socketErrorListener);
|
||||
|
||||
this._socket = socket;
|
||||
|
||||
this._socketWriter = new ProtocolWriter(this._socket, this._logFile);
|
||||
this._socketReader = new ProtocolReader(this._socket);
|
||||
this._socketReaderListener = this._socketReader.onMessage(msg => this._receiveMessage(msg));
|
||||
this._socketReader.acceptChunk(initialDataChunk);
|
||||
this._socket.on('close', this._socketCloseListener);
|
||||
this._socket.on('end', this._socketEndListener);
|
||||
this._socket.on('error', this._socketErrorListener);
|
||||
}
|
||||
|
||||
public endAcceptReconnection(): void {
|
||||
this._isReconnecting = false;
|
||||
|
||||
// Send again all unacknowledged messages
|
||||
const toSend = this._outgoingUnackMsg.toArray();
|
||||
for (let i = 0, len = toSend.length; i < len; i++) {
|
||||
this._socketWriter.write(toSend[i]);
|
||||
}
|
||||
this._recvAckCheck();
|
||||
|
||||
this._sendKeepAliveCheck();
|
||||
this._recvKeepAliveCheck();
|
||||
}
|
||||
|
||||
private _receiveMessage(msg: ProtocolMessage): void {
|
||||
if (this._logFile) {
|
||||
log(this._logFile, `recv-${ProtocolMessageTypeToString(msg.type)}-${msg.id}-${msg.ack}-`, msg.data);
|
||||
}
|
||||
if (msg.ack > this._outgoingAckId) {
|
||||
this._outgoingAckId = msg.ack;
|
||||
do {
|
||||
const first = this._outgoingUnackMsg.peek();
|
||||
if (first && first.id <= msg.ack) {
|
||||
// this message has been confirmed, remove it
|
||||
this._outgoingUnackMsg.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
if (msg.type === ProtocolMessageType.Regular) {
|
||||
if (msg.id > this._incomingMsgId) {
|
||||
if (msg.id !== this._incomingMsgId + 1) {
|
||||
console.error(`PROTOCOL CORRUPTION, LAST SAW MSG ${this._incomingMsgId} AND HAVE NOW RECEIVED MSG ${msg.id}`);
|
||||
}
|
||||
this._incomingMsgId = msg.id;
|
||||
this._incomingMsgLastTime = Date.now();
|
||||
this._sendAckCheck();
|
||||
this._onMessage.fire(msg.data);
|
||||
}
|
||||
} else if (msg.type === ProtocolMessageType.Control) {
|
||||
this._onControlMessage.fire(msg.data);
|
||||
}
|
||||
}
|
||||
|
||||
readEntireBuffer(): Buffer {
|
||||
return this._socketReader.readEntireBuffer();
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._socketWriter.flush();
|
||||
}
|
||||
|
||||
send(buffer: Buffer): void {
|
||||
const myId = ++this._outgoingMsgId;
|
||||
this._incomingAckId = this._incomingMsgId;
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Regular, myId, this._incomingAckId, buffer);
|
||||
this._outgoingUnackMsg.push(msg);
|
||||
if (!this._isReconnecting) {
|
||||
this._socketWriter.write(msg);
|
||||
this._recvAckCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message which will not be part of the regular acknowledge flow.
|
||||
* Use this for early control messages which are repeated in case of reconnection.
|
||||
*/
|
||||
sendControl(buffer: Buffer): void {
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Control, 0, 0, buffer);
|
||||
this._socketWriter.write(msg);
|
||||
}
|
||||
|
||||
private _sendAckCheck(): void {
|
||||
if (this._incomingMsgId <= this._incomingAckId) {
|
||||
// nothink to acknowledge
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._incomingAckTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastIncomingMsg = Date.now() - this._incomingMsgLastTime;
|
||||
if (timeSinceLastIncomingMsg >= ProtocolConstants.AcknowledgeTime) {
|
||||
// sufficient time has passed since this message has been received,
|
||||
// and no message from our side needed to be sent in the meantime,
|
||||
// so we will send a message containing only an ack.
|
||||
this._sendAck();
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingAckTimeout = setTimeout(() => {
|
||||
this._incomingAckTimeout = null;
|
||||
this._sendAckCheck();
|
||||
}, ProtocolConstants.AcknowledgeTime - timeSinceLastIncomingMsg + 5);
|
||||
}
|
||||
|
||||
private _recvAckCheck(): void {
|
||||
if (this._outgoingMsgId <= this._outgoingAckId) {
|
||||
// everything has been acknowledged
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._outgoingAckTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!;
|
||||
const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime;
|
||||
if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) {
|
||||
// Trash the socket
|
||||
this._onSocketTimeout.fire(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this._outgoingAckTimeout = setTimeout(() => {
|
||||
this._outgoingAckTimeout = null;
|
||||
this._recvAckCheck();
|
||||
}, ProtocolConstants.AcknowledgeTimeoutTime - timeSinceOldestUnacknowledgedMsg + 5);
|
||||
}
|
||||
|
||||
private _sendAck(): void {
|
||||
if (this._incomingMsgId <= this._incomingAckId) {
|
||||
// nothink to acknowledge
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingAckId = this._incomingMsgId;
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Ack, 0, this._incomingAckId, EMPTY_BUFFER);
|
||||
this._socketWriter.write(msg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,733 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter, Relay } from 'vs/base/common/event';
|
||||
import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
export const enum RequestType {
|
||||
Promise = 100,
|
||||
PromiseCancel = 101,
|
||||
EventListen = 102,
|
||||
EventDispose = 103
|
||||
}
|
||||
|
||||
type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; };
|
||||
type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number };
|
||||
type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; };
|
||||
type IRawEventDisposeRequest = { type: RequestType.EventDispose, id: number };
|
||||
type IRawRequest = IRawPromiseRequest | IRawPromiseCancelRequest | IRawEventListenRequest | IRawEventDisposeRequest;
|
||||
|
||||
export const enum ResponseType {
|
||||
Initialize = 200,
|
||||
PromiseSuccess = 201,
|
||||
PromiseError = 202,
|
||||
PromiseErrorObj = 203,
|
||||
EventFire = 204
|
||||
}
|
||||
|
||||
type IRawInitializeResponse = { type: ResponseType.Initialize };
|
||||
type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any };
|
||||
type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } };
|
||||
type IRawPromiseErrorObjResponse = { type: ResponseType.PromiseErrorObj; id: number; data: any };
|
||||
type IRawEventFireResponse = { type: ResponseType.EventFire; id: number; data: any };
|
||||
type IRawResponse = IRawInitializeResponse | IRawPromiseSuccessResponse | IRawPromiseErrorResponse | IRawPromiseErrorObjResponse | IRawEventFireResponse;
|
||||
|
||||
interface IHandler {
|
||||
(response: IRawResponse): void;
|
||||
}
|
||||
|
||||
export interface IMessagePassingProtocol {
|
||||
send(buffer: Buffer): void;
|
||||
onMessage: Event<Buffer>;
|
||||
}
|
||||
|
||||
enum State {
|
||||
Uninitialized,
|
||||
Idle
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelServer` hosts a collection of channels. You are
|
||||
* able to register channels onto it, provided a channel name.
|
||||
*/
|
||||
export interface IChannelServer<TContext = string> {
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelClient` has access to a collection of channels. You
|
||||
* are able to get those channels, given their channel name.
|
||||
*/
|
||||
export interface IChannelClient {
|
||||
getChannel<T extends IChannel>(channelName: string): T;
|
||||
}
|
||||
|
||||
export interface Client<TContext> {
|
||||
readonly ctx: TContext;
|
||||
}
|
||||
|
||||
export interface IConnectionHub<TContext> {
|
||||
readonly connections: Connection<TContext>[];
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IClientRouter` is responsible for routing calls to specific
|
||||
* channels, in scenarios in which there are multiple possible
|
||||
* channels (each from a separate client) to pick from.
|
||||
*/
|
||||
export interface IClientRouter<TContext = string> {
|
||||
routeCall(hub: IConnectionHub<TContext>, command: string, arg?: any, cancellationToken?: CancellationToken): Promise<Client<TContext>>;
|
||||
routeEvent(hub: IConnectionHub<TContext>, event: string, arg?: any): Promise<Client<TContext>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to the `IChannelClient`, you can get channels from this
|
||||
* collection of channels. The difference being that in the
|
||||
* `IRoutingChannelClient`, there are multiple clients providing
|
||||
* the same channel. You'll need to pass in an `IClientRouter` in
|
||||
* order to pick the right one.
|
||||
*/
|
||||
export interface IRoutingChannelClient<TContext = string> {
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T;
|
||||
}
|
||||
|
||||
interface IReader {
|
||||
read(bytes: number): Buffer;
|
||||
}
|
||||
|
||||
interface IWriter {
|
||||
write(buffer: Buffer): void;
|
||||
}
|
||||
|
||||
class BufferReader implements IReader {
|
||||
|
||||
private pos = 0;
|
||||
|
||||
constructor(private buffer: Buffer) { }
|
||||
|
||||
read(bytes: number): Buffer {
|
||||
const result = this.buffer.slice(this.pos, this.pos + bytes);
|
||||
this.pos += result.length;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class BufferWriter implements IWriter {
|
||||
|
||||
private buffers: Buffer[] = [];
|
||||
|
||||
get buffer(): Buffer {
|
||||
return Buffer.concat(this.buffers);
|
||||
}
|
||||
|
||||
write(buffer: Buffer): void {
|
||||
this.buffers.push(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
enum DataType {
|
||||
Undefined = 0,
|
||||
String = 1,
|
||||
Buffer = 2,
|
||||
Array = 3,
|
||||
Object = 4
|
||||
}
|
||||
|
||||
function createSizeBuffer(size: number): Buffer {
|
||||
const result = Buffer.allocUnsafe(4);
|
||||
result.writeUInt32BE(size, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function readSizeBuffer(reader: IReader): number {
|
||||
return reader.read(4).readUInt32BE(0);
|
||||
}
|
||||
|
||||
const BufferPresets = {
|
||||
Undefined: Buffer.alloc(1, DataType.Undefined),
|
||||
String: Buffer.alloc(1, DataType.String),
|
||||
Buffer: Buffer.alloc(1, DataType.Buffer),
|
||||
Array: Buffer.alloc(1, DataType.Array),
|
||||
Object: Buffer.alloc(1, DataType.Object)
|
||||
};
|
||||
|
||||
function serialize(writer: IWriter, data: any): void {
|
||||
if (typeof data === 'undefined') {
|
||||
writer.write(BufferPresets.Undefined);
|
||||
} else if (typeof data === 'string') {
|
||||
const buffer = Buffer.from(data);
|
||||
writer.write(BufferPresets.String);
|
||||
writer.write(createSizeBuffer(buffer.length));
|
||||
writer.write(buffer);
|
||||
} else if (Buffer.isBuffer(data)) {
|
||||
writer.write(BufferPresets.Buffer);
|
||||
writer.write(createSizeBuffer(data.length));
|
||||
writer.write(data);
|
||||
} else if (Array.isArray(data)) {
|
||||
writer.write(BufferPresets.Array);
|
||||
writer.write(createSizeBuffer(data.length));
|
||||
|
||||
for (const el of data) {
|
||||
serialize(writer, el);
|
||||
}
|
||||
} else {
|
||||
const buffer = Buffer.from(JSON.stringify(data));
|
||||
writer.write(BufferPresets.Object);
|
||||
writer.write(createSizeBuffer(buffer.length));
|
||||
writer.write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(reader: IReader): any {
|
||||
const type = reader.read(1).readUInt8(0);
|
||||
|
||||
switch (type) {
|
||||
case DataType.Undefined: return undefined;
|
||||
case DataType.String: return reader.read(readSizeBuffer(reader)).toString();
|
||||
case DataType.Buffer: return reader.read(readSizeBuffer(reader));
|
||||
case DataType.Array: {
|
||||
const length = readSizeBuffer(reader);
|
||||
const result: any[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(deserialize(reader));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case DataType.Object: return JSON.parse(reader.read(readSizeBuffer(reader)).toString());
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelServer<TContext = string> implements IChannelServer<TContext>, IDisposable {
|
||||
|
||||
private channels = new Map<string, IServerChannel<TContext>>();
|
||||
private activeRequests = new Map<number, IDisposable>();
|
||||
private protocolListener: IDisposable | null;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol, private ctx: TContext) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg));
|
||||
this.sendResponse({ type: ResponseType.Initialize });
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channels.set(channelName, channel);
|
||||
}
|
||||
|
||||
private sendResponse(response: IRawResponse): void {
|
||||
switch (response.type) {
|
||||
case ResponseType.Initialize:
|
||||
return this.send([response.type]);
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return this.send([response.type, response.id], response.data);
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: Buffer): void {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private onRawMessage(message: Buffer): void {
|
||||
const reader = new BufferReader(message);
|
||||
const header = deserialize(reader);
|
||||
const body = deserialize(reader);
|
||||
const type = header[0] as RequestType;
|
||||
|
||||
switch (type) {
|
||||
case RequestType.Promise:
|
||||
return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.EventListen:
|
||||
return this.onEventListen({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.PromiseCancel:
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
case RequestType.EventDispose:
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
}
|
||||
}
|
||||
|
||||
private onPromise(request: IRawPromiseRequest): void {
|
||||
const channel = this.channels.get(request.channelName);
|
||||
if (!channel) {
|
||||
throw new Error('Unknown channel');
|
||||
}
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
let promise: Promise<any>;
|
||||
|
||||
try {
|
||||
promise = channel.call(this.ctx, request.name, request.arg, cancellationTokenSource.token);
|
||||
} catch (err) {
|
||||
promise = Promise.reject(err);
|
||||
}
|
||||
|
||||
const id = request.id;
|
||||
|
||||
promise.then(data => {
|
||||
this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.PromiseSuccess });
|
||||
this.activeRequests.delete(request.id);
|
||||
}, err => {
|
||||
if (err instanceof Error) {
|
||||
this.sendResponse(<IRawResponse>{
|
||||
id, data: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack ? (err.stack.split ? err.stack.split('\n') : err.stack) : undefined
|
||||
}, type: ResponseType.PromiseError
|
||||
});
|
||||
} else {
|
||||
this.sendResponse(<IRawResponse>{ id, data: err, type: ResponseType.PromiseErrorObj });
|
||||
}
|
||||
|
||||
this.activeRequests.delete(request.id);
|
||||
});
|
||||
|
||||
const disposable = toDisposable(() => cancellationTokenSource.cancel());
|
||||
this.activeRequests.set(request.id, disposable);
|
||||
}
|
||||
|
||||
private onEventListen(request: IRawEventListenRequest): void {
|
||||
const channel = this.channels.get(request.channelName);
|
||||
if (!channel) {
|
||||
throw new Error('Unknown channel');
|
||||
}
|
||||
|
||||
const id = request.id;
|
||||
const event = channel.listen(this.ctx, request.name, request.arg);
|
||||
const disposable = event(data => this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.EventFire }));
|
||||
|
||||
this.activeRequests.set(request.id, disposable);
|
||||
}
|
||||
|
||||
private disposeActiveRequest(request: IRawRequest): void {
|
||||
const disposable = this.activeRequests.get(request.id);
|
||||
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
this.activeRequests.delete(request.id);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.protocolListener) {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
}
|
||||
this.activeRequests.forEach(d => d.dispose());
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelClient implements IChannelClient, IDisposable {
|
||||
|
||||
private state: State = State.Uninitialized;
|
||||
private activeRequests = new Set<IDisposable>();
|
||||
private handlers = new Map<number, IHandler>();
|
||||
private lastRequestId: number = 0;
|
||||
private protocolListener: IDisposable | null;
|
||||
|
||||
private _onDidInitialize = new Emitter<void>();
|
||||
readonly onDidInitialize = this._onDidInitialize.event;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg));
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
|
||||
return that.requestPromise(channelName, command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg: any) {
|
||||
return that.requestEvent(channelName, event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
private requestPromise(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = RequestType.Promise;
|
||||
const request: IRawRequest = { id, type, channelName, name, arg };
|
||||
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(errors.canceled());
|
||||
}
|
||||
|
||||
let disposable: IDisposable;
|
||||
|
||||
const result = new Promise((c, e) => {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return e(errors.canceled());
|
||||
}
|
||||
|
||||
let uninitializedPromise: CancelablePromise<void> | null = createCancelablePromise(_ => this.whenInitialized());
|
||||
uninitializedPromise.then(() => {
|
||||
uninitializedPromise = null;
|
||||
|
||||
const handler: IHandler = response => {
|
||||
switch (response.type) {
|
||||
case ResponseType.PromiseSuccess:
|
||||
this.handlers.delete(id);
|
||||
c(response.data);
|
||||
break;
|
||||
|
||||
case ResponseType.PromiseError:
|
||||
this.handlers.delete(id);
|
||||
const error = new Error(response.data.message);
|
||||
(<any>error).stack = response.data.stack;
|
||||
error.name = response.data.name;
|
||||
e(error);
|
||||
break;
|
||||
|
||||
case ResponseType.PromiseErrorObj:
|
||||
this.handlers.delete(id);
|
||||
e(response.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.handlers.set(id, handler);
|
||||
this.sendRequest(request);
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
if (uninitializedPromise) {
|
||||
uninitializedPromise.cancel();
|
||||
uninitializedPromise = null;
|
||||
} else {
|
||||
this.sendRequest({ id, type: RequestType.PromiseCancel });
|
||||
}
|
||||
|
||||
e(errors.canceled());
|
||||
};
|
||||
|
||||
const cancellationTokenListener = cancellationToken.onCancellationRequested(cancel);
|
||||
disposable = combinedDisposable([toDisposable(cancel), cancellationTokenListener]);
|
||||
this.activeRequests.add(disposable);
|
||||
});
|
||||
|
||||
return result.finally(() => this.activeRequests.delete(disposable));
|
||||
}
|
||||
|
||||
private requestEvent(channelName: string, name: string, arg?: any): Event<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = RequestType.EventListen;
|
||||
const request: IRawRequest = { id, type, channelName, name, arg };
|
||||
|
||||
let uninitializedPromise: CancelablePromise<void> | null = null;
|
||||
|
||||
const emitter = new Emitter<any>({
|
||||
onFirstListenerAdd: () => {
|
||||
uninitializedPromise = createCancelablePromise(_ => this.whenInitialized());
|
||||
uninitializedPromise.then(() => {
|
||||
uninitializedPromise = null;
|
||||
this.activeRequests.add(emitter);
|
||||
this.sendRequest(request);
|
||||
});
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
if (uninitializedPromise) {
|
||||
uninitializedPromise.cancel();
|
||||
uninitializedPromise = null;
|
||||
} else {
|
||||
this.activeRequests.delete(emitter);
|
||||
this.sendRequest({ id, type: RequestType.EventDispose });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handler: IHandler = (res: IRawEventFireResponse) => emitter.fire(res.data);
|
||||
this.handlers.set(id, handler);
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
private sendRequest(request: IRawRequest): void {
|
||||
switch (request.type) {
|
||||
case RequestType.Promise:
|
||||
case RequestType.EventListen:
|
||||
return this.send([request.type, request.id, request.channelName, request.name], request.arg);
|
||||
|
||||
case RequestType.PromiseCancel:
|
||||
case RequestType.EventDispose:
|
||||
return this.send([request.type, request.id]);
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: Buffer): void {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private onBuffer(message: Buffer): void {
|
||||
const reader = new BufferReader(message);
|
||||
const header = deserialize(reader);
|
||||
const body = deserialize(reader);
|
||||
const type: ResponseType = header[0];
|
||||
|
||||
switch (type) {
|
||||
case ResponseType.Initialize:
|
||||
return this.onResponse({ type: header[0] });
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return this.onResponse({ type: header[0], id: header[1], data: body });
|
||||
}
|
||||
}
|
||||
|
||||
private onResponse(response: IRawResponse): void {
|
||||
if (response.type === ResponseType.Initialize) {
|
||||
this.state = State.Idle;
|
||||
this._onDidInitialize.fire();
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = this.handlers.get(response.id);
|
||||
|
||||
if (handler) {
|
||||
handler(response);
|
||||
}
|
||||
}
|
||||
|
||||
private whenInitialized(): Promise<void> {
|
||||
if (this.state === State.Idle) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Event.toPromise(this.onDidInitialize);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.protocolListener) {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
}
|
||||
this.activeRequests.forEach(p => p.dispose());
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientConnectionEvent {
|
||||
protocol: IMessagePassingProtocol;
|
||||
onDidClientDisconnect: Event<void>;
|
||||
}
|
||||
|
||||
interface Connection<TContext> extends Client<TContext> {
|
||||
readonly channelClient: ChannelClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCServer` is both a channel server and a routing channel
|
||||
* client.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCServer<TContext = string> implements IChannelServer<TContext>, IRoutingChannelClient<TContext>, IConnectionHub<TContext>, IDisposable {
|
||||
|
||||
private channels = new Map<string, IServerChannel<TContext>>();
|
||||
private _connections = new Set<Connection<TContext>>();
|
||||
|
||||
private _onDidChangeConnections = new Emitter<Connection<TContext>>();
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>> = this._onDidChangeConnections.event;
|
||||
|
||||
get connections(): Connection<TContext>[] {
|
||||
const result: Connection<TContext>[] = [];
|
||||
this._connections.forEach(ctx => result.push(ctx));
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
|
||||
onDidClientConnect(({ protocol, onDidClientDisconnect }) => {
|
||||
const onFirstMessage = Event.once(protocol.onMessage);
|
||||
|
||||
onFirstMessage(msg => {
|
||||
const reader = new BufferReader(msg);
|
||||
const ctx = deserialize(reader) as TContext;
|
||||
|
||||
const channelServer = new ChannelServer(protocol, ctx);
|
||||
const channelClient = new ChannelClient(protocol);
|
||||
|
||||
this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel));
|
||||
|
||||
const connection: Connection<TContext> = { channelClient, ctx };
|
||||
this._connections.add(connection);
|
||||
this._onDidChangeConnections.fire(connection);
|
||||
|
||||
onDidClientDisconnect(() => {
|
||||
channelServer.dispose();
|
||||
channelClient.dispose();
|
||||
this._connections.delete(connection);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
|
||||
const channelPromise = router.routeCall(that, command, arg)
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.call(command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg: any) {
|
||||
const channelPromise = router.routeEvent(that, event, arg)
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.listen(event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channels.set(channelName, channel);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channels.clear();
|
||||
this._connections.clear();
|
||||
this._onDidChangeConnections.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCClient` is both a channel client and a channel server.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCClient<TContext = string> implements IChannelClient, IChannelServer<TContext>, IDisposable {
|
||||
|
||||
private channelClient: ChannelClient;
|
||||
private channelServer: ChannelServer<TContext>;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, ctx: TContext) {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, ctx);
|
||||
protocol.send(writer.buffer);
|
||||
|
||||
this.channelClient = new ChannelClient(protocol);
|
||||
this.channelServer = new ChannelServer(protocol, ctx);
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
return this.channelClient.getChannel(channelName) as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channelServer.registerChannel(channelName, channel);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channelClient.dispose();
|
||||
this.channelServer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function getDelayedChannel<T extends IChannel>(promise: Promise<T>): T {
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
return promise.then(c => c.call<T>(command, arg, cancellationToken));
|
||||
},
|
||||
|
||||
listen<T>(event: string, arg?: any): Event<T> {
|
||||
const relay = new Relay<any>();
|
||||
promise.then(c => relay.input = c.listen(event, arg));
|
||||
return relay.event;
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export function getNextTickChannel<T extends IChannel>(channel: T): T {
|
||||
let didTick = false;
|
||||
|
||||
return {
|
||||
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
if (didTick) {
|
||||
return channel.call(command, arg, cancellationToken);
|
||||
}
|
||||
|
||||
return timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => channel.call<T>(command, arg, cancellationToken));
|
||||
},
|
||||
listen<T>(event: string, arg?: any): Event<T> {
|
||||
if (didTick) {
|
||||
return channel.listen<T>(event, arg);
|
||||
}
|
||||
|
||||
const relay = new Relay<T>();
|
||||
|
||||
timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => relay.input = channel.listen<T>(event, arg));
|
||||
|
||||
return relay.event;
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export class StaticRouter<TContext = string> implements IClientRouter<TContext> {
|
||||
|
||||
constructor(private fn: (ctx: TContext) => boolean | Promise<boolean>) { }
|
||||
|
||||
routeCall(hub: IConnectionHub<TContext>): Promise<Client<TContext>> {
|
||||
return this.route(hub);
|
||||
}
|
||||
|
||||
routeEvent(hub: IConnectionHub<TContext>): Promise<Client<TContext>> {
|
||||
return this.route(hub);
|
||||
}
|
||||
|
||||
private async route(hub: IConnectionHub<TContext>): Promise<Client<TContext>> {
|
||||
for (const connection of hub.connections) {
|
||||
if (await Promise.resolve(this.fn(connection.ctx))) {
|
||||
return Promise.resolve(connection);
|
||||
}
|
||||
}
|
||||
|
||||
await Event.toPromise(hub.onDidChangeConnections);
|
||||
return await this.route(hub);
|
||||
}
|
||||
}
|
|
@ -6,12 +6,14 @@
|
|||
import * as assert from 'assert';
|
||||
import { Socket } from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Protocol, PersistentProtocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Protocol, PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
class MessageStream {
|
||||
|
||||
private _currentComplete: ((data: Buffer) => void) | null;
|
||||
private _messages: Buffer[];
|
||||
private _currentComplete: ((data: VSBuffer) => void) | null;
|
||||
private _messages: VSBuffer[];
|
||||
|
||||
constructor(x: Protocol | PersistentProtocol) {
|
||||
this._currentComplete = null;
|
||||
|
@ -36,8 +38,8 @@ class MessageStream {
|
|||
complete(msg);
|
||||
}
|
||||
|
||||
public waitForOne(): Promise<Buffer> {
|
||||
return new Promise<Buffer>((complete) => {
|
||||
public waitForOne(): Promise<VSBuffer> {
|
||||
return new Promise<VSBuffer>((complete) => {
|
||||
this._currentComplete = complete;
|
||||
this._trigger();
|
||||
});
|
||||
|
@ -53,6 +55,9 @@ class EtherStream extends EventEmitter {
|
|||
}
|
||||
|
||||
write(data: Buffer, cb?: Function): boolean {
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Invalid data`);
|
||||
}
|
||||
this._ether.write(this._name, data);
|
||||
return true;
|
||||
}
|
||||
|
@ -122,26 +127,26 @@ suite('IPC, Socket Protocol', () => {
|
|||
|
||||
test('read/write', async () => {
|
||||
|
||||
const a = new Protocol(ether.a);
|
||||
const b = new Protocol(ether.b);
|
||||
const a = new Protocol(new NodeSocket(ether.a));
|
||||
const b = new Protocol(new NodeSocket(ether.b));
|
||||
const bMessages = new MessageStream(b);
|
||||
|
||||
a.send(Buffer.from('foobarfarboo'));
|
||||
a.send(VSBuffer.fromString('foobarfarboo'));
|
||||
const msg1 = await bMessages.waitForOne();
|
||||
assert.equal(msg1.toString(), 'foobarfarboo');
|
||||
|
||||
const buffer = Buffer.allocUnsafe(1);
|
||||
buffer.writeInt8(123, 0);
|
||||
const buffer = VSBuffer.alloc(1);
|
||||
buffer.writeUint8(123, 0);
|
||||
a.send(buffer);
|
||||
const msg2 = await bMessages.waitForOne();
|
||||
assert.equal(msg2.readInt8(0), 123);
|
||||
assert.equal(msg2.readUint8(0), 123);
|
||||
});
|
||||
|
||||
|
||||
test('read/write, object data', async () => {
|
||||
|
||||
const a = new Protocol(ether.a);
|
||||
const b = new Protocol(ether.b);
|
||||
const a = new Protocol(new NodeSocket(ether.a));
|
||||
const b = new Protocol(new NodeSocket(ether.b));
|
||||
const bMessages = new MessageStream(b);
|
||||
|
||||
const data = {
|
||||
|
@ -151,7 +156,7 @@ suite('IPC, Socket Protocol', () => {
|
|||
data: 'Hello World'.split('')
|
||||
};
|
||||
|
||||
a.send(Buffer.from(JSON.stringify(data)));
|
||||
a.send(VSBuffer.fromString(JSON.stringify(data)));
|
||||
const msg = await bMessages.waitForOne();
|
||||
assert.deepEqual(JSON.parse(msg.toString()), data);
|
||||
});
|
||||
|
@ -166,20 +171,20 @@ suite('PersistentProtocol reconnection', () => {
|
|||
});
|
||||
|
||||
test('acks get piggybacked with messages', async () => {
|
||||
const a = new PersistentProtocol(ether.a);
|
||||
const a = new PersistentProtocol(new NodeSocket(ether.a));
|
||||
const aMessages = new MessageStream(a);
|
||||
const b = new PersistentProtocol(ether.b);
|
||||
const b = new PersistentProtocol(new NodeSocket(ether.b));
|
||||
const bMessages = new MessageStream(b);
|
||||
|
||||
a.send(Buffer.from('a1'));
|
||||
a.send(VSBuffer.fromString('a1'));
|
||||
assert.equal(a.unacknowledgedCount, 1);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
a.send(Buffer.from('a2'));
|
||||
a.send(VSBuffer.fromString('a2'));
|
||||
assert.equal(a.unacknowledgedCount, 2);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
a.send(Buffer.from('a3'));
|
||||
a.send(VSBuffer.fromString('a3'));
|
||||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
|
@ -198,7 +203,7 @@ suite('PersistentProtocol reconnection', () => {
|
|||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
b.send(Buffer.from('b1'));
|
||||
b.send(VSBuffer.fromString('b1'));
|
||||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 1);
|
||||
|
||||
|
@ -207,7 +212,7 @@ suite('PersistentProtocol reconnection', () => {
|
|||
assert.equal(a.unacknowledgedCount, 0);
|
||||
assert.equal(b.unacknowledgedCount, 1);
|
||||
|
||||
a.send(Buffer.from('a4'));
|
||||
a.send(VSBuffer.fromString('a4'));
|
||||
assert.equal(a.unacknowledgedCount, 1);
|
||||
assert.equal(b.unacknowledgedCount, 1);
|
||||
|
||||
|
|
|
@ -4,19 +4,19 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
class QueueProtocol implements IMessagePassingProtocol {
|
||||
|
||||
private buffering = true;
|
||||
private buffers: Buffer[] = [];
|
||||
private buffers: VSBuffer[] = [];
|
||||
|
||||
private _onMessage = new Emitter<Buffer>({
|
||||
private _onMessage = new Emitter<VSBuffer>({
|
||||
onFirstListenerDidAdd: () => {
|
||||
for (const buffer of this.buffers) {
|
||||
this._onMessage.fire(buffer);
|
||||
|
@ -33,11 +33,11 @@ class QueueProtocol implements IMessagePassingProtocol {
|
|||
readonly onMessage = this._onMessage.event;
|
||||
other: QueueProtocol;
|
||||
|
||||
send(buffer: Buffer): void {
|
||||
send(buffer: VSBuffer): void {
|
||||
this.other.receive(buffer);
|
||||
}
|
||||
|
||||
protected receive(buffer: Buffer): void {
|
||||
protected receive(buffer: VSBuffer): void {
|
||||
if (this.buffering) {
|
||||
this.buffers.push(buffer);
|
||||
} else {
|
||||
|
@ -196,10 +196,10 @@ suite('Base IPC', function () {
|
|||
test('createProtocolPair', async function () {
|
||||
const [clientProtocol, serverProtocol] = createProtocolPair();
|
||||
|
||||
const b1 = Buffer.alloc(0);
|
||||
const b1 = VSBuffer.alloc(0);
|
||||
clientProtocol.send(b1);
|
||||
|
||||
const b3 = Buffer.alloc(0);
|
||||
const b3 = VSBuffer.alloc(0);
|
||||
serverProtocol.send(b3);
|
||||
|
||||
const b2 = await Event.toPromise(serverProtocol.onMessage);
|
||||
|
|
|
@ -531,4 +531,30 @@ suite('Async', () => {
|
|||
assert.notEqual(r1Queue, r1Queue2); // previous one got disposed after finishing
|
||||
});
|
||||
});
|
||||
|
||||
test('retry - success case', async () => {
|
||||
let counter = 0;
|
||||
|
||||
const res = await async.retry(() => {
|
||||
counter++;
|
||||
if (counter < 2) {
|
||||
return Promise.reject(new Error('fail'));
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}, 10, 3);
|
||||
|
||||
assert.equal(res, true);
|
||||
});
|
||||
|
||||
test('retry - error case', async () => {
|
||||
let expectedError = new Error('fail');
|
||||
try {
|
||||
await async.retry(() => {
|
||||
return Promise.reject(expectedError);
|
||||
}, 10, 3);
|
||||
} catch (error) {
|
||||
assert.equal(error, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matc
|
|||
|
||||
function filterOk(filter: IFilter, word: string, wordToMatchAgainst: string, highlights?: { start: number; end: number; }[]) {
|
||||
let r = filter(word, wordToMatchAgainst);
|
||||
assert(r);
|
||||
assert(r, `${word} didn't match ${wordToMatchAgainst}`);
|
||||
if (highlights) {
|
||||
assert.deepEqual(r, highlights);
|
||||
}
|
||||
|
@ -202,6 +202,17 @@ suite('Filters', () => {
|
|||
|
||||
assert.ok(matchesWords('gipu', 'Category: Git: Pull', true) === null);
|
||||
assert.deepEqual(matchesWords('pu', 'Category: Git: Pull', true), [{ start: 15, end: 17 }]);
|
||||
|
||||
filterOk(matchesWords, 'bar', 'foo-bar');
|
||||
filterOk(matchesWords, 'bar test', 'foo-bar test');
|
||||
filterOk(matchesWords, 'fbt', 'foo-bar test');
|
||||
filterOk(matchesWords, 'bar test', 'foo-bar (test)');
|
||||
filterOk(matchesWords, 'foo bar', 'foo (bar)');
|
||||
|
||||
filterNotOk(matchesWords, 'bar est', 'foo-bar test');
|
||||
filterNotOk(matchesWords, 'fo ar', 'foo-bar test');
|
||||
filterNotOk(matchesWords, 'for', 'foo-bar test');
|
||||
filterNotOk(matchesWords, 'foo bar', 'foo-bar');
|
||||
});
|
||||
|
||||
function assertMatches(pattern: string, word: string, decoratedWord: string | undefined, filter: FuzzyScorer, opts: { patternPos?: number, wordPos?: number, firstMatchCanBeWeak?: boolean } = {}) {
|
||||
|
|
|
@ -246,41 +246,40 @@ suite('Extfs', () => {
|
|||
});
|
||||
|
||||
test('writeFileAndFlush (string)', function (done) {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
const newDir = path.join(parentDir, 'extfs', id);
|
||||
const testFile = path.join(newDir, 'flushed.txt');
|
||||
const smallData = 'Hello World';
|
||||
const bigData = (new Array(100 * 1024)).join('Large String\n');
|
||||
|
||||
mkdirp(newDir, 493, error => {
|
||||
if (error) {
|
||||
return done(error);
|
||||
}
|
||||
testWriteFileAndFlush(smallData, smallData, bigData, bigData, done);
|
||||
});
|
||||
|
||||
assert.ok(fs.existsSync(newDir));
|
||||
test('writeFileAndFlush (Buffer)', function (done) {
|
||||
const smallData = 'Hello World';
|
||||
const bigData = (new Array(100 * 1024)).join('Large String\n');
|
||||
|
||||
extfs.writeFileAndFlush(testFile, 'Hello World', null!, error => {
|
||||
if (error) {
|
||||
return done(error);
|
||||
}
|
||||
testWriteFileAndFlush(Buffer.from(smallData), smallData, Buffer.from(bigData), bigData, done);
|
||||
});
|
||||
|
||||
assert.equal(fs.readFileSync(testFile), 'Hello World');
|
||||
test('writeFileAndFlush (UInt8Array)', function (done) {
|
||||
const smallData = 'Hello World';
|
||||
const bigData = (new Array(100 * 1024)).join('Large String\n');
|
||||
|
||||
const largeString = (new Array(100 * 1024)).join('Large String\n');
|
||||
|
||||
extfs.writeFileAndFlush(testFile, largeString, null!, error => {
|
||||
if (error) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
assert.equal(fs.readFileSync(testFile), largeString);
|
||||
|
||||
extfs.del(parentDir, os.tmpdir(), done, ignore);
|
||||
});
|
||||
});
|
||||
});
|
||||
testWriteFileAndFlush(new TextEncoder().encode(smallData), smallData, new TextEncoder().encode(bigData), bigData, done);
|
||||
});
|
||||
|
||||
test('writeFileAndFlush (stream)', function (done) {
|
||||
const smallData = 'Hello World';
|
||||
const bigData = (new Array(100 * 1024)).join('Large String\n');
|
||||
|
||||
testWriteFileAndFlush(toReadable(smallData), smallData, toReadable(bigData), bigData, done);
|
||||
});
|
||||
|
||||
function testWriteFileAndFlush(
|
||||
smallData: string | Buffer | NodeJS.ReadableStream | Uint8Array,
|
||||
smallDataValue: string,
|
||||
bigData: string | Buffer | NodeJS.ReadableStream | Uint8Array,
|
||||
bigDataValue: string,
|
||||
done: (error: Error | null) => void
|
||||
): void {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
const newDir = path.join(parentDir, 'extfs', id);
|
||||
|
@ -293,27 +292,25 @@ suite('Extfs', () => {
|
|||
|
||||
assert.ok(fs.existsSync(newDir));
|
||||
|
||||
extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null!, error => {
|
||||
extfs.writeFileAndFlush(testFile, smallData, null!, error => {
|
||||
if (error) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
assert.equal(fs.readFileSync(testFile), 'Hello World');
|
||||
assert.equal(fs.readFileSync(testFile), smallDataValue);
|
||||
|
||||
const largeString = (new Array(100 * 1024)).join('Large String\n');
|
||||
|
||||
extfs.writeFileAndFlush(testFile, toReadable(largeString), null!, error => {
|
||||
extfs.writeFileAndFlush(testFile, bigData, null!, error => {
|
||||
if (error) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
assert.equal(fs.readFileSync(testFile), largeString);
|
||||
assert.equal(fs.readFileSync(testFile), bigDataValue);
|
||||
|
||||
extfs.del(parentDir, os.tmpdir(), done, ignore);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('writeFileAndFlush (file stream)', function (done) {
|
||||
const id = uuid.generateUuid();
|
||||
|
|
|
@ -16,7 +16,7 @@ import * as os from 'os';
|
|||
import { debounce } from 'vs/base/common/decorators';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { getDelayedChannel } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { IWindowConfiguration, IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
|
|
|
@ -41,14 +41,13 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
|||
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { DownloadService } from 'vs/platform/download/node/downloadService';
|
||||
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||
import { StaticRouter } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner';
|
||||
import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner';
|
||||
import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner';
|
||||
import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
export interface ISharedProcessConfiguration {
|
||||
readonly machineId: string;
|
||||
|
|
|
@ -14,7 +14,8 @@ import { getShellEnvironment } from 'vs/code/node/shellEnv';
|
|||
import { IUpdateService } from 'vs/platform/update/common/update';
|
||||
import { UpdateChannel } from 'vs/platform/update/node/updateIpc';
|
||||
import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main';
|
||||
import { Server, connect, Client } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { Server, connect } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { SharedProcess } from 'vs/code/electron-main/sharedProcess';
|
||||
import { LaunchService, LaunchChannel, ILaunchService } from 'vs/platform/launch/electron-main/launchService';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
@ -31,7 +32,7 @@ import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform
|
|||
import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
|
||||
import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import pkg from 'vs/platform/product/node/package';
|
||||
import { ProxyAuthHandler } from 'vs/code/electron-main/auth';
|
||||
|
@ -55,7 +56,7 @@ import { LogLevelSetterChannel } from 'vs/platform/log/node/logIpc';
|
|||
import * as errors from 'vs/base/common/errors';
|
||||
import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener';
|
||||
import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver';
|
||||
import { connectRemoteAgentManagement, ManagementPersistentConnection } from 'vs/platform/remote/node/remoteAgentConnection';
|
||||
import { connectRemoteAgentManagement, ManagementPersistentConnection, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { IMenubarService } from 'vs/platform/menubar/common/menubar';
|
||||
import { MenubarService } from 'vs/platform/menubar/electron-main/menubarService';
|
||||
import { MenubarChannel } from 'vs/platform/menubar/node/menubarIpc';
|
||||
|
@ -67,7 +68,7 @@ import { homedir } from 'os';
|
|||
import { join, sep } from 'vs/base/common/path';
|
||||
import { localize } from 'vs/nls';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/node/remoteAgentFileSystemChannel';
|
||||
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/common/remoteAgentFileSystemChannel';
|
||||
import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap';
|
||||
import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService';
|
||||
|
@ -79,6 +80,7 @@ import { HistoryMainService } from 'vs/platform/history/electron-main/historyMai
|
|||
import { URLService } from 'vs/platform/url/common/urlService';
|
||||
import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { nodeWebSocketFactory } from 'vs/platform/remote/node/nodeWebSocketFactory';
|
||||
|
||||
export class CodeApplication extends Disposable {
|
||||
|
||||
|
@ -575,16 +577,43 @@ export class CodeApplication extends Disposable {
|
|||
const hasFolderURIs = hasArgs(args['folder-uri']);
|
||||
const hasFileURIs = hasArgs(args['file-uri']);
|
||||
const noRecentEntry = args['skip-add-to-recently-opened'] === true;
|
||||
const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
|
||||
|
||||
if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
|
||||
return this.windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, noRecentEntry, initialStartup: true }); // new window if "-n" was used without paths
|
||||
// new window if "-n" was used without paths
|
||||
return this.windowsMainService.open({
|
||||
context,
|
||||
cli: args,
|
||||
forceNewWindow: true,
|
||||
forceEmpty: true,
|
||||
noRecentEntry,
|
||||
waitMarkerFileURI,
|
||||
initialStartup: true
|
||||
});
|
||||
}
|
||||
|
||||
if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
|
||||
return this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, urisToOpen: macOpenFiles.map(file => ({ uri: URI.file(file) })), noRecentEntry, initialStartup: true }); // mac: open-file event received on startup
|
||||
// mac: open-file event received on startup
|
||||
return this.windowsMainService.open({
|
||||
context: OpenContext.DOCK,
|
||||
cli: args,
|
||||
urisToOpen: macOpenFiles.map(file => ({ uri: URI.file(file) })),
|
||||
noRecentEntry,
|
||||
waitMarkerFileURI,
|
||||
initialStartup: true
|
||||
});
|
||||
}
|
||||
|
||||
return this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), diffMode: args.diff, noRecentEntry, initialStartup: true }); // default: read paths from cli
|
||||
// default: read paths from cli
|
||||
return this.windowsMainService.open({
|
||||
context,
|
||||
cli: args,
|
||||
forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
|
||||
diffMode: args.diff,
|
||||
noRecentEntry,
|
||||
waitMarkerFileURI,
|
||||
initialStartup: true
|
||||
});
|
||||
}
|
||||
|
||||
private afterWindowOpen(accessor: ServicesAccessor): void {
|
||||
|
@ -656,7 +685,17 @@ export class CodeApplication extends Disposable {
|
|||
|
||||
constructor(authority: string, host: string, port: number) {
|
||||
this._authority = authority;
|
||||
this._connection = connectRemoteAgentManagement(authority, host, port, `main`, isBuilt);
|
||||
const options: IConnectionOptions = {
|
||||
isBuilt: isBuilt,
|
||||
commit: product.commit,
|
||||
webSocketFactory: nodeWebSocketFactory,
|
||||
addressProvider: {
|
||||
getAddress: () => {
|
||||
return Promise.resolve({ host, port });
|
||||
}
|
||||
}
|
||||
};
|
||||
this._connection = connectRemoteAgentManagement(options, authority, `main`);
|
||||
this._disposeRunner = new RunOnceScheduler(() => this.dispose(), 5000);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ import { app, dialog } from 'electron';
|
|||
import { assign } from 'vs/base/common/objects';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { parseMainProcessArgv, createWaitMarkerFile } from 'vs/platform/environment/node/argvHelper';
|
||||
import { addArg } from 'vs/platform/environment/node/argv';
|
||||
import { parseMainProcessArgv } from 'vs/platform/environment/node/argvHelper';
|
||||
import { addArg, createWaitMarkerFile } from 'vs/platform/environment/node/argv';
|
||||
import { mkdirp } from 'vs/base/node/pfs';
|
||||
import { validatePaths } from 'vs/code/node/paths';
|
||||
import { LifecycleService, ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
|
||||
|
@ -142,7 +142,10 @@ function setupIPC(accessor: ServicesAccessor): Promise<Server> {
|
|||
if (environmentService.args.status) {
|
||||
return service.getMainProcessInfo().then(info => {
|
||||
return instantiationService.invokeFunction(accessor => {
|
||||
return accessor.get(IDiagnosticsService).printDiagnostics(info).then(() => Promise.reject(new ExpectedError()));
|
||||
return accessor.get(IDiagnosticsService).getDiagnostics(info).then(diagnostics => {
|
||||
console.log(diagnostics);
|
||||
return Promise.reject(new ExpectedError());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -212,7 +215,7 @@ function showStartupWarningDialog(message: string, detail: string): void {
|
|||
});
|
||||
}
|
||||
|
||||
function handleStartupDataDirError(environmentService: IEnvironmentService, error): void {
|
||||
function handleStartupDataDirError(environmentService: IEnvironmentService, error: NodeJS.ErrnoException): void {
|
||||
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
||||
showStartupWarningDialog(
|
||||
localize('startupDataDirError', "Unable to write program user data."),
|
||||
|
@ -357,20 +360,13 @@ function main(): void {
|
|||
// Note: we are not doing this if the wait marker has been already
|
||||
// added as argument. This can happen if Code was started from CLI.
|
||||
if (args.wait && !args.waitMarkerFilePath) {
|
||||
createWaitMarkerFile(args.verbose).then(waitMarkerFilePath => {
|
||||
if (waitMarkerFilePath) {
|
||||
addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath);
|
||||
args.waitMarkerFilePath = waitMarkerFilePath;
|
||||
}
|
||||
|
||||
startup(args);
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise just startup normally
|
||||
else {
|
||||
startup(args);
|
||||
const waitMarkerFilePath = createWaitMarkerFile(args.verbose);
|
||||
if (waitMarkerFilePath) {
|
||||
addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath);
|
||||
args.waitMarkerFilePath = waitMarkerFilePath;
|
||||
}
|
||||
}
|
||||
startup(args);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
@ -317,9 +317,6 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
|||
this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details: any, cb: any) => {
|
||||
this.marketplaceHeadersPromise.then(headers => {
|
||||
const requestHeaders = objects.assign(details.requestHeaders, headers);
|
||||
if (!this.configurationService.getValue('extensions.disableExperimentalAzureSearch')) {
|
||||
requestHeaders['Cookie'] = `${requestHeaders['Cookie'] ? requestHeaders['Cookie'] + ';' : ''}EnableExternalSearchForVSCode=true`;
|
||||
}
|
||||
cb({ cancel: false, requestHeaders });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -421,8 +421,8 @@ export class WindowsManager implements IWindowsMainService {
|
|||
}
|
||||
|
||||
// When run with --wait, make sure we keep the paths to wait for
|
||||
if (fileInputs && openConfig.cli.wait && openConfig.cli.waitMarkerFilePath) {
|
||||
fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpen, ...fileInputs.filesToCreate], waitMarkerFileUri: URI.file(openConfig.cli.waitMarkerFilePath) };
|
||||
if (fileInputs && openConfig.waitMarkerFileURI) {
|
||||
fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpen, ...fileInputs.filesToCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI };
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -506,8 +506,9 @@ export class WindowsManager implements IWindowsMainService {
|
|||
// If we got started with --wait from the CLI, we need to signal to the outside when the window
|
||||
// used for the edit operation is closed or loaded to a different folder so that the waiting
|
||||
// process can continue. We do this by deleting the waitMarkerFilePath.
|
||||
if (openConfig.context === OpenContext.CLI && openConfig.cli.wait && openConfig.cli.waitMarkerFilePath && usedWindows.length === 1 && usedWindows[0]) {
|
||||
this.waitForWindowCloseOrLoad(usedWindows[0].id).then(() => fs.unlink(openConfig.cli.waitMarkerFilePath!, _error => undefined));
|
||||
const waitMarkerFileURI = openConfig.waitMarkerFileURI;
|
||||
if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) {
|
||||
this.waitForWindowCloseOrLoad(usedWindows[0].id).then(() => fs.unlink(waitMarkerFileURI.fsPath, _error => undefined));
|
||||
}
|
||||
|
||||
return usedWindows;
|
||||
|
@ -1216,7 +1217,16 @@ export class WindowsManager implements IWindowsMainService {
|
|||
}
|
||||
|
||||
// Open it
|
||||
this.open({ context: openConfig.context, cli: openConfig.cli, forceNewWindow: true, forceEmpty: !cliArgs.length && !folderUris.length && !fileUris.length, userEnv: openConfig.userEnv, noRecentEntry: true });
|
||||
const openArgs: IOpenConfiguration = {
|
||||
context: openConfig.context,
|
||||
cli: openConfig.cli,
|
||||
forceNewWindow: true,
|
||||
forceEmpty: !cliArgs.length && !folderUris.length && !fileUris.length,
|
||||
userEnv: openConfig.userEnv,
|
||||
noRecentEntry: true,
|
||||
waitMarkerFileURI: openConfig.waitMarkerFileURI
|
||||
};
|
||||
this.open(openArgs);
|
||||
}
|
||||
|
||||
private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow {
|
||||
|
@ -1557,7 +1567,8 @@ export class WindowsManager implements IWindowsMainService {
|
|||
if (cli && (cli.remote !== remote)) {
|
||||
cli = { ...cli, remote };
|
||||
}
|
||||
return this.open({ context, cli, forceNewWindow: true, forceEmpty: true });
|
||||
const forceNewWindow = !(options && options.reuseWindow);
|
||||
return this.open({ context, cli, forceNewWindow, forceEmpty: true });
|
||||
}
|
||||
|
||||
openNewTabbedWindow(context: OpenContext): ICodeWindow[] {
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { buildHelpMessage, buildVersionMessage, addArg } from 'vs/platform/environment/node/argv';
|
||||
import { parseCLIProcessArgv, createWaitMarkerFile } from 'vs/platform/environment/node/argvHelper';
|
||||
import { buildHelpMessage, buildVersionMessage, addArg, createWaitMarkerFile } from 'vs/platform/environment/node/argv';
|
||||
import { parseCLIProcessArgv } from 'vs/platform/environment/node/argvHelper';
|
||||
import { ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import pkg from 'vs/platform/product/node/package';
|
||||
|
@ -229,7 +229,7 @@ export async function main(argv: string[]): Promise<any> {
|
|||
// is closed and then exit the waiting process.
|
||||
let waitMarkerFilePath: string | undefined;
|
||||
if (args.wait) {
|
||||
waitMarkerFilePath = await createWaitMarkerFile(verbose);
|
||||
waitMarkerFilePath = createWaitMarkerFile(verbose);
|
||||
if (waitMarkerFilePath) {
|
||||
addArg(argv, '--waitMarkerFilePath', waitMarkerFilePath);
|
||||
}
|
||||
|
|
|
@ -1567,9 +1567,9 @@ export interface ITokenizationRegistry {
|
|||
|
||||
/**
|
||||
* Get the tokenization support for a language.
|
||||
* Returns null if not found.
|
||||
* Returns `null` if not found.
|
||||
*/
|
||||
get(language: string): ITokenizationSupport;
|
||||
get(language: string): ITokenizationSupport | null;
|
||||
|
||||
/**
|
||||
* Get the promise of a tokenization support for a language.
|
||||
|
|
|
@ -57,7 +57,7 @@ export class RichEditSupport {
|
|||
public readonly indentationRules: IndentationRule | undefined;
|
||||
public readonly foldingRules: FoldingRules;
|
||||
|
||||
constructor(languageIdentifier: LanguageIdentifier, previous: RichEditSupport, rawConf: LanguageConfiguration) {
|
||||
constructor(languageIdentifier: LanguageIdentifier, previous: RichEditSupport | undefined, rawConf: LanguageConfiguration) {
|
||||
this._languageIdentifier = languageIdentifier;
|
||||
|
||||
this._brackets = null;
|
||||
|
@ -175,34 +175,30 @@ export class LanguageConfigurationChangeEvent {
|
|||
|
||||
export class LanguageConfigurationRegistryImpl {
|
||||
|
||||
private readonly _entries: RichEditSupport[];
|
||||
private readonly _entries = new Map<LanguageId, RichEditSupport>();
|
||||
|
||||
private readonly _onDidChange = new Emitter<LanguageConfigurationChangeEvent>();
|
||||
public readonly onDidChange: Event<LanguageConfigurationChangeEvent> = this._onDidChange.event;
|
||||
|
||||
constructor() {
|
||||
this._entries = [];
|
||||
}
|
||||
|
||||
public register(languageIdentifier: LanguageIdentifier, configuration: LanguageConfiguration): IDisposable {
|
||||
let previous = this._getRichEditSupport(languageIdentifier.id);
|
||||
let current = new RichEditSupport(languageIdentifier, previous, configuration);
|
||||
this._entries[languageIdentifier.id] = current;
|
||||
this._entries.set(languageIdentifier.id, current);
|
||||
this._onDidChange.fire({ languageIdentifier });
|
||||
return toDisposable(() => {
|
||||
if (this._entries[languageIdentifier.id] === current) {
|
||||
this._entries[languageIdentifier.id] = previous;
|
||||
if (this._entries.get(languageIdentifier.id) === current) {
|
||||
this._entries.set(languageIdentifier.id, previous);
|
||||
this._onDidChange.fire({ languageIdentifier });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _getRichEditSupport(languageId: LanguageId): RichEditSupport {
|
||||
return this._entries[languageId] || null;
|
||||
private _getRichEditSupport(languageId: LanguageId): RichEditSupport | undefined {
|
||||
return this._entries.get(languageId);
|
||||
}
|
||||
|
||||
public getIndentationRules(languageId: LanguageId) {
|
||||
let value = this._entries[languageId];
|
||||
const value = this._entries.get(languageId);
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
|
|
|
@ -7,11 +7,13 @@ import { Color } from 'vs/base/common/color';
|
|||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ColorId, ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent } from 'vs/editor/common/modes';
|
||||
import { withUndefinedAsNull } from 'vs/base/common/types';
|
||||
import { keys } from 'vs/base/common/map';
|
||||
|
||||
export class TokenizationRegistryImpl implements ITokenizationRegistry {
|
||||
|
||||
private readonly _map: { [language: string]: ITokenizationSupport };
|
||||
private readonly _promises: { [language: string]: Thenable<void> };
|
||||
private readonly _map = new Map<string, ITokenizationSupport>();
|
||||
private readonly _promises = new Map<string, Thenable<void>>();
|
||||
|
||||
private readonly _onDidChange = new Emitter<ITokenizationSupportChangedEvent>();
|
||||
public readonly onDidChange: Event<ITokenizationSupportChangedEvent> = this._onDidChange.event;
|
||||
|
@ -19,8 +21,6 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry {
|
|||
private _colorMap: Color[] | null;
|
||||
|
||||
constructor() {
|
||||
this._map = Object.create(null);
|
||||
this._promises = Object.create(null);
|
||||
this._colorMap = null;
|
||||
}
|
||||
|
||||
|
@ -32,13 +32,13 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry {
|
|||
}
|
||||
|
||||
public register(language: string, support: ITokenizationSupport) {
|
||||
this._map[language] = support;
|
||||
this._map.set(language, support);
|
||||
this.fire([language]);
|
||||
return toDisposable(() => {
|
||||
if (this._map[language] !== support) {
|
||||
if (this._map.get(language) !== support) {
|
||||
return;
|
||||
}
|
||||
delete this._map[language];
|
||||
this._map.delete(language);
|
||||
this.fire([language]);
|
||||
});
|
||||
}
|
||||
|
@ -48,13 +48,13 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry {
|
|||
let registration: IDisposable | null = null;
|
||||
let isDisposed: boolean = false;
|
||||
|
||||
this._promises[language] = supportPromise.then(support => {
|
||||
delete this._promises[language];
|
||||
this._promises.set(language, supportPromise.then(support => {
|
||||
this._promises.delete(language);
|
||||
if (isDisposed || !support) {
|
||||
return;
|
||||
}
|
||||
registration = this.register(language, support);
|
||||
});
|
||||
}));
|
||||
|
||||
return toDisposable(() => {
|
||||
isDisposed = true;
|
||||
|
@ -69,21 +69,21 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry {
|
|||
if (support) {
|
||||
return Promise.resolve(support);
|
||||
}
|
||||
const promise = this._promises[language];
|
||||
const promise = this._promises.get(language);
|
||||
if (promise) {
|
||||
return promise.then(_ => this.get(language));
|
||||
return promise.then(_ => this.get(language)!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public get(language: string): ITokenizationSupport {
|
||||
return (this._map[language] || null);
|
||||
public get(language: string): ITokenizationSupport | null {
|
||||
return withUndefinedAsNull(this._map.get(language));
|
||||
}
|
||||
|
||||
public setColorMap(colorMap: Color[]): void {
|
||||
this._colorMap = colorMap;
|
||||
this._onDidChange.fire({
|
||||
changedLanguages: Object.keys(this._map),
|
||||
changedLanguages: keys(this._map),
|
||||
changedColorMap: true
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export namespace AccessibilityHelpNLS {
|
||||
export const noSelection = nls.localize("noSelection", "No selection");
|
||||
export const singleSelectionRange = nls.localize("singleSelectionRange", "Line {0}, Column {1} ({2} selected)");
|
||||
export const singleSelection = nls.localize("singleSelection", "Line {0}, Column {1}");
|
||||
export const multiSelectionRange = nls.localize("multiSelectionRange", "{0} selections ({1} characters selected)");
|
||||
export const multiSelection = nls.localize("multiSelection", "{0} selections");
|
||||
export const emergencyConfOn = nls.localize("emergencyConfOn", "Now changing the setting `accessibilitySupport` to 'on'.");
|
||||
export const openingDocs = nls.localize("openingDocs", "Now opening the Editor Accessibility documentation page.");
|
||||
export const readonlyDiffEditor = nls.localize("readonlyDiffEditor", " in a read-only pane of a diff editor.");
|
||||
export const editableDiffEditor = nls.localize("editableDiffEditor", " in a pane of a diff editor.");
|
||||
export const readonlyEditor = nls.localize("readonlyEditor", " in a read-only code editor");
|
||||
export const editableEditor = nls.localize("editableEditor", " in a code editor");
|
||||
export const changeConfigToOnMac = nls.localize("changeConfigToOnMac", "To configure the editor to be optimized for usage with a Screen Reader press Command+E now.");
|
||||
export const changeConfigToOnWinLinux = nls.localize("changeConfigToOnWinLinux", "To configure the editor to be optimized for usage with a Screen Reader press Control+E now.");
|
||||
export const auto_on = nls.localize("auto_on", "The editor is configured to be optimized for usage with a Screen Reader.");
|
||||
export const auto_off = nls.localize("auto_off", "The editor is configured to never be optimized for usage with a Screen Reader, which is not the case at this time.");
|
||||
export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}.");
|
||||
export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding.");
|
||||
export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}.");
|
||||
export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding.");
|
||||
export const openDocMac = nls.localize("openDocMac", "Press Command+H now to open a browser window with more information related to editor accessibility.");
|
||||
export const openDocWinLinux = nls.localize("openDocWinLinux", "Press Control+H now to open a browser window with more information related to editor accessibility.");
|
||||
export const outroMsg = nls.localize("outroMsg", "You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape.");
|
||||
export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help");
|
||||
}
|
||||
|
||||
export namespace InspectTokensNLS {
|
||||
export const inspectTokensAction = nls.localize('inspectTokens', "Developer: Inspect Tokens");
|
||||
}
|
||||
|
||||
export namespace GoToLineNLS {
|
||||
export const gotoLineLabelValidLineAndColumn = nls.localize('gotoLineLabelValidLineAndColumn', "Go to line {0} and character {1}");
|
||||
export const gotoLineLabelValidLine = nls.localize('gotoLineLabelValidLine', "Go to line {0}");
|
||||
export const gotoLineLabelEmptyWithLineLimit = nls.localize('gotoLineLabelEmptyWithLineLimit', "Type a line number between 1 and {0} to navigate to");
|
||||
export const gotoLineLabelEmptyWithLineAndColumnLimit = nls.localize('gotoLineLabelEmptyWithLineAndColumnLimit', "Type a character between 1 and {0} to navigate to");
|
||||
export const gotoLineAriaLabel = nls.localize('gotoLineAriaLabel', "Current Line: {0}. Go to line {1}.");
|
||||
export const gotoLineActionInput = nls.localize('gotoLineActionInput', "Type a line number, followed by an optional colon and a character number to navigate to");
|
||||
export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line...");
|
||||
}
|
||||
|
||||
export namespace QuickCommandNLS {
|
||||
export const ariaLabelEntryWithKey = nls.localize('ariaLabelEntryWithKey', "{0}, {1}, commands");
|
||||
export const ariaLabelEntry = nls.localize('ariaLabelEntry', "{0}, commands");
|
||||
export const quickCommandActionInput = nls.localize('quickCommandActionInput', "Type the name of an action you want to execute");
|
||||
export const quickCommandActionLabel = nls.localize('quickCommandActionLabel', "Command Palette");
|
||||
}
|
||||
|
||||
export namespace QuickOutlineNLS {
|
||||
export const entryAriaLabel = nls.localize('entryAriaLabel', "{0}, symbols");
|
||||
export const quickOutlineActionInput = nls.localize('quickOutlineActionInput', "Type the name of an identifier you wish to navigate to");
|
||||
export const quickOutlineActionLabel = nls.localize('quickOutlineActionLabel', "Go to Symbol...");
|
||||
export const _symbols_ = nls.localize('symbols', "symbols ({0})");
|
||||
export const _modules_ = nls.localize('modules', "modules ({0})");
|
||||
export const _class_ = nls.localize('class', "classes ({0})");
|
||||
export const _interface_ = nls.localize('interface', "interfaces ({0})");
|
||||
export const _method_ = nls.localize('method', "methods ({0})");
|
||||
export const _function_ = nls.localize('function', "functions ({0})");
|
||||
export const _property_ = nls.localize('property', "properties ({0})");
|
||||
export const _variable_ = nls.localize('variable', "variables ({0})");
|
||||
export const _variable2_ = nls.localize('variable2', "variables ({0})");
|
||||
export const _constructor_ = nls.localize('_constructor', "constructors ({0})");
|
||||
export const _call_ = nls.localize('call', "calls ({0})");
|
||||
}
|
||||
|
||||
export namespace StandaloneCodeEditorNLS {
|
||||
export const editorViewAccessibleLabel = nls.localize('editorViewAccessibleLabel', "Editor content");
|
||||
export const accessibilityHelpMessageIE = nls.localize('accessibilityHelpMessageIE', "Press Ctrl+F1 for Accessibility Options.");
|
||||
export const accessibilityHelpMessage = nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options.");
|
||||
}
|
||||
|
||||
export namespace ToggleHighContrastNLS {
|
||||
export const toggleHighContrast = nls.localize('toggleHighContrast', "Toggle High Contrast Theme");
|
||||
}
|
||||
|
||||
export namespace SimpleServicesNLS {
|
||||
export const bulkEditServiceSummary = nls.localize('bulkEditServiceSummary', "Made {0} edits in {1} files");
|
||||
}
|
|
@ -18,8 +18,6 @@ export const ID_INDENT_PROVIDER = 'indent';
|
|||
export class IndentRangeProvider implements RangeProvider {
|
||||
readonly id = ID_INDENT_PROVIDER;
|
||||
|
||||
readonly decorations;
|
||||
|
||||
constructor(private readonly editorModel: ITextModel) {
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,9 @@ import { IModelService } from 'vs/editor/common/services/modelService';
|
|||
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
|
||||
export function alertFormattingEdits(edits: ISingleEditOperation[]): void {
|
||||
|
||||
|
@ -83,6 +86,32 @@ export function getRealAndSyntheticDocumentFormattersOrdered(model: ITextModel):
|
|||
return result;
|
||||
}
|
||||
|
||||
export async function formatDocumentRangeWithFirstProvider(
|
||||
accessor: ServicesAccessor,
|
||||
editorOrModel: ITextModel | IActiveCodeEditor,
|
||||
range: Range,
|
||||
token: CancellationToken
|
||||
): Promise<boolean> {
|
||||
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const statusBarService = accessor.get(IStatusbarService);
|
||||
const labelService = accessor.get(ILabelService);
|
||||
|
||||
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
|
||||
const [best, ...rest] = DocumentRangeFormattingEditProviderRegistry.ordered(model);
|
||||
if (!best) {
|
||||
return false;
|
||||
}
|
||||
const ret = await instaService.invokeFunction(formatDocumentRangeWithProvider, best, editorOrModel, range, token);
|
||||
if (rest.length > 0) {
|
||||
statusBarService.setStatusMessage(
|
||||
nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName),
|
||||
5 * 1000
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function formatDocumentRangeWithProvider(
|
||||
accessor: ServicesAccessor,
|
||||
provider: DocumentRangeFormattingEditProvider,
|
||||
|
@ -152,6 +181,31 @@ export async function formatDocumentRangeWithProvider(
|
|||
return true;
|
||||
}
|
||||
|
||||
export async function formatDocumentWithFirstProvider(
|
||||
accessor: ServicesAccessor,
|
||||
editorOrModel: ITextModel | IActiveCodeEditor,
|
||||
token: CancellationToken
|
||||
): Promise<boolean> {
|
||||
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const statusBarService = accessor.get(IStatusbarService);
|
||||
const labelService = accessor.get(ILabelService);
|
||||
|
||||
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
|
||||
const [best, ...rest] = getRealAndSyntheticDocumentFormattersOrdered(model);
|
||||
if (!best) {
|
||||
return false;
|
||||
}
|
||||
const ret = await instaService.invokeFunction(formatDocumentWithProvider, best, editorOrModel, token);
|
||||
if (rest.length > 0) {
|
||||
statusBarService.setStatusMessage(
|
||||
nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName),
|
||||
5 * 1000
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function formatDocumentWithProvider(
|
||||
accessor: ServicesAccessor,
|
||||
provider: DocumentFormattingEditProvider,
|
||||
|
|
|
@ -16,7 +16,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon';
|
|||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { getOnTypeFormattingEdits, formatDocumentWithProvider, formatDocumentRangeWithProvider, alertFormattingEdits, getRealAndSyntheticDocumentFormattersOrdered } from 'vs/editor/contrib/format/format';
|
||||
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithFirstProvider, formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format';
|
||||
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
|
||||
import * as nls from 'vs/nls';
|
||||
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
|
@ -211,12 +211,7 @@ class FormatOnPaste implements editorCommon.IEditorContribution {
|
|||
if (this.editor.getSelections().length > 1) {
|
||||
return;
|
||||
}
|
||||
const provider = DocumentRangeFormattingEditProviderRegistry.ordered(this.editor.getModel());
|
||||
if (provider.length !== 1) {
|
||||
// print status in n>1 case?
|
||||
return;
|
||||
}
|
||||
this._instantiationService.invokeFunction(formatDocumentRangeWithProvider, provider[0], this.editor, range, CancellationToken.None).catch(onUnexpectedError);
|
||||
this._instantiationService.invokeFunction(formatDocumentRangeWithFirstProvider, this.editor, range, CancellationToken.None).catch(onUnexpectedError);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +222,7 @@ class FormatDocumentAction extends EditorAction {
|
|||
id: 'editor.action.formatDocument',
|
||||
label: nls.localize('formatDocument.label', "Format Document"),
|
||||
alias: 'Format Document',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider, EditorContextKeys.hasMultipleDocumentFormattingProvider.toNegated()),
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider),
|
||||
kbOpts: {
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentFormattingProvider),
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F,
|
||||
|
@ -243,14 +238,9 @@ class FormatDocumentAction extends EditorAction {
|
|||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const model = editor.getModel();
|
||||
const [provider] = getRealAndSyntheticDocumentFormattersOrdered(model);
|
||||
if (provider) {
|
||||
await instaService.invokeFunction(formatDocumentWithProvider, provider, editor, CancellationToken.None);
|
||||
if (editor.hasModel()) {
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
await instaService.invokeFunction(formatDocumentWithFirstProvider, editor, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -262,7 +252,7 @@ class FormatSelectionAction extends EditorAction {
|
|||
id: 'editor.action.formatSelection',
|
||||
label: nls.localize('formatSelection.label', "Format Selection"),
|
||||
alias: 'Format Code',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider, EditorContextKeys.hasMultipleDocumentSelectionFormattingProvider.toNegated()),
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider),
|
||||
kbOpts: {
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentSelectionFormattingProvider),
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F),
|
||||
|
@ -281,14 +271,12 @@ class FormatSelectionAction extends EditorAction {
|
|||
return;
|
||||
}
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const [best] = DocumentRangeFormattingEditProviderRegistry.ordered(editor.getModel());
|
||||
if (best) {
|
||||
let range: Range = editor.getSelection();
|
||||
if (range.isEmpty()) {
|
||||
range = new Range(range.startLineNumber, 1, range.startLineNumber, editor.getModel().getLineMaxColumn(range.startLineNumber));
|
||||
}
|
||||
await instaService.invokeFunction(formatDocumentRangeWithProvider, best, editor, range, CancellationToken.None);
|
||||
const model = editor.getModel();
|
||||
let range: Range = editor.getSelection();
|
||||
if (range.isEmpty()) {
|
||||
range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber));
|
||||
}
|
||||
await instaService.invokeFunction(formatDocumentRangeWithFirstProvider, editor, range, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ import { Action } from 'vs/base/common/actions';
|
|||
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
|
@ -385,10 +386,10 @@ export class ModesContentHoverWidget extends ContentHoverWidget {
|
|||
model.guessColorPresentation(color, originalText);
|
||||
|
||||
const updateEditorModel = () => {
|
||||
let textEdits;
|
||||
let newRange;
|
||||
let textEdits: IIdentifiedSingleEditOperation[];
|
||||
let newRange: Range;
|
||||
if (model.presentation.textEdit) {
|
||||
textEdits = [model.presentation.textEdit];
|
||||
textEdits = [model.presentation.textEdit as IIdentifiedSingleEditOperation];
|
||||
newRange = new Range(
|
||||
model.presentation.textEdit.range.startLineNumber,
|
||||
model.presentation.textEdit.range.startColumn,
|
||||
|
@ -405,7 +406,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget {
|
|||
this._editor.executeEdits('colorpicker', textEdits);
|
||||
|
||||
if (model.presentation.additionalTextEdits) {
|
||||
textEdits = [...model.presentation.additionalTextEdits];
|
||||
textEdits = [...model.presentation.additionalTextEdits as IIdentifiedSingleEditOperation[]];
|
||||
this._editor.executeEdits('colorpicker', textEdits);
|
||||
this.hide();
|
||||
}
|
||||
|
|
|
@ -18,7 +18,38 @@ import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeE
|
|||
import { IOptions, IStyles, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
|
||||
|
||||
export const IPeekViewService = createDecorator<IPeekViewService>('IPeekViewService');
|
||||
export interface IPeekViewService {
|
||||
_serviceBrand: any;
|
||||
addExclusiveWidget(editor: ICodeEditor, widget: PeekViewWidget): void;
|
||||
}
|
||||
|
||||
registerSingleton(IPeekViewService, class implements IPeekViewService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private _widgets = new Map<ICodeEditor, { widget: PeekViewWidget, listener: IDisposable }>();
|
||||
|
||||
addExclusiveWidget(editor: ICodeEditor, widget: PeekViewWidget): void {
|
||||
const existing = this._widgets.get(editor);
|
||||
if (existing) {
|
||||
existing.listener.dispose();
|
||||
existing.widget.dispose();
|
||||
}
|
||||
const remove = () => {
|
||||
const data = this._widgets.get(editor);
|
||||
if (data && data.widget === widget) {
|
||||
data.listener.dispose();
|
||||
this._widgets.delete(editor);
|
||||
}
|
||||
};
|
||||
this._widgets.set(editor, { widget, listener: widget.onDidClose(remove) });
|
||||
}
|
||||
});
|
||||
|
||||
export namespace PeekContext {
|
||||
export const inPeekEditor = new RawContextKey<boolean>('inReferenceSearchEditor', true);
|
||||
|
|
|
@ -102,6 +102,7 @@ export abstract class ReferencesController implements editorCommon.IEditorContri
|
|||
this._widget = this._instantiationService.createInstance(ReferenceWidget, this._editor, this._defaultTreeKeyboardSupport, data);
|
||||
this._widget.setTitle(nls.localize('labelLoading', "Loading..."));
|
||||
this._widget.show(range);
|
||||
|
||||
this._disposables.push(this._widget.onDidClose(() => {
|
||||
modelPromise.cancel();
|
||||
if (this._widget) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { ISashEvent, IVerticalSashLayoutProvider, Sash } from 'vs/base/browser/ui/sash/sash';
|
||||
import { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
|
@ -29,12 +29,14 @@ import { ILabelService } from 'vs/platform/label/common/label';
|
|||
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
|
||||
import { activeContrastBorder, contrastBorder, registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { PeekViewWidget } from './peekViewWidget';
|
||||
import { PeekViewWidget, IPeekViewService } from './peekViewWidget';
|
||||
import { FileReferences, OneReference, ReferencesModel } from './referencesModel';
|
||||
import { ITreeRenderer, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree';
|
||||
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { FuzzyScore } from 'vs/base/common/filters';
|
||||
import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview';
|
||||
|
||||
|
||||
class DecorationsManager implements IDisposable {
|
||||
|
||||
|
@ -155,72 +157,6 @@ class DecorationsManager implements IDisposable {
|
|||
}
|
||||
}
|
||||
|
||||
class VSash {
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _sash: Sash;
|
||||
private _ratio: number;
|
||||
private _height: number;
|
||||
private _width: number;
|
||||
private _onDidChangePercentages = new Emitter<VSash>();
|
||||
|
||||
constructor(container: HTMLElement, ratio: number) {
|
||||
this._ratio = ratio;
|
||||
this._sash = new Sash(container, <IVerticalSashLayoutProvider>{
|
||||
getVerticalSashLeft: () => this._width * this._ratio,
|
||||
getVerticalSashHeight: () => this._height
|
||||
});
|
||||
|
||||
// compute the current widget clientX postion since
|
||||
// the sash works with clientX when dragging
|
||||
let clientX: number;
|
||||
this._disposables.push(this._sash.onDidStart((e: ISashEvent) => {
|
||||
clientX = e.startX - (this._width * this.ratio);
|
||||
}));
|
||||
|
||||
this._disposables.push(this._sash.onDidChange((e: ISashEvent) => {
|
||||
// compute the new position of the sash and from that
|
||||
// compute the new ratio that we are using
|
||||
let newLeft = e.currentX - clientX;
|
||||
if (newLeft > 20 && newLeft + 20 < this._width) {
|
||||
this._ratio = newLeft / this._width;
|
||||
this._sash.layout();
|
||||
this._onDidChangePercentages.fire(this);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._sash.dispose();
|
||||
this._onDidChangePercentages.dispose();
|
||||
dispose(this._disposables);
|
||||
}
|
||||
|
||||
get onDidChangePercentages() {
|
||||
return this._onDidChangePercentages.event;
|
||||
}
|
||||
|
||||
set width(value: number) {
|
||||
this._width = value;
|
||||
this._sash.layout();
|
||||
}
|
||||
|
||||
set height(value: number) {
|
||||
this._height = value;
|
||||
this._sash.layout();
|
||||
}
|
||||
|
||||
get percentages() {
|
||||
let left = 100 * this._ratio;
|
||||
let right = 100 - left;
|
||||
return [`${left}%`, `${right}%`];
|
||||
}
|
||||
|
||||
get ratio() {
|
||||
return this._ratio;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LayoutData {
|
||||
ratio: number;
|
||||
heightInLines: number;
|
||||
|
@ -248,14 +184,14 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
|
||||
private _tree: WorkbenchAsyncDataTree<ReferencesModel | FileReferences, TreeElement, FuzzyScore>;
|
||||
private _treeContainer: HTMLElement;
|
||||
private _sash: VSash;
|
||||
// private _sash: VSash;
|
||||
private _splitView: SplitView;
|
||||
private _preview: ICodeEditor;
|
||||
private _previewModelReference: IReference<ITextEditorModel>;
|
||||
private _previewNotAvailableMessage: TextModel;
|
||||
private _previewContainer: HTMLElement;
|
||||
private _messageContainer: HTMLElement;
|
||||
private height: number | undefined;
|
||||
private width: number | undefined;
|
||||
private _dim: dom.Dimension = { height: 0, width: 0 };
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
|
@ -264,15 +200,25 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
@IThemeService themeService: IThemeService,
|
||||
@ITextModelService private readonly _textModelResolverService: ITextModelService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IPeekViewService private readonly _peekViewService: IPeekViewService,
|
||||
@ILabelService private readonly _uriLabel: ILabelService
|
||||
) {
|
||||
super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true });
|
||||
|
||||
this._applyTheme(themeService.getTheme());
|
||||
this._callOnDispose.push(themeService.onThemeChange(this._applyTheme.bind(this)));
|
||||
this._peekViewService.addExclusiveWidget(editor, this);
|
||||
this.create();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.setModel(undefined);
|
||||
this._callOnDispose = dispose(this._callOnDispose);
|
||||
dispose<IDisposable>(this._preview, this._previewNotAvailableMessage, this._tree, this._previewModelReference);
|
||||
this._splitView.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _applyTheme(theme: ITheme) {
|
||||
const borderColor = theme.getColor(peekViewBorder) || Color.transparent;
|
||||
this.style({
|
||||
|
@ -284,13 +230,6 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.setModel(undefined);
|
||||
this._callOnDispose = dispose(this._callOnDispose);
|
||||
dispose<IDisposable>(this._preview, this._previewNotAvailableMessage, this._tree, this._sash, this._previewModelReference);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
get onDidSelectReference(): Event<SelectionEvent> {
|
||||
return this._onDidSelectReference.event;
|
||||
}
|
||||
|
@ -321,6 +260,8 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
this._messageContainer = dom.append(containerElement, dom.$('div.messages'));
|
||||
dom.hide(this._messageContainer);
|
||||
|
||||
this._splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL });
|
||||
|
||||
// editor
|
||||
this._previewContainer = dom.append(containerElement, dom.$('div.preview.inline'));
|
||||
let options: IEditorOptions = {
|
||||
|
@ -342,25 +283,8 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
dom.hide(this._previewContainer);
|
||||
this._previewNotAvailableMessage = TextModel.createFromString(nls.localize('missingPreviewMessage', "no preview available"));
|
||||
|
||||
// sash
|
||||
this._sash = new VSash(containerElement, this.layoutData.ratio || 0.8);
|
||||
this._sash.onDidChangePercentages(() => {
|
||||
let [left, right] = this._sash.percentages;
|
||||
this._previewContainer.style.width = left;
|
||||
this._treeContainer.style.width = right;
|
||||
this._preview.layout();
|
||||
this._tree.layout(this.height, this.width && this.width * (1 - this._sash.ratio));
|
||||
this.layoutData.ratio = this._sash.ratio;
|
||||
});
|
||||
|
||||
// tree
|
||||
this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline'));
|
||||
|
||||
const renderers = [
|
||||
this._instantiationService.createInstance(FileReferencesRenderer),
|
||||
this._instantiationService.createInstance(OneReferenceRenderer),
|
||||
];
|
||||
|
||||
const treeOptions: IAsyncDataTreeOptions<TreeElement, FuzzyScore> = {
|
||||
ariaLabel: nls.localize('treeAriaLabel', "References"),
|
||||
keyboardSupport: this._defaultTreeKeyboardSupport,
|
||||
|
@ -368,20 +292,48 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider),
|
||||
identityProvider: new IdentityProvider()
|
||||
};
|
||||
|
||||
const treeDataSource = this._instantiationService.createInstance(DataSource);
|
||||
|
||||
this._tree = this._instantiationService.createInstance<HTMLElement, IListVirtualDelegate<TreeElement>, ITreeRenderer<any, FuzzyScore, any>[], IAsyncDataSource<ReferencesModel | FileReferences, TreeElement>, IAsyncDataTreeOptions<TreeElement, FuzzyScore>, WorkbenchAsyncDataTree<ReferencesModel | FileReferences, TreeElement, FuzzyScore>>(
|
||||
WorkbenchAsyncDataTree,
|
||||
this._treeContainer,
|
||||
new Delegate(),
|
||||
renderers,
|
||||
treeDataSource,
|
||||
[
|
||||
this._instantiationService.createInstance(FileReferencesRenderer),
|
||||
this._instantiationService.createInstance(OneReferenceRenderer),
|
||||
],
|
||||
this._instantiationService.createInstance(DataSource),
|
||||
treeOptions
|
||||
);
|
||||
|
||||
ctxReferenceWidgetSearchTreeFocused.bindTo(this._tree.contextKeyService);
|
||||
|
||||
// split stuff
|
||||
this._splitView.addView({
|
||||
onDidChange: Event.None,
|
||||
element: this._previewContainer,
|
||||
minimumSize: 200,
|
||||
maximumSize: Number.MAX_VALUE,
|
||||
layout: (width) => {
|
||||
this._preview.layout({ height: this._dim.height, width });
|
||||
}
|
||||
}, Sizing.Distribute);
|
||||
|
||||
this._splitView.addView({
|
||||
onDidChange: Event.None,
|
||||
element: this._treeContainer,
|
||||
minimumSize: 100,
|
||||
maximumSize: Number.MAX_VALUE,
|
||||
layout: (width) => {
|
||||
this._treeContainer.style.height = `${this._dim.height}px`;
|
||||
this._treeContainer.style.width = `${width}px`;
|
||||
this._tree.layout(this._dim.height, width);
|
||||
}
|
||||
}, Sizing.Distribute);
|
||||
|
||||
this._splitView.onDidSashChange(() => {
|
||||
if (this._dim.width) {
|
||||
this.layoutData.ratio = this._splitView.getViewSize(0) / this._dim.width;
|
||||
}
|
||||
}, undefined, this._disposables);
|
||||
|
||||
// listen on selection and focus
|
||||
let onEvent = (element: any, kind: 'show' | 'goto' | 'side') => {
|
||||
if (element instanceof OneReference) {
|
||||
|
@ -428,36 +380,18 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
dom.hide(this._treeContainer);
|
||||
}
|
||||
|
||||
protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void {
|
||||
super._doLayoutBody(heightInPixel, widthInPixel);
|
||||
|
||||
this.height = heightInPixel;
|
||||
this.width = widthInPixel;
|
||||
|
||||
const height = heightInPixel + 'px';
|
||||
this._sash.height = heightInPixel;
|
||||
this._sash.width = widthInPixel;
|
||||
|
||||
// set height/width
|
||||
const [left, right] = this._sash.percentages;
|
||||
this._previewContainer.style.height = height;
|
||||
this._previewContainer.style.width = left;
|
||||
this._treeContainer.style.height = height;
|
||||
this._treeContainer.style.width = right;
|
||||
// forward
|
||||
this._tree.layout(heightInPixel, widthInPixel * (1 - this._sash.ratio));
|
||||
this._preview.layout();
|
||||
|
||||
// store layout data
|
||||
this.layoutData = {
|
||||
heightInLines: this._viewZone ? this._viewZone.heightInLines : 0,
|
||||
ratio: this._sash.ratio
|
||||
};
|
||||
protected _onWidth(width: number) {
|
||||
if (this._dim) {
|
||||
this._doLayoutBody(this._dim.height, width);
|
||||
}
|
||||
}
|
||||
|
||||
public _onWidth(widthInPixel: number): void {
|
||||
this._sash.width = widthInPixel;
|
||||
this._preview.layout();
|
||||
protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void {
|
||||
super._doLayoutBody(heightInPixel, widthInPixel);
|
||||
this._dim = { height: heightInPixel, width: widthInPixel };
|
||||
this.layoutData.heightInLines = this._viewZone ? this._viewZone.heightInLines : this.layoutData.heightInLines;
|
||||
this._splitView.layout(widthInPixel);
|
||||
this._splitView.resizeView(0, widthInPixel * this.layoutData.ratio);
|
||||
}
|
||||
|
||||
public setSelection(selection: OneReference): Promise<any> {
|
||||
|
@ -522,8 +456,7 @@ export class ReferenceWidget extends PeekViewWidget {
|
|||
dom.addClass(this.container, 'results-loaded');
|
||||
dom.show(this._treeContainer);
|
||||
dom.show(this._previewContainer);
|
||||
this._preview.layout();
|
||||
this._tree.layout();
|
||||
this._splitView.layout(this._dim.width);
|
||||
this.focus();
|
||||
|
||||
// pick input and a reference to begin with
|
||||
|
|
|
@ -116,7 +116,7 @@ export class RenameInputField implements IContentWidget, IDisposable {
|
|||
}
|
||||
|
||||
private _currentAcceptInput: (() => void) | null = null;
|
||||
private _currentCancelInput: ((focusEditor) => void) | null = null;
|
||||
private _currentCancelInput: ((focusEditor: boolean) => void) | null = null;
|
||||
|
||||
public acceptInput(): void {
|
||||
if (this._currentAcceptInput) {
|
||||
|
@ -144,7 +144,7 @@ export class RenameInputField implements IContentWidget, IDisposable {
|
|||
this._hide();
|
||||
};
|
||||
|
||||
return new Promise<string>(resolve => {
|
||||
return new Promise<string | boolean>(resolve => {
|
||||
|
||||
this._currentCancelInput = (focusEditor) => {
|
||||
this._currentAcceptInput = null;
|
||||
|
|
|
@ -41,3 +41,7 @@ import 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
|
|||
import 'vs/editor/contrib/wordHighlighter/wordHighlighter';
|
||||
import 'vs/editor/contrib/wordOperations/wordOperations';
|
||||
import 'vs/editor/contrib/wordPartOperations/wordPartOperations';
|
||||
|
||||
// Load up these strings even in VSCode, even if they are not used
|
||||
// in order to get them translated
|
||||
import 'vs/editor/common/standaloneStrings';
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./accessibilityHelp';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
|
@ -31,6 +30,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
|
|||
import { contrastBorder, editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
const CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE = new RawContextKey<boolean>('accessibilityHelpWidgetVisible', false);
|
||||
|
||||
|
@ -72,31 +72,26 @@ class AccessibilityHelpController extends Disposable
|
|||
}
|
||||
}
|
||||
|
||||
const nlsNoSelection = nls.localize("noSelection", "No selection");
|
||||
const nlsSingleSelectionRange = nls.localize("singleSelectionRange", "Line {0}, Column {1} ({2} selected)");
|
||||
const nlsSingleSelection = nls.localize("singleSelection", "Line {0}, Column {1}");
|
||||
const nlsMultiSelectionRange = nls.localize("multiSelectionRange", "{0} selections ({1} characters selected)");
|
||||
const nlsMultiSelection = nls.localize("multiSelection", "{0} selections");
|
||||
|
||||
function getSelectionLabel(selections: Selection[] | null, charactersSelected: number): string {
|
||||
if (!selections || selections.length === 0) {
|
||||
return nlsNoSelection;
|
||||
return AccessibilityHelpNLS.noSelection;
|
||||
}
|
||||
|
||||
if (selections.length === 1) {
|
||||
if (charactersSelected) {
|
||||
return strings.format(nlsSingleSelectionRange, selections[0].positionLineNumber, selections[0].positionColumn, charactersSelected);
|
||||
return strings.format(AccessibilityHelpNLS.singleSelectionRange, selections[0].positionLineNumber, selections[0].positionColumn, charactersSelected);
|
||||
}
|
||||
|
||||
return strings.format(nlsSingleSelection, selections[0].positionLineNumber, selections[0].positionColumn);
|
||||
return strings.format(AccessibilityHelpNLS.singleSelection, selections[0].positionLineNumber, selections[0].positionColumn);
|
||||
}
|
||||
|
||||
if (charactersSelected) {
|
||||
return strings.format(nlsMultiSelectionRange, selections.length, charactersSelected);
|
||||
return strings.format(AccessibilityHelpNLS.multiSelectionRange, selections.length, charactersSelected);
|
||||
}
|
||||
|
||||
if (selections.length > 0) {
|
||||
return strings.format(nlsMultiSelection, selections.length);
|
||||
return strings.format(AccessibilityHelpNLS.multiSelection, selections.length);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
@ -151,7 +146,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget {
|
|||
}
|
||||
|
||||
if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_E)) {
|
||||
alert(nls.localize("emergencyConfOn", "Now changing the setting `accessibilitySupport` to 'on'."));
|
||||
alert(AccessibilityHelpNLS.emergencyConfOn);
|
||||
|
||||
this._editor.updateOptions({
|
||||
accessibilitySupport: 'on'
|
||||
|
@ -166,7 +161,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget {
|
|||
}
|
||||
|
||||
if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_H)) {
|
||||
alert(nls.localize("openingDocs", "Now opening the Editor Accessibility documentation page."));
|
||||
alert(AccessibilityHelpNLS.openingDocs);
|
||||
|
||||
let url = (<IEditorConstructionOptions>this._editor.getRawConfiguration()).accessibilityHelpUrl;
|
||||
if (typeof url === 'undefined') {
|
||||
|
@ -246,56 +241,52 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget {
|
|||
|
||||
if (opts.wrappingInfo.inDiffEditor) {
|
||||
if (opts.readOnly) {
|
||||
text += nls.localize("readonlyDiffEditor", " in a read-only pane of a diff editor.");
|
||||
text += AccessibilityHelpNLS.readonlyDiffEditor;
|
||||
} else {
|
||||
text += nls.localize("editableDiffEditor", " in a pane of a diff editor.");
|
||||
text += AccessibilityHelpNLS.editableDiffEditor;
|
||||
}
|
||||
} else {
|
||||
if (opts.readOnly) {
|
||||
text += nls.localize("readonlyEditor", " in a read-only code editor");
|
||||
text += AccessibilityHelpNLS.readonlyEditor;
|
||||
} else {
|
||||
text += nls.localize("editableEditor", " in a code editor");
|
||||
text += AccessibilityHelpNLS.editableEditor;
|
||||
}
|
||||
}
|
||||
|
||||
const turnOnMessage = (
|
||||
platform.isMacintosh
|
||||
? nls.localize("changeConfigToOnMac", "To configure the editor to be optimized for usage with a Screen Reader press Command+E now.")
|
||||
: nls.localize("changeConfigToOnWinLinux", "To configure the editor to be optimized for usage with a Screen Reader press Control+E now.")
|
||||
? AccessibilityHelpNLS.changeConfigToOnMac
|
||||
: AccessibilityHelpNLS.changeConfigToOnWinLinux
|
||||
);
|
||||
switch (opts.accessibilitySupport) {
|
||||
case AccessibilitySupport.Unknown:
|
||||
text += '\n\n - ' + turnOnMessage;
|
||||
break;
|
||||
case AccessibilitySupport.Enabled:
|
||||
text += '\n\n - ' + nls.localize("auto_on", "The editor is configured to be optimized for usage with a Screen Reader.");
|
||||
text += '\n\n - ' + AccessibilityHelpNLS.auto_on;
|
||||
break;
|
||||
case AccessibilitySupport.Disabled:
|
||||
text += '\n\n - ' + nls.localize("auto_off", "The editor is configured to never be optimized for usage with a Screen Reader, which is not the case at this time.");
|
||||
text += '\n\n - ' + AccessibilityHelpNLS.auto_off;
|
||||
text += ' ' + turnOnMessage;
|
||||
break;
|
||||
}
|
||||
|
||||
const NLS_TAB_FOCUS_MODE_ON = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}.");
|
||||
const NLS_TAB_FOCUS_MODE_ON_NO_KB = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding.");
|
||||
const NLS_TAB_FOCUS_MODE_OFF = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}.");
|
||||
const NLS_TAB_FOCUS_MODE_OFF_NO_KB = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding.");
|
||||
|
||||
if (opts.tabFocusMode) {
|
||||
text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, NLS_TAB_FOCUS_MODE_ON, NLS_TAB_FOCUS_MODE_ON_NO_KB);
|
||||
text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOnMsg, AccessibilityHelpNLS.tabFocusModeOnMsgNoKb);
|
||||
} else {
|
||||
text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, NLS_TAB_FOCUS_MODE_OFF, NLS_TAB_FOCUS_MODE_OFF_NO_KB);
|
||||
text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOffMsg, AccessibilityHelpNLS.tabFocusModeOffMsgNoKb);
|
||||
}
|
||||
|
||||
const openDocMessage = (
|
||||
platform.isMacintosh
|
||||
? nls.localize("openDocMac", "Press Command+H now to open a browser window with more information related to editor accessibility.")
|
||||
: nls.localize("openDocWinLinux", "Press Control+H now to open a browser window with more information related to editor accessibility.")
|
||||
? AccessibilityHelpNLS.openDocMac
|
||||
: AccessibilityHelpNLS.openDocWinLinux
|
||||
);
|
||||
|
||||
text += '\n\n - ' + openDocMessage;
|
||||
|
||||
text += '\n\n' + nls.localize("outroMsg", "You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape.");
|
||||
text += '\n\n' + AccessibilityHelpNLS.outroMsg;
|
||||
|
||||
this._contentDomNode.domNode.appendChild(renderFormattedText(text));
|
||||
// Per https://www.w3.org/TR/wai-aria/roles#document, Authors SHOULD provide a title or label for documents
|
||||
|
@ -337,7 +328,7 @@ class ShowAccessibilityHelpAction extends EditorAction {
|
|||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.showAccessibilityHelp',
|
||||
label: nls.localize("ShowAccessibilityHelpAction", "Show Accessibility Help"),
|
||||
label: AccessibilityHelpNLS.showAccessibilityHelpAction,
|
||||
alias: 'Show Accessibility Help',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./inspectTokens';
|
||||
import * as nls from 'vs/nls';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
@ -21,6 +20,7 @@ import { IModeService } from 'vs/editor/common/services/modeService';
|
|||
import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService';
|
||||
import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { HIGH_CONTRAST, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { InspectTokensNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
|
||||
class InspectTokensController extends Disposable implements IEditorContribution {
|
||||
|
@ -82,7 +82,7 @@ class InspectTokens extends EditorAction {
|
|||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.inspectTokens',
|
||||
label: nls.localize('inspectTokens', "Developer: Inspect Tokens"),
|
||||
label: InspectTokensNLS.inspectTokensAction,
|
||||
alias: 'Developer: Inspect Tokens',
|
||||
precondition: null
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./gotoLine';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { QuickOpenEntry, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel';
|
||||
import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen';
|
||||
|
@ -17,6 +17,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
|||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { BaseEditorQuickOpenAction, IDecorator } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { GoToLineNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
interface ParseResult {
|
||||
position: Position;
|
||||
|
@ -62,14 +63,14 @@ export class GotoLineEntry extends QuickOpenEntry {
|
|||
|
||||
if (isValid) {
|
||||
if (position.column && position.column > 1) {
|
||||
label = nls.localize('gotoLineLabelValidLineAndColumn', "Go to line {0} and character {1}", position.lineNumber, position.column);
|
||||
label = strings.format(GoToLineNLS.gotoLineLabelValidLineAndColumn, position.lineNumber, position.column);
|
||||
} else {
|
||||
label = nls.localize('gotoLineLabelValidLine', "Go to line {0}", position.lineNumber, position.column);
|
||||
label = strings.format(GoToLineNLS.gotoLineLabelValidLine, position.lineNumber);
|
||||
}
|
||||
} else if (position.lineNumber < 1 || position.lineNumber > (model ? model.getLineCount() : 0)) {
|
||||
label = nls.localize('gotoLineLabelEmptyWithLineLimit', "Type a line number between 1 and {0} to navigate to", model ? model.getLineCount() : 0);
|
||||
label = strings.format(GoToLineNLS.gotoLineLabelEmptyWithLineLimit, model ? model.getLineCount() : 0);
|
||||
} else {
|
||||
label = nls.localize('gotoLineLabelEmptyWithLineAndColumnLimit', "Type a character between 1 and {0} to navigate to", model ? model.getLineMaxColumn(position.lineNumber) : 0);
|
||||
label = strings.format(GoToLineNLS.gotoLineLabelEmptyWithLineAndColumnLimit, model ? model.getLineMaxColumn(position.lineNumber) : 0);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -86,7 +87,7 @@ export class GotoLineEntry extends QuickOpenEntry {
|
|||
getAriaLabel(): string {
|
||||
const position = this.editor.getPosition();
|
||||
const currentLine = position ? position.lineNumber : 0;
|
||||
return nls.localize('gotoLineAriaLabel', "Current Line: {0}. Go to line {0}.", currentLine, this.parseResult.label);
|
||||
return strings.format(GoToLineNLS.gotoLineAriaLabel, currentLine, this.parseResult.label);
|
||||
}
|
||||
|
||||
run(mode: Mode, _context: IEntryRunContext): boolean {
|
||||
|
@ -144,9 +145,9 @@ export class GotoLineEntry extends QuickOpenEntry {
|
|||
export class GotoLineAction extends BaseEditorQuickOpenAction {
|
||||
|
||||
constructor() {
|
||||
super(nls.localize('gotoLineActionInput', "Type a line number, followed by an optional colon and a character number to navigate to"), {
|
||||
super(GoToLineNLS.gotoLineActionInput, {
|
||||
id: 'editor.action.gotoLine',
|
||||
label: nls.localize('GotoLineAction.label', "Go to Line..."),
|
||||
label: GoToLineNLS.gotoLineActionLabel,
|
||||
alias: 'Go to Line...',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { matchesFuzzy } from 'vs/base/common/filters';
|
||||
|
@ -17,6 +17,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
|||
import { BaseEditorQuickOpenAction } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { QuickCommandNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
export class EditorActionCommandEntry extends QuickOpenEntryGroup {
|
||||
private readonly key: string;
|
||||
|
@ -40,10 +41,10 @@ export class EditorActionCommandEntry extends QuickOpenEntryGroup {
|
|||
|
||||
public getAriaLabel(): string {
|
||||
if (this.keyAriaLabel) {
|
||||
return nls.localize('ariaLabelEntryWithKey', "{0}, {1}, commands", this.getLabel(), this.keyAriaLabel);
|
||||
return strings.format(QuickCommandNLS.ariaLabelEntryWithKey, this.getLabel(), this.keyAriaLabel);
|
||||
}
|
||||
|
||||
return nls.localize('ariaLabelEntry', "{0}, commands", this.getLabel());
|
||||
return strings.format(QuickCommandNLS.ariaLabelEntry, this.getLabel());
|
||||
}
|
||||
|
||||
public getGroupLabel(): string {
|
||||
|
@ -77,9 +78,9 @@ export class EditorActionCommandEntry extends QuickOpenEntryGroup {
|
|||
export class QuickCommandAction extends BaseEditorQuickOpenAction {
|
||||
|
||||
constructor() {
|
||||
super(nls.localize('quickCommandActionInput', "Type the name of an action you want to execute"), {
|
||||
super(QuickCommandNLS.quickCommandActionInput, {
|
||||
id: 'editor.action.quickCommand',
|
||||
label: nls.localize('QuickCommandAction.label', "Command Palette"),
|
||||
label: QuickCommandNLS.quickCommandActionLabel,
|
||||
alias: 'Command Palette',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./quickOutline';
|
||||
import * as nls from 'vs/nls';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { matchesFuzzy } from 'vs/base/common/filters';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
|
@ -20,6 +19,7 @@ import { DocumentSymbol, DocumentSymbolProviderRegistry, symbolKindToCssClass }
|
|||
import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen';
|
||||
import { BaseEditorQuickOpenAction, IDecorator } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { QuickOutlineNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
let SCOPE_PREFIX = ':';
|
||||
|
||||
|
@ -48,7 +48,7 @@ export class SymbolEntry extends QuickOpenEntryGroup {
|
|||
}
|
||||
|
||||
public getAriaLabel(): string {
|
||||
return nls.localize('entryAriaLabel', "{0}, symbols", this.name);
|
||||
return strings.format(QuickOutlineNLS.entryAriaLabel, this.name);
|
||||
}
|
||||
|
||||
public getIcon(): string {
|
||||
|
@ -111,9 +111,9 @@ export class SymbolEntry extends QuickOpenEntryGroup {
|
|||
export class QuickOutlineAction extends BaseEditorQuickOpenAction {
|
||||
|
||||
constructor() {
|
||||
super(nls.localize('quickOutlineActionInput', "Type the name of an identifier you wish to navigate to"), {
|
||||
super(QuickOutlineNLS.quickOutlineActionInput, {
|
||||
id: 'editor.action.quickOutline',
|
||||
label: nls.localize('QuickOutlineAction.label', "Go to Symbol..."),
|
||||
label: QuickOutlineNLS.quickOutlineActionLabel,
|
||||
alias: 'Go to Symbol...',
|
||||
precondition: EditorContextKeys.hasDocumentSymbolProvider,
|
||||
kbOpts: {
|
||||
|
@ -249,7 +249,7 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction {
|
|||
|
||||
// Mark first entry as outline
|
||||
else if (results.length > 0) {
|
||||
results[0].setGroupLabel(nls.localize('symbols', "symbols ({0})", results.length));
|
||||
results[0].setGroupLabel(strings.format(QuickOutlineNLS._symbols_, results.length));
|
||||
}
|
||||
|
||||
return results;
|
||||
|
@ -257,16 +257,16 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction {
|
|||
|
||||
private typeToLabel(type: string, count: number): string {
|
||||
switch (type) {
|
||||
case 'module': return nls.localize('modules', "modules ({0})", count);
|
||||
case 'class': return nls.localize('class', "classes ({0})", count);
|
||||
case 'interface': return nls.localize('interface', "interfaces ({0})", count);
|
||||
case 'method': return nls.localize('method', "methods ({0})", count);
|
||||
case 'function': return nls.localize('function', "functions ({0})", count);
|
||||
case 'property': return nls.localize('property', "properties ({0})", count);
|
||||
case 'variable': return nls.localize('variable', "variables ({0})", count);
|
||||
case 'var': return nls.localize('variable2', "variables ({0})", count);
|
||||
case 'constructor': return nls.localize('_constructor', "constructors ({0})", count);
|
||||
case 'call': return nls.localize('call', "calls ({0})", count);
|
||||
case 'module': return strings.format(QuickOutlineNLS._modules_, count);
|
||||
case 'class': return strings.format(QuickOutlineNLS._class_, count);
|
||||
case 'interface': return strings.format(QuickOutlineNLS._interface_, count);
|
||||
case 'method': return strings.format(QuickOutlineNLS._method_, count);
|
||||
case 'function': return strings.format(QuickOutlineNLS._function_, count);
|
||||
case 'property': return strings.format(QuickOutlineNLS._property_, count);
|
||||
case 'variable': return strings.format(QuickOutlineNLS._variable_, count);
|
||||
case 'var': return strings.format(QuickOutlineNLS._variable2_, count);
|
||||
case 'constructor': return strings.format(QuickOutlineNLS._constructor_, count);
|
||||
case 'call': return strings.format(QuickOutlineNLS._call_, count);
|
||||
}
|
||||
|
||||
return type;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
@ -43,6 +43,7 @@ import { ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/
|
|||
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { ILayoutService, IDimension } from 'vs/platform/layout/browser/layoutService';
|
||||
import { SimpleServicesNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
export class SimpleModel implements IResolvedTextEditorModel {
|
||||
|
||||
|
@ -612,7 +613,7 @@ export class SimpleBulkEditService implements IBulkEditService {
|
|||
|
||||
return Promise.resolve({
|
||||
selection: undefined,
|
||||
ariaSummary: localize('summary', 'Made {0} edits in {1} files', totalEdits, totalFiles)
|
||||
ariaSummary: strings.format(SimpleServicesNLS.bulkEditServiceSummary, totalEdits, totalFiles)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as aria from 'vs/base/browser/ui/aria/aria';
|
||||
import { Disposable, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
|
@ -29,6 +28,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
|||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { StandaloneCodeEditorNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
/**
|
||||
* Description of an action contribution
|
||||
|
@ -168,11 +168,11 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon
|
|||
@IAccessibilityService accessibilityService: IAccessibilityService
|
||||
) {
|
||||
options = options || {};
|
||||
options.ariaLabel = options.ariaLabel || nls.localize('editorViewAccessibleLabel', "Editor content");
|
||||
options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel;
|
||||
options.ariaLabel = options.ariaLabel + ';' + (
|
||||
browser.isIE
|
||||
? nls.localize('accessibilityHelpMessageIE', "Press Ctrl+F1 for Accessibility Options.")
|
||||
: nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options.")
|
||||
? StandaloneCodeEditorNLS.accessibilityHelpMessageIE
|
||||
: StandaloneCodeEditorNLS.accessibilityHelpMessage
|
||||
);
|
||||
super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService);
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService';
|
||||
import { ToggleHighContrastNLS } from 'vs/editor/common/standaloneStrings';
|
||||
|
||||
class ToggleHighContrast extends EditorAction {
|
||||
|
||||
|
@ -15,7 +15,7 @@ class ToggleHighContrast extends EditorAction {
|
|||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.toggleHighContrast',
|
||||
label: nls.localize('toggleHighContrast', "Toggle High Contrast Theme"),
|
||||
label: ToggleHighContrastNLS.toggleHighContrast,
|
||||
alias: 'Toggle High Contrast Theme',
|
||||
precondition: null
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => {
|
|||
|
||||
test('TextToHtmlTokenizer 1', () => {
|
||||
let mode = new Mode();
|
||||
let support = TokenizationRegistry.get(mode.getId());
|
||||
let support = TokenizationRegistry.get(mode.getId())!;
|
||||
|
||||
let actual = tokenizeToString('.abc..def...gh', support);
|
||||
let expected = [
|
||||
|
@ -38,7 +38,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => {
|
|||
|
||||
test('TextToHtmlTokenizer 2', () => {
|
||||
let mode = new Mode();
|
||||
let support = TokenizationRegistry.get(mode.getId());
|
||||
let support = TokenizationRegistry.get(mode.getId())!;
|
||||
|
||||
let actual = tokenizeToString('.abc..def...gh\n.abc..def...gh', support);
|
||||
let expected1 = [
|
||||
|
|
|
@ -90,10 +90,10 @@ export function fillInActionBarActions(menu: IMenu, options: IMenuActionOptions
|
|||
}
|
||||
|
||||
// {{SQL CARBON EDIT}} add export modifier
|
||||
export function fillInActions(groups: [string, Array<MenuItemAction | SubmenuItemAction>][], target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, getAlternativeActions, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
|
||||
export function fillInActions(groups: [string, Array<MenuItemAction | SubmenuItemAction>][], target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
|
||||
for (let tuple of groups) {
|
||||
let [group, actions] = tuple;
|
||||
if (getAlternativeActions) {
|
||||
if (useAlternativeActions) {
|
||||
actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export const enum MenuId {
|
|||
DebugConsoleContext,
|
||||
DebugVariablesContext,
|
||||
DebugWatchContext,
|
||||
DebugToolbar,
|
||||
DebugToolBar,
|
||||
EditorContext,
|
||||
EditorTitle,
|
||||
EditorTitleContext,
|
||||
|
|
|
@ -29,6 +29,8 @@ export interface IConfigurationOverrides {
|
|||
|
||||
export const enum ConfigurationTarget {
|
||||
USER = 1,
|
||||
USER_LOCAL,
|
||||
USER_REMOTE,
|
||||
WORKSPACE,
|
||||
WORKSPACE_FOLDER,
|
||||
DEFAULT,
|
||||
|
@ -37,6 +39,8 @@ export const enum ConfigurationTarget {
|
|||
export function ConfigurationTargetToString(configurationTarget: ConfigurationTarget) {
|
||||
switch (configurationTarget) {
|
||||
case ConfigurationTarget.USER: return 'USER';
|
||||
case ConfigurationTarget.USER_LOCAL: return 'USER_LOCAL';
|
||||
case ConfigurationTarget.USER_REMOTE: return 'USER_REMOTE';
|
||||
case ConfigurationTarget.WORKSPACE: return 'WORKSPACE';
|
||||
case ConfigurationTarget.WORKSPACE_FOLDER: return 'WORKSPACE_FOLDER';
|
||||
case ConfigurationTarget.DEFAULT: return 'DEFAULT';
|
||||
|
@ -88,6 +92,8 @@ export interface IConfigurationService {
|
|||
inspect<T>(key: string, overrides?: IConfigurationOverrides): {
|
||||
default: T,
|
||||
user: T,
|
||||
userLocal?: T,
|
||||
userRemote?: T,
|
||||
workspace?: T,
|
||||
workspaceFolder?: T,
|
||||
memory?: T,
|
||||
|
|
|
@ -36,6 +36,10 @@ export class ConfigurationModel implements IConfigurationModel {
|
|||
return this.checkAndFreeze(this._keys);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._keys.length === 0 && Object.keys(this._contents).length === 0 && this._overrides.length === 0;
|
||||
}
|
||||
|
||||
getValue<V>(section: string | undefined): V {
|
||||
return section ? getConfigurationValue<any>(this.contents, section) : this.contents;
|
||||
}
|
||||
|
@ -284,7 +288,8 @@ export class Configuration {
|
|||
|
||||
constructor(
|
||||
private _defaultConfiguration: ConfigurationModel,
|
||||
private _userConfiguration: ConfigurationModel,
|
||||
private _localUserConfiguration: ConfigurationModel,
|
||||
private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(),
|
||||
private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(),
|
||||
private _folderConfigurations: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(),
|
||||
private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(),
|
||||
|
@ -323,6 +328,8 @@ export class Configuration {
|
|||
inspect<C>(key: string, overrides: IConfigurationOverrides, workspace: Workspace | undefined): {
|
||||
default: C,
|
||||
user: C,
|
||||
userLocal?: C,
|
||||
userRemote?: C,
|
||||
workspace?: C,
|
||||
workspaceFolder?: C
|
||||
memory?: C
|
||||
|
@ -333,7 +340,9 @@ export class Configuration {
|
|||
const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration;
|
||||
return {
|
||||
default: overrides.overrideIdentifier ? this._defaultConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._defaultConfiguration.freeze().getValue(key),
|
||||
user: overrides.overrideIdentifier ? this._userConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._userConfiguration.freeze().getValue(key),
|
||||
user: overrides.overrideIdentifier ? this.userConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.userConfiguration.freeze().getValue(key),
|
||||
userLocal: overrides.overrideIdentifier ? this.localUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.localUserConfiguration.freeze().getValue(key),
|
||||
userRemote: overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.remoteUserConfiguration.freeze().getValue(key),
|
||||
workspace: workspace ? overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._workspaceConfiguration.freeze().getValue(key) : undefined, //Check on workspace exists or not because _workspaceConfiguration is never null
|
||||
workspaceFolder: folderConfigurationModel ? overrides.overrideIdentifier ? folderConfigurationModel.freeze().override(overrides.overrideIdentifier).getValue(key) : folderConfigurationModel.freeze().getValue(key) : undefined,
|
||||
memory: overrides.overrideIdentifier ? memoryConfigurationModel.override(overrides.overrideIdentifier).getValue(key) : memoryConfigurationModel.getValue(key),
|
||||
|
@ -350,7 +359,7 @@ export class Configuration {
|
|||
const folderConfigurationModel = this.getFolderConfigurationModelForResource(undefined, workspace);
|
||||
return {
|
||||
default: this._defaultConfiguration.freeze().keys,
|
||||
user: this._userConfiguration.freeze().keys,
|
||||
user: this.userConfiguration.freeze().keys,
|
||||
workspace: this._workspaceConfiguration.freeze().keys,
|
||||
workspaceFolder: folderConfigurationModel ? folderConfigurationModel.freeze().keys : []
|
||||
};
|
||||
|
@ -362,8 +371,16 @@ export class Configuration {
|
|||
this._foldersConsolidatedConfigurations.clear();
|
||||
}
|
||||
|
||||
updateUserConfiguration(userConfiguration: ConfigurationModel): void {
|
||||
this._userConfiguration = userConfiguration;
|
||||
updateLocalUserConfiguration(localUserConfiguration: ConfigurationModel): void {
|
||||
this._localUserConfiguration = localUserConfiguration;
|
||||
this._userConfiguration = null;
|
||||
this._workspaceConsolidatedConfiguration = null;
|
||||
this._foldersConsolidatedConfigurations.clear();
|
||||
}
|
||||
|
||||
updateRemoteUserConfiguration(remoteUserConfiguration: ConfigurationModel): void {
|
||||
this._remoteUserConfiguration = remoteUserConfiguration;
|
||||
this._userConfiguration = null;
|
||||
this._workspaceConsolidatedConfiguration = null;
|
||||
this._foldersConsolidatedConfigurations.clear();
|
||||
}
|
||||
|
@ -380,7 +397,7 @@ export class Configuration {
|
|||
}
|
||||
|
||||
deleteFolderConfiguration(resource: URI): void {
|
||||
this.folders.delete(resource);
|
||||
this.folderConfigurations.delete(resource);
|
||||
this._foldersConsolidatedConfigurations.delete(resource);
|
||||
}
|
||||
|
||||
|
@ -388,15 +405,30 @@ export class Configuration {
|
|||
return this._defaultConfiguration;
|
||||
}
|
||||
|
||||
get user(): ConfigurationModel {
|
||||
private _userConfiguration: ConfigurationModel | null;
|
||||
get userConfiguration(): ConfigurationModel {
|
||||
if (!this._userConfiguration) {
|
||||
this._userConfiguration = this._remoteUserConfiguration.isEmpty() ? this._localUserConfiguration : this._localUserConfiguration.merge(this._remoteUserConfiguration);
|
||||
if (this._freeze) {
|
||||
this._userConfiguration.freeze();
|
||||
}
|
||||
}
|
||||
return this._userConfiguration;
|
||||
}
|
||||
|
||||
get workspace(): ConfigurationModel {
|
||||
get localUserConfiguration(): ConfigurationModel {
|
||||
return this._localUserConfiguration;
|
||||
}
|
||||
|
||||
get remoteUserConfiguration(): ConfigurationModel {
|
||||
return this._remoteUserConfiguration;
|
||||
}
|
||||
|
||||
get workspaceConfiguration(): ConfigurationModel {
|
||||
return this._workspaceConfiguration;
|
||||
}
|
||||
|
||||
protected get folders(): ResourceMap<ConfigurationModel> {
|
||||
protected get folderConfigurations(): ResourceMap<ConfigurationModel> {
|
||||
return this._folderConfigurations;
|
||||
}
|
||||
|
||||
|
@ -424,7 +456,7 @@ export class Configuration {
|
|||
|
||||
private getWorkspaceConsolidatedConfiguration(): ConfigurationModel {
|
||||
if (!this._workspaceConsolidatedConfiguration) {
|
||||
this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge(this._userConfiguration, this._workspaceConfiguration, this._memoryConfiguration);
|
||||
this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge(this.userConfiguration, this._workspaceConfiguration, this._memoryConfiguration);
|
||||
if (this._freeze) {
|
||||
this._workspaceConfiguration = this._workspaceConfiguration.freeze();
|
||||
}
|
||||
|
@ -468,9 +500,9 @@ export class Configuration {
|
|||
keys: this._defaultConfiguration.keys
|
||||
},
|
||||
user: {
|
||||
contents: this._userConfiguration.contents,
|
||||
overrides: this._userConfiguration.overrides,
|
||||
keys: this._userConfiguration.keys
|
||||
contents: this.userConfiguration.contents,
|
||||
overrides: this.userConfiguration.overrides,
|
||||
keys: this.userConfiguration.keys
|
||||
},
|
||||
workspace: {
|
||||
contents: this._workspaceConfiguration.contents,
|
||||
|
@ -489,7 +521,7 @@ export class Configuration {
|
|||
allKeys(workspace: Workspace | undefined): string[] {
|
||||
let keys = this.keys(workspace);
|
||||
let all = [...keys.default];
|
||||
const addKeys = (keys) => {
|
||||
const addKeys = (keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
if (all.indexOf(key) === -1) {
|
||||
all.push(key);
|
||||
|
@ -498,8 +530,8 @@ export class Configuration {
|
|||
};
|
||||
addKeys(keys.user);
|
||||
addKeys(keys.workspace);
|
||||
for (const resource of this.folders.keys()) {
|
||||
addKeys(this.folders.get(resource)!.keys);
|
||||
for (const resource of this.folderConfigurations.keys()) {
|
||||
addKeys(this.folderConfigurations.get(resource)!.keys);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
|
|
@ -3,13 +3,17 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { ConfigurationModelParser, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { ConfigWatcher } from 'vs/base/node/config';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
|
||||
export class UserConfiguration extends Disposable {
|
||||
export class NodeBasedUserConfiguration extends Disposable {
|
||||
|
||||
private userConfigModelWatcher: ConfigWatcher<ConfigurationModelParser>;
|
||||
private initializePromise: Promise<void>;
|
||||
|
@ -50,4 +54,53 @@ export class UserConfiguration extends Disposable {
|
|||
return this.initialize().then(() => new Promise<ConfigurationModel>(c => this.userConfigModelWatcher.reload(userConfigModelParser => c(userConfigModelParser.configurationModel))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class FileServiceBasedUserConfiguration extends Disposable {
|
||||
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
protected readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
|
||||
|
||||
constructor(
|
||||
private readonly configurationResource: URI,
|
||||
private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(fileService.onFileChanges(e => this.handleFileEvents(e)));
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
|
||||
this.fileService.watchFileChanges(this.configurationResource);
|
||||
this._register(toDisposable(() => this.fileService.unwatchFileChanges(this.configurationResource)));
|
||||
}
|
||||
|
||||
initialize(): Promise<ConfigurationModel> {
|
||||
return this.reload();
|
||||
}
|
||||
|
||||
reload(): Promise<ConfigurationModel> {
|
||||
return this.fileService.resolveContent(this.configurationResource)
|
||||
.then(content => content.value, () => {
|
||||
// File not found
|
||||
return '';
|
||||
}).then(content => {
|
||||
const parser = new ConfigurationModelParser(this.configurationResource.toString());
|
||||
parser.parse(content);
|
||||
return parser.configurationModel;
|
||||
});
|
||||
}
|
||||
|
||||
private handleFileEvents(event: FileChangesEvent): void {
|
||||
const events = event.changes;
|
||||
|
||||
let affectedByChanges = false;
|
||||
// Find changes that affect workspace file
|
||||
for (let i = 0, len = events.length; i < len && !affectedByChanges; i++) {
|
||||
affectedByChanges = resources.isEqual(this.configurationResource, events[i].resource);
|
||||
}
|
||||
|
||||
if (affectedByChanges) {
|
||||
this.reloadConfigurationScheduler.schedule();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,14 +11,14 @@ import { DefaultConfigurationModel, Configuration, ConfigurationChangeEvent, Con
|
|||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { UserConfiguration } from 'vs/platform/configuration/node/configuration';
|
||||
import { NodeBasedUserConfiguration } from 'vs/platform/configuration/node/configuration';
|
||||
|
||||
export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _configuration: Configuration;
|
||||
private userConfiguration: UserConfiguration;
|
||||
private userConfiguration: NodeBasedUserConfiguration;
|
||||
|
||||
private readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
|
||||
readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
|
||||
|
@ -28,7 +28,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
|
|||
) {
|
||||
super();
|
||||
|
||||
this.userConfiguration = this._register(new UserConfiguration(environmentService.appSettingsPath));
|
||||
this.userConfiguration = this._register(new NodeBasedUserConfiguration(environmentService.appSettingsPath));
|
||||
|
||||
// Initialize
|
||||
const defaults = new DefaultConfigurationModel();
|
||||
|
@ -91,10 +91,10 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
|
|||
}
|
||||
|
||||
private onDidChangeUserConfiguration(userConfigurationModel: ConfigurationModel): void {
|
||||
const { added, updated, removed } = compare(this._configuration.user, userConfigurationModel);
|
||||
const { added, updated, removed } = compare(this._configuration.localUserConfiguration, userConfigurationModel);
|
||||
const changedKeys = [...added, ...updated, ...removed];
|
||||
if (changedKeys.length) {
|
||||
this._configuration.updateUserConfiguration(userConfigurationModel);
|
||||
this._configuration.updateLocalUserConfiguration(userConfigurationModel);
|
||||
this.trigger(changedKeys, ConfigurationTarget.USER);
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
|
|||
case ConfigurationTarget.DEFAULT:
|
||||
return this._configuration.defaults.contents;
|
||||
case ConfigurationTarget.USER:
|
||||
return this._configuration.user.contents;
|
||||
return this._configuration.localUserConfiguration.contents;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ export class TestConfigurationService implements IConfigurationService {
|
|||
public inspect<T>(key: string, overrides?: IConfigurationOverrides): {
|
||||
default: T,
|
||||
user: T,
|
||||
userLocal?: T,
|
||||
userRemote?: T,
|
||||
workspace?: T,
|
||||
workspaceFolder?: T
|
||||
value: T,
|
||||
|
|
|
@ -26,7 +26,7 @@ export interface IDiagnosticsService {
|
|||
formatEnvironment(info: IMainProcessInfo): string;
|
||||
getPerformanceInfo(info: IMainProcessInfo): Promise<PerformanceInfo>;
|
||||
getSystemInfo(info: IMainProcessInfo): SystemInfo;
|
||||
printDiagnostics(info: IMainProcessInfo): Promise<any>;
|
||||
getDiagnostics(info: IMainProcessInfo): Promise<string>;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
|
@ -156,28 +156,29 @@ export class DiagnosticsService implements IDiagnosticsService {
|
|||
return systemInfo;
|
||||
}
|
||||
|
||||
printDiagnostics(info: IMainProcessInfo): Promise<any> {
|
||||
getDiagnostics(info: IMainProcessInfo): Promise<string> {
|
||||
const output: string[] = [];
|
||||
return listProcesses(info.mainPID).then(rootProcess => {
|
||||
|
||||
// Environment Info
|
||||
console.log('');
|
||||
console.log(this.formatEnvironment(info));
|
||||
output.push('');
|
||||
output.push(this.formatEnvironment(info));
|
||||
|
||||
// Process List
|
||||
console.log('');
|
||||
console.log(this.formatProcessList(info, rootProcess));
|
||||
output.push('');
|
||||
output.push(this.formatProcessList(info, rootProcess));
|
||||
|
||||
// Workspace Stats
|
||||
const workspaceStatPromises: Promise<void>[] = [];
|
||||
if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) {
|
||||
console.log('');
|
||||
console.log('Workspace Stats: ');
|
||||
output.push('');
|
||||
output.push('Workspace Stats: ');
|
||||
info.windows.forEach(window => {
|
||||
if (window.folderURIs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`| Window (${window.title})`);
|
||||
output.push(`| Window (${window.title})`);
|
||||
|
||||
window.folderURIs.forEach(uriComponents => {
|
||||
const folderUri = URI.revive(uriComponents);
|
||||
|
@ -188,22 +189,24 @@ export class DiagnosticsService implements IDiagnosticsService {
|
|||
if (stats.maxFilesReached) {
|
||||
countMessage = `more than ${countMessage}`;
|
||||
}
|
||||
console.log(`| Folder (${basename(folder)}): ${countMessage}`);
|
||||
console.log(this.formatWorkspaceStats(stats));
|
||||
output.push(`| Folder (${basename(folder)}): ${countMessage}`);
|
||||
output.push(this.formatWorkspaceStats(stats));
|
||||
|
||||
}).catch(error => {
|
||||
console.log(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`);
|
||||
output.push(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`);
|
||||
}));
|
||||
} else {
|
||||
console.log(`| Folder (${folderUri.toString()}): Workspace stats not available.`);
|
||||
output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all(workspaceStatPromises).then(() => {
|
||||
console.log('');
|
||||
console.log('');
|
||||
output.push('');
|
||||
output.push('');
|
||||
|
||||
return output.join('\n');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -340,7 +343,7 @@ function asSortedItems(map: Map<string, number>): WorkspaceStatItem[] {
|
|||
// const errors: ParseError[] = [];
|
||||
// const json = parse(contents.toString(), errors);
|
||||
// if (errors.length) {
|
||||
// console.log(`Unable to parse ${launchConfig}`);
|
||||
// output.push(`Unable to parse ${launchConfig}`);
|
||||
// return resolve([]);
|
||||
// }
|
||||
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IDialogService, IDialogOptions, IConfirmation, IConfirmationResult, DialogType } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { Dialog } from 'vs/base/browser/ui/dialog/dialog';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachDialogStyler } from 'vs/platform/theme/common/styler';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class DialogService implements IDialogService {
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@ILayoutService private readonly layoutService: ILayoutService,
|
||||
@IThemeService private readonly themeService: IThemeService
|
||||
) { }
|
||||
|
||||
async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
|
||||
this.logService.trace('DialogService#confirm', confirmation.message);
|
||||
|
||||
const buttons: string[] = [];
|
||||
if (confirmation.primaryButton) {
|
||||
buttons.push(confirmation.primaryButton);
|
||||
} else {
|
||||
buttons.push(nls.localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));
|
||||
}
|
||||
|
||||
if (confirmation.secondaryButton) {
|
||||
buttons.push(confirmation.secondaryButton);
|
||||
} else if (typeof confirmation.secondaryButton === 'undefined') {
|
||||
buttons.push(nls.localize('cancelButton', "Cancel"));
|
||||
}
|
||||
|
||||
const severity = this.getSeverity(confirmation.type || 'none');
|
||||
const result = await this.show(severity, confirmation.message, buttons, { cancelId: 1, detail: confirmation.detail });
|
||||
|
||||
return { confirmed: result === 0 };
|
||||
}
|
||||
|
||||
private getSeverity(type: DialogType): Severity {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return Severity.Error;
|
||||
case 'warning':
|
||||
return Severity.Warning;
|
||||
case 'question':
|
||||
case 'info':
|
||||
return Severity.Info;
|
||||
case 'none':
|
||||
default:
|
||||
return Severity.Ignore;
|
||||
}
|
||||
}
|
||||
|
||||
private getDialogType(severity: Severity): DialogType {
|
||||
return (severity === Severity.Info) ? 'question' : (severity === Severity.Error) ? 'error' : (severity === Severity.Warning) ? 'warning' : 'none';
|
||||
}
|
||||
|
||||
|
||||
async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<number> {
|
||||
this.logService.trace('DialogService#show', message);
|
||||
|
||||
const dialogDisposables: IDisposable[] = [];
|
||||
const dialog = new Dialog(
|
||||
this.layoutService.container,
|
||||
message,
|
||||
buttons,
|
||||
{
|
||||
detail: options ? options.detail : undefined,
|
||||
cancelId: options ? options.cancelId : undefined,
|
||||
type: this.getDialogType(severity)
|
||||
});
|
||||
|
||||
dialogDisposables.push(dialog);
|
||||
dialogDisposables.push(attachDialogStyler(dialog, this.themeService));
|
||||
|
||||
const choice = await dialog.show();
|
||||
dispose(dialogDisposables);
|
||||
|
||||
return choice;
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IDialogService, DialogService, true);
|
|
@ -11,9 +11,11 @@ import { localize } from 'vs/nls';
|
|||
import { FileFilter } from 'vs/platform/windows/common/windows';
|
||||
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';
|
||||
|
||||
export interface IConfirmation {
|
||||
title?: string;
|
||||
type?: 'none' | 'info' | 'error' | 'question' | 'warning';
|
||||
type?: DialogType;
|
||||
message: string;
|
||||
detail?: string;
|
||||
primaryButton?: string;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
|||
import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { SimpleKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
|
||||
import { OS } from 'vs/base/common/platform';
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { connect as connectNet, Client } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
|
|
@ -7,6 +7,8 @@ import * as minimist from 'minimist';
|
|||
import * as os from 'os';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
import { join } from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
/**
|
||||
* This code is also used by standalone cli's. Avoid adding any other dependencies.
|
||||
|
@ -263,3 +265,20 @@ export function addArg(argv: string[], ...args: string[]): string[] {
|
|||
|
||||
return argv;
|
||||
}
|
||||
|
||||
export function createWaitMarkerFile(verbose?: boolean): string | undefined {
|
||||
const randomWaitMarkerPath = join(os.tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
|
||||
|
||||
try {
|
||||
writeFileSync(randomWaitMarkerPath, '');
|
||||
if (verbose) {
|
||||
console.log(`Marker file for --wait created: ${randomWaitMarkerPath}`);
|
||||
}
|
||||
return randomWaitMarkerPath;
|
||||
} catch (err) {
|
||||
if (verbose) {
|
||||
console.error(`Failed to create marker file for --wait: ${err}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,11 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { tmpdir } from 'os';
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ParsedArgs } from '../common/environment';
|
||||
import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files';
|
||||
import { parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { writeFile } from 'vs/base/node/pfs';
|
||||
|
||||
function validate(args: ParsedArgs): ParsedArgs {
|
||||
if (args.goto) {
|
||||
|
@ -60,21 +57,3 @@ export function parseCLIProcessArgv(processArgv: string[]): ParsedArgs {
|
|||
|
||||
return validate(parseArgs(args));
|
||||
}
|
||||
|
||||
export function createWaitMarkerFile(verbose?: boolean): Promise<string> {
|
||||
const randomWaitMarkerPath = join(tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
|
||||
|
||||
return writeFile(randomWaitMarkerPath, '').then(() => {
|
||||
if (verbose) {
|
||||
console.log(`Marker file for --wait created: ${randomWaitMarkerPath}`);
|
||||
}
|
||||
|
||||
return randomWaitMarkerPath;
|
||||
}, error => {
|
||||
if (verbose) {
|
||||
console.error(`Failed to create marker file for --wait: ${error}`);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@ export interface IFileService {
|
|||
*/
|
||||
canHandleResource(resource: URI): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the provider for the provided resource has the provided file system capability.
|
||||
*/
|
||||
hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise<boolean>;
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
|
@ -188,6 +193,7 @@ export interface FileOpenOptions {
|
|||
|
||||
export interface FileDeleteOptions {
|
||||
recursive: boolean;
|
||||
useTrash: boolean;
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
|
@ -215,7 +221,9 @@ export const enum FileSystemProviderCapabilities {
|
|||
FileFolderCopy = 1 << 3,
|
||||
|
||||
PathCaseSensitive = 1 << 10,
|
||||
Readonly = 1 << 11
|
||||
Readonly = 1 << 11,
|
||||
|
||||
Trash = 1 << 12
|
||||
}
|
||||
|
||||
export interface IFileSystemProvider {
|
||||
|
@ -243,6 +251,34 @@ export interface IFileSystemProvider {
|
|||
write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number>;
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderWithFileReadWriteCapability extends IFileSystemProvider {
|
||||
readFile(resource: URI): Promise<Uint8Array>;
|
||||
writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export function hasReadWriteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileReadWriteCapability {
|
||||
return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite);
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderWithFileFolderCopyCapability extends IFileSystemProvider {
|
||||
copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export function hasFileFolderCopyCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileFolderCopyCapability {
|
||||
return !!(provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy);
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderWithOpenReadWriteCloseCapability extends IFileSystemProvider {
|
||||
open(resource: URI, opts: FileOpenOptions): Promise<number>;
|
||||
close(fd: number): Promise<void>;
|
||||
read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number>;
|
||||
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number>;
|
||||
}
|
||||
|
||||
export function hasOpenReadWriteCloseCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithOpenReadWriteCloseCapability {
|
||||
return !!(provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose);
|
||||
}
|
||||
|
||||
export enum FileSystemProviderErrorCode {
|
||||
FileExists = 'EntryExists',
|
||||
FileNotFound = 'EntryNotFound',
|
||||
|
@ -1103,8 +1139,6 @@ export interface ILegacyFileService {
|
|||
|
||||
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStat>;
|
||||
|
||||
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
|
||||
|
||||
watchFileChanges(resource: URI): void;
|
||||
|
||||
unwatchFileChanges(resource: URI): void;
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Client, connect } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { connect } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { getDelayedChannel } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
export const ISharedProcessService = createDecorator<ISharedProcessService>('sharedProcessService');
|
||||
|
||||
|
|
|
@ -89,4 +89,5 @@ export interface IIssueService {
|
|||
_serviceBrand: any;
|
||||
openReporter(data: IssueReporterData): Promise<void>;
|
||||
openProcessExplorer(data: ProcessExplorerData): Promise<void>;
|
||||
getSystemStatus(): Promise<string>;
|
||||
}
|
||||
|
|
|
@ -25,4 +25,8 @@ export class IssueService implements IIssueService {
|
|||
openProcessExplorer(data: ProcessExplorerData): Promise<void> {
|
||||
return this.channel.call('openProcessExplorer', data);
|
||||
}
|
||||
|
||||
getSystemStatus(): Promise<string> {
|
||||
return this.channel.call('getSystemStatus');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,6 +214,12 @@ export class IssueService implements IIssueService {
|
|||
});
|
||||
}
|
||||
|
||||
public getSystemStatus(): Promise<string> {
|
||||
return this.launchService.getMainProcessInfo().then(info => {
|
||||
return this.diagnosticsService.getDiagnostics(info);
|
||||
});
|
||||
}
|
||||
|
||||
private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number): IWindowState {
|
||||
// We want the new window to open on the same display that the parent is in
|
||||
let displayToUse: Electron.Display | undefined;
|
||||
|
|
|
@ -21,6 +21,8 @@ export class IssueChannel implements IServerChannel {
|
|||
return this.service.openReporter(arg);
|
||||
case 'openProcessExplorer':
|
||||
return this.service.openProcessExplorer(arg);
|
||||
case 'getSystemStatus':
|
||||
return this.service.getSystemStatus();
|
||||
}
|
||||
|
||||
throw new Error(`Call not found: ${command}`);
|
||||
|
|
|
@ -163,9 +163,11 @@ export class LaunchService implements ILaunchService {
|
|||
const context = !!userEnv['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
|
||||
let usedWindows: ICodeWindow[] = [];
|
||||
|
||||
const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
|
||||
|
||||
// Special case extension development
|
||||
if (!!args.extensionDevelopmentPath) {
|
||||
this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context, cli: args, userEnv });
|
||||
this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context, cli: args, userEnv, waitMarkerFileURI });
|
||||
}
|
||||
|
||||
// Start without file/folder arguments
|
||||
|
@ -199,7 +201,14 @@ export class LaunchService implements ILaunchService {
|
|||
}
|
||||
|
||||
if (openNewWindow) {
|
||||
usedWindows = this.windowsMainService.open({ context, cli: args, userEnv, forceNewWindow: true, forceEmpty: true });
|
||||
usedWindows = this.windowsMainService.open({
|
||||
context,
|
||||
cli: args,
|
||||
userEnv,
|
||||
forceNewWindow: true,
|
||||
forceEmpty: true,
|
||||
waitMarkerFileURI
|
||||
});
|
||||
} else {
|
||||
usedWindows = [this.windowsMainService.focusLastActive(args, context)];
|
||||
}
|
||||
|
@ -216,7 +225,8 @@ export class LaunchService implements ILaunchService {
|
|||
forceReuseWindow: args['reuse-window'],
|
||||
diffMode: args.diff,
|
||||
addMode: args.add,
|
||||
noRecentEntry: !!args['skip-add-to-recently-opened']
|
||||
noRecentEntry: !!args['skip-add-to-recently-opened'],
|
||||
waitMarkerFileURI
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -232,10 +242,10 @@ export class LaunchService implements ILaunchService {
|
|||
// If the other instance is waiting to be killed, we hook up a window listener if one window
|
||||
// is being used and only then resolve the startup promise which will kill this second instance.
|
||||
// In addition, we poll for the wait marker file to be deleted to return.
|
||||
if (args.wait && args.waitMarkerFilePath && usedWindows.length === 1 && usedWindows[0]) {
|
||||
if (waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) {
|
||||
return Promise.race([
|
||||
this.windowsMainService.waitForWindowCloseOrLoad(usedWindows[0].id),
|
||||
whenDeleted(args.waitMarkerFilePath)
|
||||
whenDeleted(waitMarkerFileURI.fsPath)
|
||||
]).then(() => undefined, () => undefined);
|
||||
}
|
||||
|
||||
|
|
|
@ -195,6 +195,8 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
|
|||
valueSelection: Readonly<[number, number]> | undefined;
|
||||
|
||||
validationMessage: string | undefined;
|
||||
|
||||
inputHasFocus(): boolean;
|
||||
}
|
||||
|
||||
export interface IInputBox extends IQuickInput {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResolvedAuthority, IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
|
||||
export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
resolveAuthority(authority: string): Promise<ResolvedAuthority> {
|
||||
if (authority.indexOf(':') >= 0) {
|
||||
const pieces = authority.split(':');
|
||||
return Promise.resolve({ authority, host: pieces[0], port: parseInt(pieces[1], 10) });
|
||||
}
|
||||
return Promise.resolve({ authority, host: authority, port: 80 });
|
||||
}
|
||||
|
||||
setResolvedAuthority(resolvedAuthority: ResolvedAuthority) {
|
||||
throw new Error(`Not implemented`);
|
||||
}
|
||||
|
||||
setResolvedAuthorityError(authority: string, err: any): void {
|
||||
throw new Error(`Not implemented`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Client, PersistentProtocol, ISocket } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export const enum ConnectionType {
|
||||
Management = 1,
|
||||
ExtensionHost = 2,
|
||||
Tunnel = 3,
|
||||
}
|
||||
|
||||
interface ISimpleConnectionOptions {
|
||||
isBuilt: boolean;
|
||||
commit: string | undefined;
|
||||
host: string;
|
||||
port: number;
|
||||
reconnectionToken: string;
|
||||
reconnectionProtocol: PersistentProtocol | null;
|
||||
webSocketFactory: IWebSocketFactory;
|
||||
}
|
||||
|
||||
export interface IConnectCallback {
|
||||
(err: any | undefined, socket: ISocket | undefined): void;
|
||||
}
|
||||
|
||||
export interface IWebSocketFactory {
|
||||
connect(host: string, port: number, query: string, callback: IConnectCallback): void;
|
||||
}
|
||||
|
||||
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<PersistentProtocol> {
|
||||
throw new Error(`Not implemented`);
|
||||
}
|
||||
|
||||
interface IManagementConnectionResult {
|
||||
protocol: PersistentProtocol;
|
||||
}
|
||||
|
||||
async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions): Promise<IManagementConnectionResult> {
|
||||
const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.Management, undefined);
|
||||
return new Promise<IManagementConnectionResult>((c, e) => {
|
||||
const registration = protocol.onControlMessage(raw => {
|
||||
registration.dispose();
|
||||
const msg = JSON.parse(raw.toString());
|
||||
const error = getErrorFromMessage(msg);
|
||||
if (error) {
|
||||
return e(error);
|
||||
}
|
||||
if (options.reconnectionProtocol) {
|
||||
options.reconnectionProtocol.endAcceptReconnection();
|
||||
}
|
||||
c({ protocol });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface IRemoteExtensionHostStartParams {
|
||||
language: string;
|
||||
debugId?: string;
|
||||
break?: boolean;
|
||||
port?: number | null;
|
||||
updatePort?: boolean;
|
||||
}
|
||||
|
||||
interface IExtensionHostConnectionResult {
|
||||
protocol: PersistentProtocol;
|
||||
debugPort?: number;
|
||||
}
|
||||
|
||||
async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise<IExtensionHostConnectionResult> {
|
||||
const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.ExtensionHost, startArguments);
|
||||
return new Promise<IExtensionHostConnectionResult>((c, e) => {
|
||||
const registration = protocol.onControlMessage(raw => {
|
||||
registration.dispose();
|
||||
const msg = JSON.parse(raw.toString());
|
||||
const error = getErrorFromMessage(msg);
|
||||
if (error) {
|
||||
return e(error);
|
||||
}
|
||||
const debugPort = msg && msg.debugPort;
|
||||
if (options.reconnectionProtocol) {
|
||||
options.reconnectionProtocol.endAcceptReconnection();
|
||||
}
|
||||
c({ protocol, debugPort });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface ITunnelConnectionStartParams {
|
||||
port: number;
|
||||
}
|
||||
|
||||
async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, startParams: ITunnelConnectionStartParams): Promise<PersistentProtocol> {
|
||||
const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.Tunnel, startParams);
|
||||
return protocol;
|
||||
}
|
||||
|
||||
export interface IConnectionOptions {
|
||||
isBuilt: boolean;
|
||||
commit: string | undefined;
|
||||
webSocketFactory: IWebSocketFactory;
|
||||
addressProvider: IAddressProvider;
|
||||
}
|
||||
|
||||
async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise<ISimpleConnectionOptions> {
|
||||
const { host, port } = await options.addressProvider.getAddress();
|
||||
return {
|
||||
isBuilt: options.isBuilt,
|
||||
commit: options.commit,
|
||||
host: host,
|
||||
port: port,
|
||||
reconnectionToken: reconnectionToken,
|
||||
reconnectionProtocol: reconnectionProtocol,
|
||||
webSocketFactory: options.webSocketFactory,
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAddress {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface IAddressProvider {
|
||||
getAddress(): Promise<IAddress>;
|
||||
}
|
||||
|
||||
export async function connectRemoteAgentManagement(options: IConnectionOptions, remoteAuthority: string, clientId: string): Promise<ManagementPersistentConnection> {
|
||||
const reconnectionToken = generateUuid();
|
||||
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
|
||||
const { protocol } = await doConnectRemoteAgentManagement(simpleOptions);
|
||||
return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol);
|
||||
}
|
||||
|
||||
export async function connectRemoteAgentExtensionHost(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise<ExtensionHostPersistentConnection> {
|
||||
const reconnectionToken = generateUuid();
|
||||
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
|
||||
const { protocol, debugPort } = await doConnectRemoteAgentExtensionHost(simpleOptions, startArguments);
|
||||
return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort);
|
||||
}
|
||||
|
||||
export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise<PersistentProtocol> {
|
||||
const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null);
|
||||
const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort });
|
||||
return protocol;
|
||||
}
|
||||
|
||||
abstract class PersistentConnection extends Disposable {
|
||||
|
||||
protected readonly _options: IConnectionOptions;
|
||||
public readonly reconnectionToken: string;
|
||||
public readonly protocol: PersistentProtocol;
|
||||
|
||||
constructor(options: IConnectionOptions, reconnectionToken: string, protocol: PersistentProtocol) {
|
||||
super();
|
||||
this._options = options;
|
||||
this.reconnectionToken = reconnectionToken;
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
protected abstract _reconnect(options: ISimpleConnectionOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export class ManagementPersistentConnection extends PersistentConnection {
|
||||
|
||||
public readonly client: Client<RemoteAgentConnectionContext>;
|
||||
|
||||
constructor(options: IConnectionOptions, remoteAuthority: string, clientId: string, reconnectionToken: string, protocol: PersistentProtocol) {
|
||||
super(options, reconnectionToken, protocol);
|
||||
this.client = this._register(new Client<RemoteAgentConnectionContext>(protocol, {
|
||||
remoteAuthority: remoteAuthority,
|
||||
clientId: clientId
|
||||
}));
|
||||
}
|
||||
|
||||
protected async _reconnect(options: ISimpleConnectionOptions): Promise<void> {
|
||||
await doConnectRemoteAgentManagement(options);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionHostPersistentConnection extends PersistentConnection {
|
||||
|
||||
private readonly _startArguments: IRemoteExtensionHostStartParams;
|
||||
public readonly debugPort: number | undefined;
|
||||
|
||||
constructor(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams, reconnectionToken: string, protocol: PersistentProtocol, debugPort: number | undefined) {
|
||||
super(options, reconnectionToken, protocol);
|
||||
this._startArguments = startArguments;
|
||||
this.debugPort = debugPort;
|
||||
}
|
||||
|
||||
protected async _reconnect(options: ISimpleConnectionOptions): Promise<void> {
|
||||
await doConnectRemoteAgentExtensionHost(options, this._startArguments);
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorFromMessage(msg: any): Error | null {
|
||||
if (msg && msg.type === 'error') {
|
||||
const error = new Error(`Connection error: ${msg.reason}`);
|
||||
(<any>error).code = 'VSCODE_CONNECTION_ERROR';
|
||||
return error;
|
||||
}
|
||||
return null;
|
||||
}
|