Merge branch 'master' into notebook/outputs
This commit is contained in:
commit
b881f37fcc
|
@ -268,7 +268,6 @@
|
|||
"**/vs/base/{common,node,electron-main}/**",
|
||||
"**/vs/base/parts/*/{common,node,electron-main}/**",
|
||||
"**/vs/platform/*/{common,node,electron-main}/**",
|
||||
"**/vs/code/**",
|
||||
"*" // node modules
|
||||
]
|
||||
},
|
||||
|
@ -807,7 +806,6 @@
|
|||
"**/vs/platform/**/{common,node}/**",
|
||||
"**/vs/workbench/**/{common,node}/**",
|
||||
"**/vs/server/**",
|
||||
"**/vs/code/**/{common,node}/**",
|
||||
"*" // node modules
|
||||
]
|
||||
},
|
||||
|
@ -1009,6 +1007,7 @@
|
|||
"end",
|
||||
"expand",
|
||||
"hide",
|
||||
"invalidate",
|
||||
"open",
|
||||
"override",
|
||||
"receive",
|
||||
|
|
2
.github/commands.json
vendored
2
.github/commands.json
vendored
|
@ -93,7 +93,7 @@
|
|||
"addLabel": "z-author-verified",
|
||||
"removeLabel": "author-verification-requested",
|
||||
"requireLabel": "author-verification-requested",
|
||||
"disallowLabel": "awaiting-insiders-release"
|
||||
"disallowLabel": "unreleased"
|
||||
},
|
||||
{
|
||||
"type": "comment",
|
||||
|
|
1
.mailmap
1
.mailmap
|
@ -1,4 +1,5 @@
|
|||
Eric Amodio <eamodio@microsoft.com> Eric Amodio <eamodio@gmail.com>
|
||||
Eric Amodio <eamodio@microsoft.com> Eric Amodio <eamodio@ioninteractive.com>
|
||||
Daniel Imms <daimms@microsoft.com> Daniel Imms <tyriar@tyriar.com>
|
||||
Tanha Kabir <tanha.kabir@microsoft.com> Tanha Kabir <tanhakabir.ca@gmail.com>
|
||||
Raymond Zhao <raymondzhao@microsoft.com>
|
||||
|
|
|
@ -319,7 +319,7 @@
|
|||
"watch": "npm run build-preview && gulp watch-extension:markdown-language-features",
|
||||
"vscode:prepublish": "npm run build-ext && npm run build-preview",
|
||||
"build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json",
|
||||
"build-preview": "webpack --mode production",
|
||||
"build-preview": "npx webpack-cli --mode production",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"license": "MIT",
|
||||
"description": "Dependencies shared by all extensions",
|
||||
"dependencies": {
|
||||
"typescript": "^4.2.0-dev.20210201"
|
||||
"typescript": "insiders"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node ./postinstall"
|
||||
|
|
|
@ -1,220 +1,2 @@
|
|||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // create a fake namespace object
|
||||
/******/ // mode & 1: value is a module id, require it
|
||||
/******/ // mode & 2: merge all properties of value into the ns
|
||||
/******/ // mode & 4: return value when already ns object
|
||||
/******/ // mode & 8|1: behave like require
|
||||
/******/ __webpack_require__.t = function(value, mode) {
|
||||
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||
/******/ if(mode & 8) return value;
|
||||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||
/******/ var ns = Object.create(null);
|
||||
/******/ __webpack_require__.r(ns);
|
||||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||
/******/ return ns;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = "./preview-src/index.ts");
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ({
|
||||
|
||||
/***/ "./preview-src/events.ts":
|
||||
/*!*******************************!*\
|
||||
!*** ./preview-src/events.ts ***!
|
||||
\*******************************/
|
||||
/*! no static exports found */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"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 });
|
||||
exports.onceDocumentLoaded = void 0;
|
||||
function onceDocumentLoaded(f) {
|
||||
if (document.readyState === 'loading' || document.readyState === 'uninitialized') {
|
||||
document.addEventListener('DOMContentLoaded', f);
|
||||
}
|
||||
else {
|
||||
f();
|
||||
}
|
||||
}
|
||||
exports.onceDocumentLoaded = onceDocumentLoaded;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./preview-src/index.ts":
|
||||
/*!******************************!*\
|
||||
!*** ./preview-src/index.ts ***!
|
||||
\******************************/
|
||||
/*! no static exports found */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"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 events_1 = __webpack_require__(/*! ./events */ "./preview-src/events.ts");
|
||||
const vscode = acquireVsCodeApi();
|
||||
function getSettings() {
|
||||
const element = document.getElementById('simple-browser-settings');
|
||||
if (element) {
|
||||
const data = element.getAttribute('data-settings');
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not load settings`);
|
||||
}
|
||||
const settings = getSettings();
|
||||
const iframe = document.querySelector('iframe');
|
||||
const header = document.querySelector('.header');
|
||||
const input = header.querySelector('.url-input');
|
||||
const forwardButton = header.querySelector('.forward-button');
|
||||
const backButton = header.querySelector('.back-button');
|
||||
const reloadButton = header.querySelector('.reload-button');
|
||||
const openExternalButton = header.querySelector('.open-external-button');
|
||||
window.addEventListener('message', e => {
|
||||
switch (e.data.type) {
|
||||
case 'focus':
|
||||
{
|
||||
iframe.focus();
|
||||
break;
|
||||
}
|
||||
case 'didChangeFocusLockIndicatorEnabled':
|
||||
{
|
||||
toggleFocusLockIndicatorEnabled(e.data.enabled);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
events_1.onceDocumentLoaded(() => {
|
||||
setInterval(() => {
|
||||
var _a;
|
||||
const iframeFocused = ((_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) === 'IFRAME';
|
||||
document.body.classList.toggle('iframe-focused', iframeFocused);
|
||||
}, 50);
|
||||
iframe.addEventListener('load', () => {
|
||||
// Noop
|
||||
});
|
||||
input.addEventListener('change', e => {
|
||||
const url = e.target.value;
|
||||
navigateTo(url);
|
||||
});
|
||||
forwardButton.addEventListener('click', () => {
|
||||
history.forward();
|
||||
});
|
||||
backButton.addEventListener('click', () => {
|
||||
history.back();
|
||||
});
|
||||
openExternalButton.addEventListener('click', () => {
|
||||
vscode.postMessage({
|
||||
type: 'openExternal',
|
||||
url: input.value
|
||||
});
|
||||
});
|
||||
reloadButton.addEventListener('click', () => {
|
||||
// This does not seem to trigger what we want
|
||||
// history.go(0);
|
||||
// This incorrectly adds entries to the history but does reload
|
||||
iframe.src = input.value;
|
||||
});
|
||||
navigateTo(settings.url);
|
||||
input.value = settings.url;
|
||||
toggleFocusLockIndicatorEnabled(settings.focusLockIndicatorEnabled);
|
||||
function navigateTo(rawUrl) {
|
||||
try {
|
||||
const url = new URL(rawUrl);
|
||||
// Try to bust the cache for the iframe
|
||||
// There does not appear to be any way to reliably do this except modifying the url
|
||||
url.searchParams.append('vscodeBrowserReqId', Date.now().toString());
|
||||
iframe.src = url.toString();
|
||||
}
|
||||
catch (_a) {
|
||||
iframe.src = rawUrl;
|
||||
}
|
||||
}
|
||||
});
|
||||
function toggleFocusLockIndicatorEnabled(enabled) {
|
||||
document.body.classList.toggle('enable-focus-lock-indicator', enabled);
|
||||
}
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(1),r=acquireVsCodeApi();const c=function(){const e=document.getElementById("simple-browser-settings");if(e){const t=e.getAttribute("data-settings");if(t)return JSON.parse(t)}throw new Error("Could not load settings")}(),a=document.querySelector("iframe"),u=document.querySelector(".header"),d=u.querySelector(".url-input"),i=u.querySelector(".forward-button"),s=u.querySelector(".back-button"),l=u.querySelector(".reload-button"),f=u.querySelector(".open-external-button");function y(e){document.body.classList.toggle("enable-focus-lock-indicator",e)}window.addEventListener("message",e=>{switch(e.data.type){case"focus":a.focus();break;case"didChangeFocusLockIndicatorEnabled":y(e.data.enabled)}}),o.onceDocumentLoaded(()=>{function e(e){try{const t=new URL(e);t.searchParams.append("vscodeBrowserReqId",Date.now().toString()),a.src=t.toString()}catch(t){a.src=e}}setInterval(()=>{var e;const t="IFRAME"===(null===(e=document.activeElement)||void 0===e?void 0:e.tagName);document.body.classList.toggle("iframe-focused",t)},50),a.addEventListener("load",()=>{}),d.addEventListener("change",t=>{e(t.target.value)}),i.addEventListener("click",()=>{history.forward()}),s.addEventListener("click",()=>{history.back()}),f.addEventListener("click",()=>{r.postMessage({type:"openExternal",url:d.value})}),l.addEventListener("click",()=>{a.src=d.value}),e(c.url),d.value=c.url,y(c.focusLockIndicatorEnabled)})},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.onceDocumentLoaded=void 0,t.onceDocumentLoaded=function(e){"loading"===document.readyState||"uninitialized"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}}]);
|
||||
//# sourceMappingURL=index.js.map
|
File diff suppressed because one or more lines are too long
|
@ -54,8 +54,8 @@
|
|||
"watch": "npm run build-preview && gulp watch-extension:markdown-language-features",
|
||||
"vscode:prepublish": "npm run build-ext && npm run build-preview",
|
||||
"build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json",
|
||||
"build-preview": "webpack --mode development",
|
||||
"build-preview-production": "webpack --mode production",
|
||||
"build-preview": "npx webpack-cli --mode development",
|
||||
"build-preview-production": "npx webpack-cli --mode production",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose",
|
||||
"postinstall": "node ./build/copy"
|
||||
|
@ -66,11 +66,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"vscode-codicons": "^0.0.14",
|
||||
"@types/node": "^12.11.7",
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.7.3",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.3.0"
|
||||
"@types/node": "^12.11.7"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,410 +3,6 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface IDisposable {
|
||||
dispose(): void;
|
||||
export function activate() {
|
||||
// no-op. This extension may be removed in the future
|
||||
}
|
||||
|
||||
const enum Constants {
|
||||
ConfigSection = 'testing',
|
||||
EnableCodeLensConfig = 'enableCodeLens',
|
||||
EnableDiagnosticsConfig = 'enableProblemDiagnostics',
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const diagnostics = vscode.languages.createDiagnosticCollection();
|
||||
const services = new TestingEditorServices(diagnostics);
|
||||
context.subscriptions.push(
|
||||
services,
|
||||
diagnostics,
|
||||
vscode.languages.registerCodeLensProvider({ scheme: 'file' }, services),
|
||||
);
|
||||
}
|
||||
|
||||
class TestingConfig implements IDisposable {
|
||||
private section = vscode.workspace.getConfiguration(Constants.ConfigSection);
|
||||
private readonly changeEmitter = new vscode.EventEmitter<void>();
|
||||
private readonly listener = vscode.workspace.onDidChangeConfiguration(evt => {
|
||||
if (evt.affectsConfiguration(Constants.ConfigSection)) {
|
||||
this.section = vscode.workspace.getConfiguration(Constants.ConfigSection);
|
||||
this.changeEmitter.fire();
|
||||
}
|
||||
});
|
||||
|
||||
public readonly onChange = this.changeEmitter.event;
|
||||
|
||||
public get codeLens() {
|
||||
return this.section.get(Constants.EnableCodeLensConfig, true);
|
||||
}
|
||||
|
||||
public get diagnostics() {
|
||||
return this.section.get(Constants.EnableDiagnosticsConfig, false);
|
||||
}
|
||||
|
||||
public get isEnabled() {
|
||||
return this.codeLens || this.diagnostics;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.listener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class TestingEditorServices implements IDisposable, vscode.CodeLensProvider {
|
||||
private readonly codeLensChangeEmitter = new vscode.EventEmitter<void>();
|
||||
private readonly documents = new Map<string, DocumentTestObserver>();
|
||||
private readonly config = new TestingConfig();
|
||||
private disposables: IDisposable[];
|
||||
private wasEnabled = this.config.isEnabled;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event;
|
||||
|
||||
constructor(private readonly diagnostics: vscode.DiagnosticCollection) {
|
||||
this.disposables = [
|
||||
new vscode.Disposable(() => this.expireAll()),
|
||||
|
||||
this.config,
|
||||
|
||||
vscode.window.onDidChangeVisibleTextEditors((editors) => {
|
||||
if (!this.config.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiredEditors = new Set(this.documents.keys());
|
||||
for (const editor of editors) {
|
||||
const key = editor.document.uri.toString();
|
||||
this.ensure(key, editor.document);
|
||||
expiredEditors.delete(key);
|
||||
}
|
||||
|
||||
for (const expired of expiredEditors) {
|
||||
this.expire(expired);
|
||||
}
|
||||
}),
|
||||
|
||||
vscode.workspace.onDidCloseTextDocument((document) => {
|
||||
this.expire(document.uri.toString());
|
||||
}),
|
||||
|
||||
this.config.onChange(() => {
|
||||
if (!this.wasEnabled || this.config.isEnabled) {
|
||||
this.attachToAllVisible();
|
||||
} else if (this.wasEnabled || !this.config.isEnabled) {
|
||||
this.expireAll();
|
||||
}
|
||||
|
||||
this.wasEnabled = this.config.isEnabled;
|
||||
this.codeLensChangeEmitter.fire();
|
||||
}),
|
||||
];
|
||||
|
||||
if (this.config.isEnabled) {
|
||||
this.attachToAllVisible();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public provideCodeLenses(document: vscode.TextDocument) {
|
||||
if (!this.config.codeLens) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.documents.get(document.uri.toString())?.provideCodeLenses() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach to all currently visible editors.
|
||||
*/
|
||||
private attachToAllVisible() {
|
||||
for (const editor of vscode.window.visibleTextEditors) {
|
||||
this.ensure(editor.document.uri.toString(), editor.document);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unattaches to all tests.
|
||||
*/
|
||||
private expireAll() {
|
||||
for (const observer of this.documents.values()) {
|
||||
observer.dispose();
|
||||
}
|
||||
|
||||
this.documents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to tests for the document URI.
|
||||
*/
|
||||
private ensure(key: string, document: vscode.TextDocument) {
|
||||
const state = this.documents.get(key);
|
||||
if (!state) {
|
||||
const observer = new DocumentTestObserver(document, this.diagnostics, this.config);
|
||||
this.documents.set(key, observer);
|
||||
observer.onDidChangeCodeLenses(() => this.config.codeLens && this.codeLensChangeEmitter.fire());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expires and removes the watcher for the document.
|
||||
*/
|
||||
private expire(key: string) {
|
||||
const observer = this.documents.get(key);
|
||||
if (!observer) {
|
||||
return;
|
||||
}
|
||||
|
||||
observer.dispose();
|
||||
this.documents.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public dispose() {
|
||||
this.disposables.forEach((d) => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentTestObserver implements IDisposable {
|
||||
private readonly codeLensChangeEmitter = new vscode.EventEmitter<void>();
|
||||
private readonly observer = vscode.test.createDocumentTestObserver(this.document);
|
||||
private readonly disposables: IDisposable[];
|
||||
public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event;
|
||||
private didHaveDiagnostics = this.config.diagnostics;
|
||||
|
||||
constructor(
|
||||
private readonly document: vscode.TextDocument,
|
||||
private readonly diagnostics: vscode.DiagnosticCollection,
|
||||
private readonly config: TestingConfig,
|
||||
) {
|
||||
this.disposables = [
|
||||
this.observer,
|
||||
this.codeLensChangeEmitter,
|
||||
|
||||
config.onChange(() => {
|
||||
if (this.didHaveDiagnostics && !config.diagnostics) {
|
||||
this.diagnostics.set(document.uri, []);
|
||||
} else if (!this.didHaveDiagnostics && config.diagnostics) {
|
||||
this.updateDiagnostics();
|
||||
}
|
||||
|
||||
this.didHaveDiagnostics = config.diagnostics;
|
||||
}),
|
||||
|
||||
this.observer.onDidChangeTest(() => {
|
||||
this.updateDiagnostics();
|
||||
this.codeLensChangeEmitter.fire();
|
||||
}),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
private updateDiagnostics() {
|
||||
if (!this.config.diagnostics) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uriString = this.document.uri.toString();
|
||||
const diagnostics: vscode.Diagnostic[] = [];
|
||||
for (const test of iterateOverTests(this.observer.tests)) {
|
||||
for (const message of test.state.messages) {
|
||||
if (message.location?.uri.toString() === uriString) {
|
||||
diagnostics.push({
|
||||
range: message.location.range,
|
||||
message: message.message.toString(),
|
||||
severity: testToDiagnosticSeverity(message.severity),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.diagnostics.set(this.document.uri, diagnostics);
|
||||
}
|
||||
|
||||
public provideCodeLenses(): vscode.CodeLens[] {
|
||||
const lenses: vscode.CodeLens[] = [];
|
||||
|
||||
for (const test of iterateOverTests(this.observer.tests)) {
|
||||
const { debuggable = false, runnable = true } = test;
|
||||
if (!test.location || !(debuggable || runnable)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const summary = summarize(test);
|
||||
|
||||
lenses.push({
|
||||
isResolved: true,
|
||||
range: test.location.range,
|
||||
command: {
|
||||
title: `$(${testStateToIcon[summary.computedState]}) ${getLabelFor(test, summary)}`,
|
||||
command: 'vscode.runTests',
|
||||
arguments: [[test]],
|
||||
tooltip: localize('tooltip.debug', 'Debug {0}', test.label),
|
||||
},
|
||||
});
|
||||
|
||||
if (debuggable) {
|
||||
lenses.push({
|
||||
isResolved: true,
|
||||
range: test.location.range,
|
||||
command: {
|
||||
title: localize('action.debug', 'Debug'),
|
||||
command: 'vscode.debugTests',
|
||||
arguments: [[test]],
|
||||
tooltip: localize('tooltip.debug', 'Debug {0}', test.label),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return lenses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public dispose() {
|
||||
this.diagnostics.set(this.document.uri, []);
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelFor(test: vscode.TestItem, summary: ITestSummary) {
|
||||
if (summary.duration !== undefined) {
|
||||
return localize(
|
||||
'tooltip.runStateWithDuration',
|
||||
'{0}/{1} Tests Passed in {2}',
|
||||
summary.passed,
|
||||
summary.passed + summary.failed,
|
||||
formatDuration(summary.duration),
|
||||
);
|
||||
}
|
||||
|
||||
if (summary.passed > 0 || summary.failed > 0) {
|
||||
return localize('tooltip.runState', '{0}/{1} Tests Passed', summary.passed, summary.failed);
|
||||
}
|
||||
|
||||
if (test.state.runState === vscode.TestRunState.Passed) {
|
||||
return test.state.duration !== undefined
|
||||
? localize('state.passedWithDuration', 'Passed in {0}', formatDuration(test.state.duration))
|
||||
: localize('state.passed', 'Passed');
|
||||
}
|
||||
|
||||
if (isFailedState(test.state.runState)) {
|
||||
return localize('state.failed', 'Failed');
|
||||
}
|
||||
|
||||
return localize('action.run', 'Run Tests');
|
||||
}
|
||||
|
||||
function formatDuration(duration: number) {
|
||||
if (duration < 1_000) {
|
||||
return `${Math.round(duration)}ms`;
|
||||
}
|
||||
|
||||
if (duration < 100_000) {
|
||||
return `${(duration / 1000).toPrecision(3)}s`;
|
||||
}
|
||||
|
||||
return `${(duration / 1000 / 60).toPrecision(3)}m`;
|
||||
}
|
||||
|
||||
const statePriority: { [K in vscode.TestRunState]: number } = {
|
||||
[vscode.TestRunState.Running]: 6,
|
||||
[vscode.TestRunState.Queued]: 5,
|
||||
[vscode.TestRunState.Errored]: 4,
|
||||
[vscode.TestRunState.Failed]: 3,
|
||||
[vscode.TestRunState.Passed]: 2,
|
||||
[vscode.TestRunState.Skipped]: 1,
|
||||
[vscode.TestRunState.Unset]: 0,
|
||||
};
|
||||
|
||||
const maxPriority = (a: vscode.TestRunState, b: vscode.TestRunState) =>
|
||||
statePriority[a] > statePriority[b] ? a : b;
|
||||
|
||||
const isFailedState = (s: vscode.TestRunState) =>
|
||||
s === vscode.TestRunState.Failed || s === vscode.TestRunState.Errored;
|
||||
|
||||
interface ITestSummary {
|
||||
passed: number;
|
||||
failed: number;
|
||||
duration: number | undefined;
|
||||
computedState: vscode.TestRunState;
|
||||
}
|
||||
|
||||
function summarize(test: vscode.TestItem) {
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let duration: number | undefined;
|
||||
let computedState = test.state.runState;
|
||||
|
||||
const queue = test.children ? [test.children] : [];
|
||||
while (queue.length) {
|
||||
for (const test of queue.pop()!) {
|
||||
computedState = maxPriority(computedState, test.state.runState);
|
||||
if (test.state.runState === vscode.TestRunState.Passed) {
|
||||
passed++;
|
||||
if (test.state.duration !== undefined) {
|
||||
duration = test.state.duration + (duration ?? 0);
|
||||
}
|
||||
} else if (isFailedState(test.state.runState)) {
|
||||
failed++;
|
||||
if (test.state.duration !== undefined) {
|
||||
duration = test.state.duration + (duration ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (test.children) {
|
||||
queue.push(test.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { passed, failed, duration, computedState };
|
||||
}
|
||||
|
||||
function* iterateOverTests(tests: ReadonlyArray<vscode.TestItem>) {
|
||||
const queue = [tests];
|
||||
while (queue.length) {
|
||||
for (const test of queue.pop()!) {
|
||||
yield test;
|
||||
if (test.children) {
|
||||
queue.push(test.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testStateToIcon: { [K in vscode.TestRunState]: string } = {
|
||||
[vscode.TestRunState.Errored]: 'testing-error-icon',
|
||||
[vscode.TestRunState.Failed]: 'testing-failed-icon',
|
||||
[vscode.TestRunState.Passed]: 'testing-passed-icon',
|
||||
[vscode.TestRunState.Queued]: 'testing-queued-icon',
|
||||
[vscode.TestRunState.Skipped]: 'testing-skipped-icon',
|
||||
[vscode.TestRunState.Unset]: 'beaker',
|
||||
[vscode.TestRunState.Running]: 'loading~spin',
|
||||
};
|
||||
|
||||
const testToDiagnosticSeverity = (severity: vscode.TestMessageSeverity | undefined) => {
|
||||
switch (severity) {
|
||||
case vscode.TestMessageSeverity.Hint:
|
||||
return vscode.DiagnosticSeverity.Hint;
|
||||
case vscode.TestMessageSeverity.Information:
|
||||
return vscode.DiagnosticSeverity.Information;
|
||||
case vscode.TestMessageSeverity.Warning:
|
||||
return vscode.DiagnosticSeverity.Warning;
|
||||
case vscode.TestMessageSeverity.Error:
|
||||
default:
|
||||
return vscode.DiagnosticSeverity.Error;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -15,7 +15,9 @@ import * as languageModeIds from '../utils/languageModeIds';
|
|||
const jsTsLanguageConfiguration: vscode.LanguageConfiguration = {
|
||||
indentationRules: {
|
||||
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
|
||||
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
|
||||
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/,
|
||||
// e.g. * ...| or */| or *-----*/|
|
||||
unIndentedLinePattern: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/
|
||||
},
|
||||
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
|
||||
onEnterRules: [
|
||||
|
|
|
@ -67,4 +67,47 @@ suite.skip('OnEnter', () => {
|
|||
` x`));
|
||||
});
|
||||
});
|
||||
|
||||
test('should not indent after a multi-line comment block 1', () => {
|
||||
return withRandomFileEditor(`/*-----\n * line 1\n * line 2\n *-----*/\n${CURSOR}`, 'js', async (_editor, document) => {
|
||||
await type(document, '\nx');
|
||||
assert.strictEqual(
|
||||
document.getText(),
|
||||
joinLines(
|
||||
`/*-----`,
|
||||
` * line 1`,
|
||||
` * line 2`,
|
||||
` *-----*/`,
|
||||
``,
|
||||
`x`));
|
||||
});
|
||||
});
|
||||
|
||||
test('should not indent after a multi-line comment block 2', () => {
|
||||
return withRandomFileEditor(`/*-----\n * line 1\n * line 2\n */\n${CURSOR}`, 'js', async (_editor, document) => {
|
||||
await type(document, '\nx');
|
||||
assert.strictEqual(
|
||||
document.getText(),
|
||||
joinLines(
|
||||
`/*-----`,
|
||||
` * line 1`,
|
||||
` * line 2`,
|
||||
` */`,
|
||||
``,
|
||||
`x`));
|
||||
});
|
||||
});
|
||||
|
||||
test('should indent within a multi-line comment block', () => {
|
||||
return withRandomFileEditor(`/*-----\n * line 1\n * line 2${CURSOR}`, 'js', async (_editor, document) => {
|
||||
await type(document, '\nx');
|
||||
assert.strictEqual(
|
||||
document.getText(),
|
||||
joinLines(
|
||||
`/*-----`,
|
||||
` * line 1`,
|
||||
` * line 2`,
|
||||
` * x`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,10 +19,10 @@ fast-plist@0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8"
|
||||
integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg=
|
||||
|
||||
typescript@^4.2.0-dev.20210201:
|
||||
version "4.2.0-dev.20210201"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.0-dev.20210201.tgz#3d8ae7214cd4b23d3ae400f84d1afe5679f3e2f0"
|
||||
integrity sha512-By8G30ZYs+b/8084cSnjP7ILd8ExUBC4Qi9FY2iRlBApHu/A08ExLlCRnYkHN1PgxcNs4rTaozJsgXHJ6cg92g==
|
||||
typescript@insiders:
|
||||
version "4.2.0-insiders.20210210"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.0-insiders.20210210.tgz#f109217dec41af7ef0fece60a91e5c3cce572d25"
|
||||
integrity sha512-PBBQ/WNJ/HG+cH2nu+sPFbUPqv8FMraVihOfwlBzoa+0iGHa1I8gueJTUyBIOPU24tClLwKoU8dfscpn0rnuWw==
|
||||
|
||||
vscode-grammar-updater@^1.0.3:
|
||||
version "1.0.3"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.54.0",
|
||||
"distro": "1bc28dd86603be17b5bf6ffa9370864f58e0ef77",
|
||||
"distro": "f542aeaaf397d5c71e480442fe772dbe362726db",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
@ -195,7 +195,7 @@
|
|||
"underscore": "^1.8.2",
|
||||
"vinyl": "^2.0.0",
|
||||
"vinyl-fs": "^3.0.0",
|
||||
"vscode-debugprotocol": "1.44.0",
|
||||
"vscode-debugprotocol": "1.45.0-pre.0",
|
||||
"vscode-nls-dev": "^3.3.1",
|
||||
"vscode-telemetry-extractor": "^1.6.0",
|
||||
"webpack": "^4.43.0",
|
||||
|
@ -222,4 +222,4 @@
|
|||
"elliptic": "^6.5.3",
|
||||
"nwmatcher": "^1.4.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
|||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { getElementsByTagName, EventHelper, createStyleSheet, addDisposableListener, append, $ } from 'vs/base/browser/dom';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
|
||||
let DEBUG = false;
|
||||
// DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
|
||||
|
@ -87,6 +88,7 @@ export class Sash extends Disposable {
|
|||
private hidden: boolean;
|
||||
private orientation!: Orientation;
|
||||
private size: number;
|
||||
private hoverDelayer = this._register(new Delayer(300));
|
||||
|
||||
private _state: SashState = SashState.Enabled;
|
||||
get state(): SashState { return this._state; }
|
||||
|
@ -136,10 +138,10 @@ export class Sash extends Disposable {
|
|||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
|
||||
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove()));
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseenter', false)
|
||||
(() => sash.el.classList.add('hover'), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseleave', false)
|
||||
(() => sash.el.classList.remove('hover'), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseenter')
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseleave')
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -166,10 +168,10 @@ export class Sash extends Disposable {
|
|||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
|
||||
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove()));
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseenter', false)
|
||||
(() => sash.el.classList.add('hover'), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseleave', false)
|
||||
(() => sash.el.classList.remove('hover'), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseenter')
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseleave')
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -197,6 +199,8 @@ export class Sash extends Disposable {
|
|||
|
||||
this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this));
|
||||
this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this));
|
||||
this._register(domEvent(this.el, 'mouseenter')(() => Sash.onMouseEnter(this)));
|
||||
this._register(domEvent(this.el, 'mouseleave')(() => Sash.onMouseLeave(this)));
|
||||
|
||||
this._register(Gesture.addTarget(this.el));
|
||||
this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
|
||||
|
@ -394,6 +398,15 @@ export class Sash extends Disposable {
|
|||
}));
|
||||
}
|
||||
|
||||
private static onMouseEnter(sash: Sash): void {
|
||||
sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'));
|
||||
}
|
||||
|
||||
private static onMouseLeave(sash: Sash): void {
|
||||
sash.hoverDelayer.cancel();
|
||||
sash.el.classList.remove('hover');
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (this.orientation === Orientation.VERTICAL) {
|
||||
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
|
||||
|
|
|
@ -69,7 +69,7 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
|
|||
this.model.updateElementHeight(element, height);
|
||||
}
|
||||
|
||||
resort(element: T, recursive = true): void {
|
||||
resort(element: T | null, recursive = true): void {
|
||||
this.model.resort(element, recursive);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,13 @@ export interface ReadableStreamEvents<T> {
|
|||
/**
|
||||
* The 'data' event is emitted whenever the stream is
|
||||
* relinquishing ownership of a chunk of data to a consumer.
|
||||
*
|
||||
* NOTE: PLEASE UNDERSTAND THAT ADDING A DATA LISTENER CAN
|
||||
* TURN THE STREAM INTO FLOWING MODE. IT IS THEREFOR THE
|
||||
* LAST LISTENER THAT SHOULD BE ADDED AND NOT THE FIRST
|
||||
*
|
||||
* Use `listenStream` as a helper method to listen to
|
||||
* stream events in the right order.
|
||||
*/
|
||||
on(event: 'data', callback: (data: T) => void): void;
|
||||
|
||||
|
@ -268,7 +275,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
|
|||
// end with data or error if provided
|
||||
if (result instanceof Error) {
|
||||
this.error(result);
|
||||
} else if (result) {
|
||||
} else if (typeof result !== 'undefined') {
|
||||
this.write(result);
|
||||
}
|
||||
|
||||
|
@ -489,18 +496,74 @@ export function peekReadable<T>(readable: Readable<T>, reducer: IReducer<T>, max
|
|||
}
|
||||
|
||||
/**
|
||||
* Helper to fully read a T stream into a T.
|
||||
* Helper to fully read a T stream into a T or consuming
|
||||
* a stream fully, awaiting all the events without caring
|
||||
* about the data.
|
||||
*/
|
||||
export function consumeStream<T>(stream: ReadableStreamEvents<T>, reducer: IReducer<T>): Promise<T> {
|
||||
export function consumeStream<T>(stream: ReadableStreamEvents<T>, reducer: IReducer<T>): Promise<T>;
|
||||
export function consumeStream(stream: ReadableStreamEvents<unknown>): Promise<undefined>;
|
||||
export function consumeStream<T>(stream: ReadableStreamEvents<T>, reducer?: IReducer<T>): Promise<T | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: T[] = [];
|
||||
|
||||
stream.on('data', data => chunks.push(data));
|
||||
stream.on('error', error => reject(error));
|
||||
stream.on('end', () => resolve(reducer(chunks)));
|
||||
listenStream(stream, {
|
||||
onData: chunk => {
|
||||
if (reducer) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
if (reducer) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
},
|
||||
onEnd: () => {
|
||||
if (reducer) {
|
||||
resolve(reducer(chunks));
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface IStreamListener<T> {
|
||||
|
||||
/**
|
||||
* The 'data' event is emitted whenever the stream is
|
||||
* relinquishing ownership of a chunk of data to a consumer.
|
||||
*/
|
||||
onData(data: T): void;
|
||||
|
||||
/**
|
||||
* Emitted when any error occurs.
|
||||
*/
|
||||
onError(err: Error): void;
|
||||
|
||||
/**
|
||||
* The 'end' event is emitted when there is no more data
|
||||
* to be consumed from the stream. The 'end' event will
|
||||
* not be emitted unless the data is completely consumed.
|
||||
*/
|
||||
onEnd(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to listen to all events of a T stream in proper order.
|
||||
*/
|
||||
export function listenStream<T>(stream: ReadableStreamEvents<T>, listener: IStreamListener<T>): void {
|
||||
stream.on('error', error => listener.onError(error));
|
||||
stream.on('end', () => listener.onEnd());
|
||||
|
||||
// Adding the `data` listener will turn the stream
|
||||
// into flowing mode. As such it is important to
|
||||
// add this listener last (DO NOT CHANGE!)
|
||||
stream.on('data', data => listener.onData(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to peek up to `maxChunks` into a stream. The return type signals if
|
||||
* the stream has ended or not. If not, caller needs to add a `data` listener
|
||||
|
@ -509,9 +572,9 @@ export function consumeStream<T>(stream: ReadableStreamEvents<T>, reducer: IRedu
|
|||
export function peekStream<T>(stream: ReadableStream<T>, maxChunks: number): Promise<ReadableBufferedStream<T>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const streamListeners = new DisposableStore();
|
||||
const buffer: T[] = [];
|
||||
|
||||
// Data Listener
|
||||
const buffer: T[] = [];
|
||||
const dataListener = (chunk: T) => {
|
||||
|
||||
// Add to buffer
|
||||
|
@ -529,23 +592,27 @@ export function peekStream<T>(stream: ReadableStream<T>, maxChunks: number): Pro
|
|||
}
|
||||
};
|
||||
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('data', dataListener)));
|
||||
stream.on('data', dataListener);
|
||||
|
||||
// Error Listener
|
||||
const errorListener = (error: Error) => {
|
||||
return reject(error);
|
||||
};
|
||||
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('error', errorListener)));
|
||||
stream.on('error', errorListener);
|
||||
|
||||
// End Listener
|
||||
const endListener = () => {
|
||||
return resolve({ stream, buffer, ended: true });
|
||||
};
|
||||
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('error', errorListener)));
|
||||
stream.on('error', errorListener);
|
||||
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('end', endListener)));
|
||||
stream.on('end', endListener);
|
||||
|
||||
// Important: leave the `data` listener last because
|
||||
// this can turn the stream into flowing mode and we
|
||||
// want `error` events to be received as well.
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('data', dataListener)));
|
||||
stream.on('data', dataListener);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -585,46 +652,11 @@ export function toReadable<T>(t: T): Readable<T> {
|
|||
export function transform<Original, Transformed>(stream: ReadableStreamEvents<Original>, transformer: ITransformer<Original, Transformed>, reducer: IReducer<Transformed>): ReadableStream<Transformed> {
|
||||
const target = newWriteableStream<Transformed>(reducer);
|
||||
|
||||
stream.on('data', data => target.write(transformer.data(data)));
|
||||
stream.on('end', () => target.end());
|
||||
stream.on('error', error => target.error(transformer.error ? transformer.error(error) : error));
|
||||
listenStream(stream, {
|
||||
onData: data => target.write(transformer.data(data)),
|
||||
onError: error => target.error(transformer.error ? transformer.error(error) : error),
|
||||
onEnd: () => target.end()
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
export interface IReadableStreamObservable {
|
||||
|
||||
/**
|
||||
* A promise to await the `end` or `error` event
|
||||
* of a stream.
|
||||
*/
|
||||
errorOrEnd: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to observe a stream for certain events through
|
||||
* a promise based API.
|
||||
*/
|
||||
export function observe(stream: ReadableStream<unknown>): IReadableStreamObservable {
|
||||
|
||||
// A stream is closed when it ended or errord
|
||||
// We install this listener right from the
|
||||
// beginning to catch the events early.
|
||||
const errorOrEnd = Promise.race([
|
||||
new Promise<void>(resolve => stream.on('end', () => resolve())),
|
||||
new Promise<void>(resolve => stream.on('error', () => resolve()))
|
||||
]);
|
||||
|
||||
return {
|
||||
errorOrEnd(): Promise<void> {
|
||||
|
||||
// We need to ensure the stream is flowing so that our
|
||||
// listeners are getting triggered. It is possible that
|
||||
// the stream is not flowing because no `data` listener
|
||||
// was attached yet.
|
||||
stream.resume();
|
||||
|
||||
return errorOrEnd;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1023,146 +1023,141 @@ export class StaticRouter<TContext = string> implements IClientRouter<TContext>
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
//#region createChannelReceiver / createChannelSender
|
||||
|
||||
/**
|
||||
* Use both `createChannelReceiver` and `createChannelSender`
|
||||
* for automated process <=> process communication over methods
|
||||
* and events. You do not need to spell out each method on both
|
||||
* sides, a proxy will take care of this.
|
||||
* Use ProxyChannels to automatically wrapping and unwrapping
|
||||
* services to/from IPC channels, instead of manually wrapping
|
||||
* each service method and event.
|
||||
*
|
||||
* Rules:
|
||||
* - if marshalling is enabled, only `URI` and `RegExp` is converted
|
||||
* Restrictions:
|
||||
* - If marshalling is enabled, only `URI` and `RegExp` is converted
|
||||
* automatically for you
|
||||
* - events must follow the naming convention `onUppercase`
|
||||
* - Events must follow the naming convention `onUpperCase`
|
||||
* - `CancellationToken` is currently not supported
|
||||
* - if a context is provided, you can use `AddFirstParameterToFunctions`
|
||||
* - If a context is provided, you can use `AddFirstParameterToFunctions`
|
||||
* utility to signal this in the receiving side type
|
||||
*/
|
||||
export namespace ProxyChannel {
|
||||
|
||||
export interface IBaseChannelOptions {
|
||||
export interface IProxyOptions {
|
||||
|
||||
/**
|
||||
* Disables automatic marshalling of `URI`.
|
||||
* If marshalling is disabled, `UriComponents`
|
||||
* must be used instead.
|
||||
*/
|
||||
disableMarshalling?: boolean;
|
||||
}
|
||||
|
||||
export interface IChannelReceiverOptions extends IBaseChannelOptions { }
|
||||
|
||||
export function createChannelReceiver(service: unknown, options?: IChannelReceiverOptions): IServerChannel {
|
||||
const handler = service as { [key: string]: unknown };
|
||||
const disableMarshalling = options && options.disableMarshalling;
|
||||
|
||||
// Buffer any event that should be supported by
|
||||
// iterating over all property keys and finding them
|
||||
const mapEventNameToEvent = new Map<string, Event<unknown>>();
|
||||
for (const key in handler) {
|
||||
if (propertyIsEvent(key)) {
|
||||
mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event<unknown>, true));
|
||||
}
|
||||
/**
|
||||
* Disables automatic marshalling of `URI`.
|
||||
* If marshalling is disabled, `UriComponents`
|
||||
* must be used instead.
|
||||
*/
|
||||
disableMarshalling?: boolean;
|
||||
}
|
||||
|
||||
return new class implements IServerChannel {
|
||||
export interface ICreateServiceChannelOptions extends IProxyOptions { }
|
||||
|
||||
listen<T>(_: unknown, event: string): Event<T> {
|
||||
const eventImpl = mapEventNameToEvent.get(event);
|
||||
if (eventImpl) {
|
||||
return eventImpl as Event<T>;
|
||||
export function fromService(service: unknown, options?: ICreateServiceChannelOptions): IServerChannel {
|
||||
const handler = service as { [key: string]: unknown };
|
||||
const disableMarshalling = options && options.disableMarshalling;
|
||||
|
||||
// Buffer any event that should be supported by
|
||||
// iterating over all property keys and finding them
|
||||
const mapEventNameToEvent = new Map<string, Event<unknown>>();
|
||||
for (const key in handler) {
|
||||
if (propertyIsEvent(key)) {
|
||||
mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event<unknown>, true));
|
||||
}
|
||||
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(_: unknown, command: string, args?: any[]): Promise<any> {
|
||||
const target = handler[command];
|
||||
if (typeof target === 'function') {
|
||||
return new class implements IServerChannel {
|
||||
|
||||
// Revive unless marshalling disabled
|
||||
if (!disableMarshalling && Array.isArray(args)) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
args[i] = revive(args[i]);
|
||||
}
|
||||
listen<T>(_: unknown, event: string): Event<T> {
|
||||
const eventImpl = mapEventNameToEvent.get(event);
|
||||
if (eventImpl) {
|
||||
return eventImpl as Event<T>;
|
||||
}
|
||||
|
||||
return target.apply(handler, args);
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
throw new Error(`Method not found: ${command}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface IChannelSenderOptions extends IBaseChannelOptions {
|
||||
|
||||
/**
|
||||
* If provided, will add the value of `context`
|
||||
* to each method call to the target.
|
||||
*/
|
||||
context?: unknown;
|
||||
|
||||
/**
|
||||
* If provided, will not proxy any of the properties
|
||||
* that are part of the Map but rather return that value.
|
||||
*/
|
||||
properties?: Map<string, unknown>;
|
||||
}
|
||||
|
||||
export function createChannelSender<T>(channel: IChannel, options?: IChannelSenderOptions): T {
|
||||
const disableMarshalling = options && options.disableMarshalling;
|
||||
|
||||
return new Proxy({}, {
|
||||
get(_target: T, propKey: PropertyKey) {
|
||||
if (typeof propKey === 'string') {
|
||||
|
||||
// Check for predefined values
|
||||
if (options?.properties?.has(propKey)) {
|
||||
return options.properties.get(propKey);
|
||||
}
|
||||
|
||||
// Event
|
||||
if (propertyIsEvent(propKey)) {
|
||||
return channel.listen(propKey);
|
||||
}
|
||||
|
||||
// Function
|
||||
return async function (...args: any[]) {
|
||||
|
||||
// Add context if any
|
||||
let methodArgs: any[];
|
||||
if (options && !isUndefinedOrNull(options.context)) {
|
||||
methodArgs = [options.context, ...args];
|
||||
} else {
|
||||
methodArgs = args;
|
||||
}
|
||||
|
||||
const result = await channel.call(propKey, methodArgs);
|
||||
call(_: unknown, command: string, args?: any[]): Promise<any> {
|
||||
const target = handler[command];
|
||||
if (typeof target === 'function') {
|
||||
|
||||
// Revive unless marshalling disabled
|
||||
if (!disableMarshalling) {
|
||||
return revive(result);
|
||||
if (!disableMarshalling && Array.isArray(args)) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
args[i] = revive(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
return target.apply(handler, args);
|
||||
}
|
||||
|
||||
throw new Error(`Method not found: ${command}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Property not found: ${String(propKey)}`);
|
||||
}
|
||||
}) as T;
|
||||
export interface ICreateProxyServiceOptions extends IProxyOptions {
|
||||
|
||||
/**
|
||||
* If provided, will add the value of `context`
|
||||
* to each method call to the target.
|
||||
*/
|
||||
context?: unknown;
|
||||
|
||||
/**
|
||||
* If provided, will not proxy any of the properties
|
||||
* that are part of the Map but rather return that value.
|
||||
*/
|
||||
properties?: Map<string, unknown>;
|
||||
}
|
||||
|
||||
export function toService<T>(channel: IChannel, options?: ICreateProxyServiceOptions): T {
|
||||
const disableMarshalling = options && options.disableMarshalling;
|
||||
|
||||
return new Proxy({}, {
|
||||
get(_target: T, propKey: PropertyKey) {
|
||||
if (typeof propKey === 'string') {
|
||||
|
||||
// Check for predefined values
|
||||
if (options?.properties?.has(propKey)) {
|
||||
return options.properties.get(propKey);
|
||||
}
|
||||
|
||||
// Event
|
||||
if (propertyIsEvent(propKey)) {
|
||||
return channel.listen(propKey);
|
||||
}
|
||||
|
||||
// Function
|
||||
return async function (...args: any[]) {
|
||||
|
||||
// Add context if any
|
||||
let methodArgs: any[];
|
||||
if (options && !isUndefinedOrNull(options.context)) {
|
||||
methodArgs = [options.context, ...args];
|
||||
} else {
|
||||
methodArgs = args;
|
||||
}
|
||||
|
||||
const result = await channel.call(propKey, methodArgs);
|
||||
|
||||
// Revive unless marshalling disabled
|
||||
if (!disableMarshalling) {
|
||||
return revive(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Property not found: ${String(propKey)}`);
|
||||
}
|
||||
}) as T;
|
||||
}
|
||||
|
||||
function propertyIsEvent(name: string): boolean {
|
||||
// Assume a property is an event if it has a form of "onSomething"
|
||||
return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2));
|
||||
}
|
||||
}
|
||||
|
||||
function propertyIsEvent(name: string): boolean {
|
||||
// Assume a property is an event if it has a form of "onSomething"
|
||||
return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
const colorTables = [
|
||||
['#2977B1', '#FC802D', '#34A13A', '#D3282F', '#9366BA'],
|
||||
['#8B564C', '#E177C0', '#7F7F7F', '#BBBE3D', '#2EBECD']
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol as MessagePortProtocol } from 'vs/base/parts/ipc/common/ipc.mp';
|
||||
|
@ -39,6 +39,9 @@ export class Server extends IPCServer {
|
|||
};
|
||||
|
||||
// Send one port back to the requestor
|
||||
// Note: we intentionally use `electron` APIs here because
|
||||
// transferables like the `MessagePort` cannot be transfered
|
||||
// over preload scripts when `contextIsolation: true`
|
||||
ipcRenderer.postMessage('vscode:createMessageChannelResult', nonce, [outgoingPort]);
|
||||
|
||||
return result;
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient, ProxyChannel } 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';
|
||||
|
@ -332,10 +332,10 @@ suite('Base IPC', function () {
|
|||
const testServer = new TestIPCServer();
|
||||
server = testServer;
|
||||
|
||||
server.registerChannel(TestChannelId, createChannelReceiver(service));
|
||||
server.registerChannel(TestChannelId, ProxyChannel.fromService(service));
|
||||
|
||||
client = testServer.createConnection('client1');
|
||||
ipcService = createChannelSender(client.getChannel(TestChannelId));
|
||||
ipcService = ProxyChannel.toService(client.getChannel(TestChannelId));
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
|
@ -398,10 +398,10 @@ suite('Base IPC', function () {
|
|||
const testServer = new TestIPCServer();
|
||||
server = testServer;
|
||||
|
||||
server.registerChannel(TestChannelId, createChannelReceiver(service));
|
||||
server.registerChannel(TestChannelId, ProxyChannel.fromService(service));
|
||||
|
||||
client = testServer.createConnection('client1');
|
||||
ipcService = createChannelSender(client.getChannel(TestChannelId), { context: 'Super Context' });
|
||||
ipcService = ProxyChannel.toService(client.getChannel(TestChannelId), { context: 'Super Context' });
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
|
|
|
@ -35,17 +35,6 @@
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @param {any} message
|
||||
* @param {MessagePort[]} transfer
|
||||
*/
|
||||
postMessage(channel, message, transfer) {
|
||||
if (validateIPC(channel)) {
|
||||
ipcRenderer.postMessage(channel, message, transfer);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @param {any[]} args
|
||||
|
@ -88,6 +77,33 @@
|
|||
}
|
||||
},
|
||||
|
||||
ipcMessagePort: {
|
||||
|
||||
/**
|
||||
* @param {string} channelRequest
|
||||
* @param {string} channelResponse
|
||||
* @param {string} requestNonce
|
||||
*/
|
||||
connect(channelRequest, channelResponse, requestNonce) {
|
||||
if (validateIPC(channelRequest) && validateIPC(channelResponse)) {
|
||||
const responseListener = (/** @type {import('electron').IpcRendererEvent} */ e, /** @type {string} */ responseNonce) => {
|
||||
// validate that the nonce from the response is the same
|
||||
// as when requested. and if so, use `postMessage` to
|
||||
// send the `MessagePort` safely over, even when context
|
||||
// isolation is enabled
|
||||
if (requestNonce === responseNonce) {
|
||||
ipcRenderer.off(channelResponse, responseListener);
|
||||
window.postMessage(requestNonce, '*', e.ports);
|
||||
}
|
||||
};
|
||||
|
||||
// request message port from main and await result
|
||||
ipcRenderer.on(channelResponse, responseListener);
|
||||
ipcRenderer.send(channelRequest, requestNonce);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Support for subset of methods of Electron's `webFrame` type.
|
||||
*/
|
||||
|
|
|
@ -15,10 +15,12 @@ export interface IpcRendererEvent extends Event {
|
|||
|
||||
// Docs: https://electronjs.org/docs/api/structures/ipc-renderer-event
|
||||
|
||||
/**
|
||||
* A list of MessagePorts that were transferred with this message
|
||||
*/
|
||||
ports: MessagePort[];
|
||||
// Note: API with `Transferable` intentionally commented out because you
|
||||
// cannot transfer these when `contextIsolation: true`.
|
||||
// /**
|
||||
// * A list of MessagePorts that were transferred with this message
|
||||
// */
|
||||
// ports: MessagePort[];
|
||||
/**
|
||||
* The `IpcRenderer` instance that emitted the event originally
|
||||
*/
|
||||
|
@ -93,20 +95,23 @@ export interface IpcRenderer {
|
|||
* If you do not need a response to the message, consider using `ipcRenderer.send`.
|
||||
*/
|
||||
invoke(channel: string, ...args: any[]): Promise<any>;
|
||||
/**
|
||||
* Send a message to the main process, optionally transferring ownership of zero or
|
||||
* more `MessagePort` objects.
|
||||
*
|
||||
* The transferred `MessagePort` objects will be available in the main process as
|
||||
* `MessagePortMain` objects by accessing the `ports` property of the emitted
|
||||
* event.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* For more information on using `MessagePort` and `MessageChannel`, see the MDN
|
||||
* documentation.
|
||||
*/
|
||||
postMessage(channel: string, message: any, transfer?: MessagePort[]): void;
|
||||
|
||||
// Note: API with `Transferable` intentionally commented out because you
|
||||
// cannot transfer these when `contextIsolation: true`.
|
||||
// /**
|
||||
// * Send a message to the main process, optionally transferring ownership of zero or
|
||||
// * more `MessagePort` objects.
|
||||
// *
|
||||
// * The transferred `MessagePort` objects will be available in the main process as
|
||||
// * `MessagePortMain` objects by accessing the `ports` property of the emitted
|
||||
// * event.
|
||||
// *
|
||||
// * For example:
|
||||
// *
|
||||
// * For more information on using `MessagePort` and `MessageChannel`, see the MDN
|
||||
// * documentation.
|
||||
// */
|
||||
// postMessage(channel: string, message: any): void;
|
||||
}
|
||||
|
||||
export interface WebFrame {
|
||||
|
|
|
@ -94,7 +94,23 @@ export interface ISandboxContext {
|
|||
sandbox: boolean;
|
||||
}
|
||||
|
||||
export interface IpcMessagePort {
|
||||
|
||||
/**
|
||||
* Establish a connection via `MessagePort` to a target. The main process
|
||||
* will need to transfer the port over to the `channelResponse` after listening
|
||||
* to `channelRequest` with a payload of `requestNonce` so that the
|
||||
* source can correlate the response.
|
||||
*
|
||||
* The source should install a `window.on('message')` listener, ensuring `e.data`
|
||||
* matches `requestNonce`, `e.source` matches `window` and then receiving the
|
||||
* `MessagePort` via `e.ports[0]`.
|
||||
*/
|
||||
connect(channelRequest: string, channelResponse: string, requestNonce: string): void;
|
||||
}
|
||||
|
||||
export const ipcRenderer: IpcRenderer = globals.vscode.ipcRenderer;
|
||||
export const ipcMessagePort: IpcMessagePort = globals.vscode.ipcMessagePort;
|
||||
export const webFrame: WebFrame = globals.vscode.webFrame;
|
||||
export const crashReporter: CrashReporter = globals.vscode.crashReporter;
|
||||
export const process: ISandboxNodeProcess = globals.vscode.process;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream, observe } from 'vs/base/common/stream';
|
||||
import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream, listenStream } from 'vs/base/common/stream';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
suite('Stream', () => {
|
||||
|
@ -69,6 +69,7 @@ suite('Stream', () => {
|
|||
stream.end('Final Bit');
|
||||
assert.strictEqual(chunks.length, 4);
|
||||
assert.strictEqual(chunks[3], 'Final Bit');
|
||||
assert.strictEqual(end, true);
|
||||
|
||||
stream.destroy();
|
||||
|
||||
|
@ -76,6 +77,15 @@ suite('Stream', () => {
|
|||
assert.strictEqual(chunks.length, 4);
|
||||
});
|
||||
|
||||
test('WriteableStream - end with empty string works', async () => {
|
||||
const reducer = (strings: string[]) => strings.length > 0 ? strings.join() : 'error';
|
||||
const stream = newWriteableStream<string>(reducer);
|
||||
stream.end('');
|
||||
|
||||
const result = await consumeStream(stream, reducer);
|
||||
assert.strictEqual(result, '');
|
||||
});
|
||||
|
||||
test('WriteableStream - removeListener', () => {
|
||||
const stream = newWriteableStream<string>(strings => strings.join());
|
||||
|
||||
|
@ -270,6 +280,56 @@ suite('Stream', () => {
|
|||
assert.strictEqual(consumed, '1,2,3,4,5');
|
||||
});
|
||||
|
||||
test('consumeStream - without reducer', async () => {
|
||||
const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5']));
|
||||
const consumed = await consumeStream(stream);
|
||||
assert.strictEqual(consumed, undefined);
|
||||
});
|
||||
|
||||
test('consumeStream - without reducer and error', async () => {
|
||||
const stream = newWriteableStream<string>(strings => strings.join());
|
||||
stream.error(new Error());
|
||||
|
||||
const consumed = await consumeStream(stream);
|
||||
assert.strictEqual(consumed, undefined);
|
||||
});
|
||||
|
||||
test('listenStream', () => {
|
||||
const stream = newWriteableStream<string>(strings => strings.join());
|
||||
|
||||
let error = false;
|
||||
let end = false;
|
||||
let data = '';
|
||||
|
||||
listenStream(stream, {
|
||||
onData: d => {
|
||||
data = d;
|
||||
},
|
||||
onError: e => {
|
||||
error = true;
|
||||
},
|
||||
onEnd: () => {
|
||||
end = true;
|
||||
}
|
||||
});
|
||||
|
||||
stream.write('Hello');
|
||||
|
||||
assert.strictEqual(data, 'Hello');
|
||||
|
||||
stream.write('World');
|
||||
assert.strictEqual(data, 'World');
|
||||
|
||||
assert.strictEqual(error, false);
|
||||
assert.strictEqual(end, false);
|
||||
|
||||
stream.error(new Error());
|
||||
assert.strictEqual(error, true);
|
||||
|
||||
stream.end('Final Bit');
|
||||
assert.strictEqual(end, true);
|
||||
});
|
||||
|
||||
test('peekStream', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5']));
|
||||
|
@ -335,30 +395,6 @@ suite('Stream', () => {
|
|||
assert.strictEqual(consumed, '11,22,33,44,55');
|
||||
});
|
||||
|
||||
test('observer', async () => {
|
||||
const source1 = newWriteableStream<string>(strings => strings.join());
|
||||
setTimeout(() => source1.error(new Error()));
|
||||
await observe(source1).errorOrEnd();
|
||||
|
||||
const source2 = newWriteableStream<string>(strings => strings.join());
|
||||
setTimeout(() => source2.end('Hello Test'));
|
||||
await observe(source2).errorOrEnd();
|
||||
|
||||
const source3 = newWriteableStream<string>(strings => strings.join());
|
||||
setTimeout(() => {
|
||||
source3.write('Hello Test');
|
||||
source3.error(new Error());
|
||||
});
|
||||
await observe(source3).errorOrEnd();
|
||||
|
||||
const source4 = newWriteableStream<string>(strings => strings.join());
|
||||
setTimeout(() => {
|
||||
source4.write('Hello Test');
|
||||
source4.end();
|
||||
});
|
||||
await observe(source4).errorOrEnd();
|
||||
});
|
||||
|
||||
test('events are delivered even if a listener is removed during delivery', () => {
|
||||
const stream = newWriteableStream<string>(strings => strings.join());
|
||||
|
||||
|
|
|
@ -7,8 +7,9 @@ import product from 'vs/platform/product/common/product';
|
|||
import * as fs from 'fs';
|
||||
import { release } from 'os';
|
||||
import { gracefulify } from 'graceful-fs';
|
||||
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp';
|
||||
import { StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp';
|
||||
import { StaticRouter, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
|
@ -28,7 +29,6 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProp
|
|||
import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc';
|
||||
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { ILogService, ILoggerService, MultiplexLogService, ConsoleLogger } from 'vs/platform/log/common/log';
|
||||
import { LogLevelChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc';
|
||||
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
|
||||
|
@ -40,7 +40,8 @@ import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/co
|
|||
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, MessagePortMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { MessagePortMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
|
||||
import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog';
|
||||
import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
|
||||
import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics';
|
||||
|
@ -184,7 +185,7 @@ class SharedProcessMain extends Disposable {
|
|||
services.set(IRequestService, new SyncDescriptor(RequestService));
|
||||
|
||||
// Native Host
|
||||
const nativeHostService = createChannelSender<INativeHostService>(mainProcessService.getChannel('nativeHost'), { context: this.configuration.windowId });
|
||||
const nativeHostService = ProxyChannel.toService<INativeHostService>(mainProcessService.getChannel('nativeHost'), { context: this.configuration.windowId });
|
||||
services.set(INativeHostService, nativeHostService);
|
||||
|
||||
// Download
|
||||
|
@ -270,12 +271,12 @@ class SharedProcessMain extends Disposable {
|
|||
|
||||
// Localizations
|
||||
const localizationsService = accessor.get(ILocalizationsService);
|
||||
const localizationsChannel = createChannelReceiver(localizationsService);
|
||||
const localizationsChannel = ProxyChannel.fromService(localizationsService);
|
||||
this.server.registerChannel('localizations', localizationsChannel);
|
||||
|
||||
// Diagnostics
|
||||
const diagnosticsService = accessor.get(IDiagnosticsService);
|
||||
const diagnosticsChannel = createChannelReceiver(diagnosticsService);
|
||||
const diagnosticsChannel = ProxyChannel.fromService(diagnosticsService);
|
||||
this.server.registerChannel('diagnostics', diagnosticsChannel);
|
||||
|
||||
// Extension Tips
|
||||
|
|
|
@ -10,14 +10,14 @@ import { IProcessEnvironment, isWindows, isMacintosh, isLinux, isLinuxSnap } fro
|
|||
import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService';
|
||||
import { IWindowOpenable } from 'vs/platform/windows/common/windows';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { resolveShellEnv } from 'vs/code/node/shellEnv';
|
||||
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
|
||||
import { IUpdateService } from 'vs/platform/update/common/update';
|
||||
import { UpdateChannel } from 'vs/platform/update/electron-main/updateIpc';
|
||||
import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { getDelayedChannel, StaticRouter, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron';
|
||||
import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp';
|
||||
import { SharedProcess } from 'vs/code/electron-main/sharedProcess';
|
||||
import { SharedProcess } from 'vs/platform/sharedProcess/electron-main/sharedProcess';
|
||||
import { LaunchMainService, ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
|
@ -525,7 +525,7 @@ export class CodeApplication extends Disposable {
|
|||
services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, this.userEnv]));
|
||||
services.set(IDialogMainService, new SyncDescriptor(DialogMainService));
|
||||
services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService));
|
||||
services.set(IDiagnosticsService, createChannelSender(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics')))));
|
||||
services.set(IDiagnosticsService, ProxyChannel.toService(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics')))));
|
||||
|
||||
services.set(IIssueMainService, new SyncDescriptor(IssueMainService, [machineId, this.userEnv]));
|
||||
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId]));
|
||||
|
@ -606,7 +606,7 @@ export class CodeApplication extends Disposable {
|
|||
|
||||
// Register more Main IPC services
|
||||
const launchMainService = accessor.get(ILaunchMainService);
|
||||
const launchChannel = createChannelReceiver(launchMainService, { disableMarshalling: true });
|
||||
const launchChannel = ProxyChannel.fromService(launchMainService, { disableMarshalling: true });
|
||||
this.mainIpcServer.registerChannel('launch', launchChannel);
|
||||
|
||||
// Register more Electron IPC services
|
||||
|
@ -615,44 +615,44 @@ export class CodeApplication extends Disposable {
|
|||
electronIpcServer.registerChannel('update', updateChannel);
|
||||
|
||||
const issueMainService = accessor.get(IIssueMainService);
|
||||
const issueChannel = createChannelReceiver(issueMainService);
|
||||
const issueChannel = ProxyChannel.fromService(issueMainService);
|
||||
electronIpcServer.registerChannel('issue', issueChannel);
|
||||
|
||||
const encryptionMainService = accessor.get(IEncryptionMainService);
|
||||
const encryptionChannel = createChannelReceiver(encryptionMainService);
|
||||
const encryptionChannel = ProxyChannel.fromService(encryptionMainService);
|
||||
electronIpcServer.registerChannel('encryption', encryptionChannel);
|
||||
|
||||
const keyboardLayoutMainService = accessor.get(IKeyboardLayoutMainService);
|
||||
const keyboardLayoutChannel = createChannelReceiver(keyboardLayoutMainService);
|
||||
const keyboardLayoutChannel = ProxyChannel.fromService(keyboardLayoutMainService);
|
||||
electronIpcServer.registerChannel('keyboardLayout', keyboardLayoutChannel);
|
||||
|
||||
const displayMainService = accessor.get(IDisplayMainService);
|
||||
const displayChannel = createChannelReceiver(displayMainService);
|
||||
const displayChannel = ProxyChannel.fromService(displayMainService);
|
||||
electronIpcServer.registerChannel('display', displayChannel);
|
||||
|
||||
const nativeHostMainService = this.nativeHostMainService = accessor.get(INativeHostMainService);
|
||||
const nativeHostChannel = createChannelReceiver(this.nativeHostMainService);
|
||||
const nativeHostChannel = ProxyChannel.fromService(this.nativeHostMainService);
|
||||
electronIpcServer.registerChannel('nativeHost', nativeHostChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel));
|
||||
|
||||
const workspacesService = accessor.get(IWorkspacesService);
|
||||
const workspacesChannel = createChannelReceiver(workspacesService);
|
||||
const workspacesChannel = ProxyChannel.fromService(workspacesService);
|
||||
electronIpcServer.registerChannel('workspaces', workspacesChannel);
|
||||
|
||||
const menubarMainService = accessor.get(IMenubarMainService);
|
||||
const menubarChannel = createChannelReceiver(menubarMainService);
|
||||
const menubarChannel = ProxyChannel.fromService(menubarMainService);
|
||||
electronIpcServer.registerChannel('menubar', menubarChannel);
|
||||
|
||||
const urlService = accessor.get(IURLService);
|
||||
const urlChannel = createChannelReceiver(urlService);
|
||||
const urlChannel = ProxyChannel.fromService(urlService);
|
||||
electronIpcServer.registerChannel('url', urlChannel);
|
||||
|
||||
const extensionUrlTrustService = accessor.get(IExtensionUrlTrustService);
|
||||
const extensionUrlTrustChannel = createChannelReceiver(extensionUrlTrustService);
|
||||
const extensionUrlTrustChannel = ProxyChannel.fromService(extensionUrlTrustService);
|
||||
electronIpcServer.registerChannel('extensionUrlTrust', extensionUrlTrustChannel);
|
||||
|
||||
const webviewManagerService = accessor.get(IWebviewManagerService);
|
||||
const webviewChannel = createChannelReceiver(webviewManagerService);
|
||||
const webviewChannel = ProxyChannel.fromService(webviewManagerService);
|
||||
electronIpcServer.registerChannel('webview', webviewChannel);
|
||||
|
||||
const storageMainService = accessor.get(IStorageMainService);
|
||||
|
|
|
@ -12,7 +12,7 @@ import product from 'vs/platform/product/common/product';
|
|||
import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper';
|
||||
import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile';
|
||||
import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Server as NodeIPCServer, serve as nodeIPCServe, connect as nodeIPCConnect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Client as NodeIPCClient } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
|
||||
|
@ -292,7 +292,7 @@ class CodeMain {
|
|||
}, 10000);
|
||||
}
|
||||
|
||||
const launchService = createChannelSender<ILaunchMainService>(client.getChannel('launch'), { disableMarshalling: true });
|
||||
const launchService = ProxyChannel.toService<ILaunchMainService>(client.getChannel('launch'), { disableMarshalling: true });
|
||||
|
||||
// Process Info
|
||||
if (args.status) {
|
||||
|
|
|
@ -38,6 +38,7 @@ import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRed
|
|||
import { TextChange } from 'vs/editor/common/model/textChange';
|
||||
import { Constants } from 'vs/base/common/uint';
|
||||
import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer';
|
||||
import { listenStream } from 'vs/base/common/stream';
|
||||
|
||||
function createTextBufferBuilder() {
|
||||
return new PieceTreeTextBufferBuilder();
|
||||
|
@ -64,33 +65,33 @@ export function createTextBufferFactoryFromStream(stream: ITextStream | VSBuffer
|
|||
|
||||
let done = false;
|
||||
|
||||
stream.on('data', (chunk: string | VSBuffer) => {
|
||||
if (validator) {
|
||||
const error = validator(chunk);
|
||||
if (error) {
|
||||
listenStream<string | VSBuffer>(stream, {
|
||||
onData: chunk => {
|
||||
if (validator) {
|
||||
const error = validator(chunk);
|
||||
if (error) {
|
||||
done = true;
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
chunk = filter(chunk);
|
||||
}
|
||||
|
||||
builder.acceptChunk((typeof chunk === 'string') ? chunk : chunk.toString());
|
||||
},
|
||||
onError: error => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
chunk = filter(chunk);
|
||||
}
|
||||
|
||||
builder.acceptChunk((typeof chunk === 'string') ? chunk : chunk.toString());
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
resolve(builder.finish());
|
||||
},
|
||||
onEnd: () => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
resolve(builder.finish());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
.monaco-action-bar .action-item.menu-entry .action-label {
|
||||
background-image: var(--menu-entry-icon-light);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-action-bar .action-item.menu-entry .action-label,
|
||||
|
|
|
@ -95,7 +95,7 @@ function fillInActions(
|
|||
const target = isPrimaryGroup(group) ? primaryBucket : secondaryBucket;
|
||||
|
||||
const submenuActions = action.actions;
|
||||
if (target.length + submenuActions.length - 1 <= primaryMaxCount && shouldInlineSubmenu(action, group, target.length)) {
|
||||
if (target.length + submenuActions.length - 2 <= primaryMaxCount && shouldInlineSubmenu(action, group, target.length)) {
|
||||
target.splice(index, 1, ...submenuActions);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { TernarySearchTree } from 'vs/base/common/map';
|
|||
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
|
||||
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream';
|
||||
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, listenStream, consumeStream } from 'vs/base/common/stream';
|
||||
import { Promises, Queue } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
@ -454,8 +454,7 @@ export class FileService extends Disposable implements IFileService {
|
|||
throw error;
|
||||
});
|
||||
|
||||
let fileStreamObserver: IReadableStreamObservable | undefined = undefined;
|
||||
|
||||
let fileStream: VSBufferReadableStream | undefined = undefined;
|
||||
try {
|
||||
|
||||
// if the etag is provided, we await the result of the validation
|
||||
|
@ -466,8 +465,6 @@ export class FileService extends Disposable implements IFileService {
|
|||
await statPromise;
|
||||
}
|
||||
|
||||
let fileStream: VSBufferReadableStream | undefined = undefined;
|
||||
|
||||
// read unbuffered (only if either preferred, or the provider has no buffered read capability)
|
||||
if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) {
|
||||
fileStream = this.readFileUnbuffered(provider, resource, options);
|
||||
|
@ -483,9 +480,6 @@ export class FileService extends Disposable implements IFileService {
|
|||
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options);
|
||||
}
|
||||
|
||||
// observe the stream for the error case below
|
||||
fileStreamObserver = observe(fileStream);
|
||||
|
||||
const fileStat = await statPromise;
|
||||
|
||||
return {
|
||||
|
@ -497,8 +491,8 @@ export class FileService extends Disposable implements IFileService {
|
|||
// Await the stream to finish so that we exit this method
|
||||
// in a consistent state with file handles closed
|
||||
// (https://github.com/microsoft/vscode/issues/114024)
|
||||
if (fileStreamObserver) {
|
||||
await fileStreamObserver.errorOrEnd();
|
||||
if (fileStream) {
|
||||
await consumeStream(fileStream);
|
||||
}
|
||||
|
||||
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
|
||||
|
@ -1065,28 +1059,29 @@ export class FileService extends Disposable implements IFileService {
|
|||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
stream.on('data', async chunk => {
|
||||
listenStream(stream, {
|
||||
onData: async chunk => {
|
||||
|
||||
// pause stream to perform async write operation
|
||||
stream.pause();
|
||||
// pause stream to perform async write operation
|
||||
stream.pause();
|
||||
|
||||
try {
|
||||
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
try {
|
||||
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
posInFile += chunk.byteLength;
|
||||
posInFile += chunk.byteLength;
|
||||
|
||||
// resume stream now that we have successfully written
|
||||
// run this on the next tick to prevent increasing the
|
||||
// execution stack because resume() may call the event
|
||||
// handler again before finishing.
|
||||
setTimeout(() => stream.resume());
|
||||
// resume stream now that we have successfully written
|
||||
// run this on the next tick to prevent increasing the
|
||||
// execution stack because resume() may call the event
|
||||
// handler again before finishing.
|
||||
setTimeout(() => stream.resume());
|
||||
},
|
||||
onError: error => reject(error),
|
||||
onEnd: () => resolve()
|
||||
});
|
||||
|
||||
stream.on('error', error => reject(error));
|
||||
stream.on('end', () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
|||
|
||||
export interface IWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
usePolling: boolean;
|
||||
usePolling: boolean | string[];
|
||||
}
|
||||
|
||||
export interface IDiskFileSystemProviderOptions {
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
|
||||
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new NsfwWatcherService();
|
||||
server.registerChannel('watcher', createChannelReceiver(service));
|
||||
server.registerChannel('watcher', ProxyChannel.fromService(service));
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
@ -61,7 +61,7 @@ export class FileWatcher extends Disposable {
|
|||
}));
|
||||
|
||||
// Initialize watcher
|
||||
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
|
||||
this.service.setVerboseLogging(this.verboseLogging);
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
|
|||
get wacherCount() { return this._watcherCount; }
|
||||
|
||||
private pollingInterval?: number;
|
||||
private usePolling?: boolean;
|
||||
private usePolling?: boolean | string[];
|
||||
private verboseLogging: boolean | undefined;
|
||||
|
||||
private spamCheckStartTime: number | undefined;
|
||||
|
@ -101,7 +101,11 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
|
|||
|
||||
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
|
||||
const pollingInterval = this.pollingInterval || 5000;
|
||||
const usePolling = this.usePolling;
|
||||
let usePolling = this.usePolling; // boolean or a list of path patterns
|
||||
if (Array.isArray(usePolling)) {
|
||||
// switch to polling if one of the paths matches with a watched path
|
||||
usePolling = usePolling.some(pattern => requests.some(r => glob.match(pattern, r.path)));
|
||||
}
|
||||
|
||||
const watcherOpts: chokidar.WatchOptions = {
|
||||
ignoreInitial: true,
|
||||
|
|
|
@ -13,7 +13,7 @@ export interface IWatcherRequest {
|
|||
|
||||
export interface IWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
usePolling?: boolean;
|
||||
usePolling?: boolean | string[]; // boolean or a set of glob patterns matching folders that need polling
|
||||
verboseLogging?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
|
||||
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new ChokidarWatcherService();
|
||||
server.registerChannel('watcher', createChannelReceiver(service));
|
||||
server.registerChannel('watcher', ProxyChannel.fromService(service));
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
@ -62,7 +62,7 @@ export class FileWatcher extends Disposable {
|
|||
}));
|
||||
|
||||
// Initialize watcher
|
||||
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging });
|
||||
|
||||
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
|
||||
|
|
|
@ -1584,6 +1584,20 @@ flakySuite('Disk File Service', function () {
|
|||
assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE);
|
||||
}
|
||||
|
||||
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => {
|
||||
const link = URI.file(join(testDir, 'small.js-link'));
|
||||
await promises.symlink(join(testDir, 'small.js'), link.fsPath);
|
||||
|
||||
let error: FileOperationError | undefined = undefined;
|
||||
try {
|
||||
await service.readFile(link);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
});
|
||||
|
||||
test('createFile', async () => {
|
||||
return assertCreateFile(contents => VSBuffer.fromString(contents));
|
||||
});
|
||||
|
|
29
src/vs/platform/ipc/electron-browser/mainProcessService.ts
Normal file
29
src/vs/platform/ipc/electron-browser/mainProcessService.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
|
||||
/**
|
||||
* An implementation of `IMainProcessService` that leverages MessagePorts.
|
||||
*/
|
||||
export class MessagePortMainProcessService implements IMainProcessService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
private server: MessagePortServer,
|
||||
private router: StaticRouter
|
||||
) { }
|
||||
|
||||
getChannel(channelName: string): IChannel {
|
||||
return this.server.getChannel(channelName, this.router);
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<string>): void {
|
||||
this.server.registerChannel(channelName, channel);
|
||||
}
|
||||
}
|
|
@ -3,11 +3,10 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client as IPCElectronClient } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp';
|
||||
|
||||
export const IMainProcessService = createDecorator<IMainProcessService>('mainProcessService');
|
||||
|
||||
|
@ -45,24 +44,3 @@ export class ElectronIPCMainProcessService extends Disposable implements IMainPr
|
|||
this.mainProcessConnection.registerChannel(channelName, channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of `IMainProcessService` that leverages MessagePorts.
|
||||
*/
|
||||
export class MessagePortMainProcessService implements IMainProcessService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
private server: MessagePortServer,
|
||||
private router: StaticRouter
|
||||
) { }
|
||||
|
||||
getChannel(channelName: string): IChannel {
|
||||
return this.server.getChannel(channelName, this.router);
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<string>): void {
|
||||
this.server.registerChannel(channelName, channel);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { ipcMessagePort } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
|
||||
import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
|
@ -45,13 +44,13 @@ export class SharedProcessService extends Disposable implements ISharedProcessSe
|
|||
// Ask to create message channel inside the window
|
||||
// and send over a UUID to correlate the response
|
||||
const nonce = generateUuid();
|
||||
ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce);
|
||||
ipcMessagePort.connect('vscode:createSharedProcessMessageChannel', 'vscode:createSharedProcessMessageChannelResult', nonce);
|
||||
|
||||
// Wait until the main side has returned the `MessagePort`
|
||||
// We need to filter by the `nonce` to ensure we listen
|
||||
// to the right response.
|
||||
const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] }));
|
||||
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce)));
|
||||
const onMessageChannelResult = Event.fromDOMEventEmitter<{ nonce: string, port: MessagePort, source: unknown }>(window, 'message', (e: MessageEvent) => ({ nonce: e.data, port: e.ports[0], source: e.source }));
|
||||
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce && e.source === window)));
|
||||
|
||||
this.logService.trace('Renderer->SharedProcess#connect: connection established');
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
// @ts-ignore: interface is implemented via proxy
|
||||
export class NativeHostService implements INativeHostService {
|
||||
|
@ -16,7 +16,7 @@ export class NativeHostService implements INativeHostService {
|
|||
readonly windowId: number,
|
||||
@IMainProcessService mainProcessService: IMainProcessService
|
||||
) {
|
||||
return createChannelSender<INativeHostService>(mainProcessService.getChannel('nativeHost'), {
|
||||
return ProxyChannel.toService<INativeHostService>(mainProcessService.getChannel('nativeHost'), {
|
||||
context: windowId,
|
||||
properties: (() => {
|
||||
const properties = new Map<string, unknown>();
|
||||
|
|
|
@ -17,7 +17,7 @@ export class UserDataSyncLogService extends AbstractLogger implements IUserDataS
|
|||
@IEnvironmentService environmentService: IEnvironmentService
|
||||
) {
|
||||
super();
|
||||
this.logger = this._register(loggerService.createLogger(environmentService.userDataSyncLogResource));
|
||||
this.logger = this._register(loggerService.createLogger(environmentService.userDataSyncLogResource, { name: 'settingssync' }));
|
||||
}
|
||||
|
||||
trace(message: string, ...args: any[]): void {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { bufferToStream, VSBufferReadableStream } from 'vs/base/common/buffer';
|
|||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { FileAccess, Schemas } from 'vs/base/common/network';
|
||||
import { listenStream } from 'vs/base/common/stream';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
@ -75,28 +76,27 @@ export class WebviewProtocolProvider extends Disposable {
|
|||
if (!this.listening) {
|
||||
this.listening = true;
|
||||
|
||||
// Data
|
||||
stream.on('data', data => {
|
||||
try {
|
||||
if (!this.push(data.buffer)) {
|
||||
stream.pause(); // pause the stream if we should not push anymore
|
||||
listenStream(stream, {
|
||||
onData: data => {
|
||||
try {
|
||||
if (!this.push(data.buffer)) {
|
||||
stream.pause(); // pause the stream if we should not push anymore
|
||||
}
|
||||
} catch (error) {
|
||||
this.emit(error);
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
this.emit('error', error);
|
||||
},
|
||||
onEnd: () => {
|
||||
try {
|
||||
this.push(null); // signal EOS
|
||||
} catch (error) {
|
||||
this.emit(error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.emit(error);
|
||||
}
|
||||
});
|
||||
|
||||
// End
|
||||
stream.on('end', () => {
|
||||
try {
|
||||
this.push(null); // signal EOS
|
||||
} catch (error) {
|
||||
this.emit(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Error
|
||||
stream.on('error', error => this.emit('error', error));
|
||||
}
|
||||
|
||||
// ensure the stream is flowing
|
||||
|
@ -168,7 +168,11 @@ export class WebviewProtocolProvider extends Disposable {
|
|||
rewriteUri = (uri) => {
|
||||
if (metadata.remoteConnectionData) {
|
||||
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === Schemas.vscodeRemote)) {
|
||||
return URI.parse(`http://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
|
||||
let host = metadata.remoteConnectionData.host;
|
||||
if (host && host.indexOf(':') !== -1) { // IPv6 address
|
||||
host = `[${host}]`;
|
||||
}
|
||||
return URI.parse(`http://${host}:${metadata.remoteConnectionData.port}`).with({
|
||||
path: '/vscode-remote-resource',
|
||||
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ import product from 'vs/platform/product/common/product';
|
|||
import { WindowMinimumSize, IWindowSettings, MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, zoomLevelToZoomFactor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
|
||||
import { defaultWindowState, ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
|
||||
import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
|
@ -44,14 +44,6 @@ export interface IWindowCreationOptions {
|
|||
isExtensionTestHost?: boolean;
|
||||
}
|
||||
|
||||
export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState {
|
||||
return {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
mode
|
||||
};
|
||||
};
|
||||
|
||||
interface ITouchBarSegment extends SegmentedControlSegment {
|
||||
id: string;
|
||||
}
|
||||
|
@ -702,6 +694,15 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
|||
config.userEnv = { ...currentUserEnv, ...config.userEnv }; // still allow to override certain environment as passed in
|
||||
}
|
||||
|
||||
// If named pipe was instantiated for the crashpad_handler process, reuse the same
|
||||
// pipe for new app instances connecting to the original app instance.
|
||||
// Ref: https://github.com/microsoft/vscode/issues/115874
|
||||
if (process.env['CHROME_CRASHPAD_PIPE_NAME']) {
|
||||
Object.assign(config.userEnv, {
|
||||
CHROME_CRASHPAD_PIPE_NAME: process.env['CHROME_CRASHPAD_PIPE_NAME']
|
||||
});
|
||||
}
|
||||
|
||||
// If this is the first time the window is loaded, we associate the paths
|
||||
// directly with the window because we assume the loading will just work
|
||||
if (this._readyState === ReadyState.NONE) {
|
|
@ -45,6 +45,14 @@ export interface IWindowState {
|
|||
display?: number;
|
||||
}
|
||||
|
||||
export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState {
|
||||
return {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
mode
|
||||
};
|
||||
};
|
||||
|
||||
export const enum WindowMode {
|
||||
Maximized,
|
||||
Normal,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
|||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { CodeWindow } from 'vs/code/electron-main/window';
|
||||
import { CodeWindow } from 'vs/platform/windows/electron-main/window';
|
||||
import { BrowserWindow, MessageBoxOptions, WebContents } from 'electron';
|
||||
import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
|
|
@ -8,13 +8,12 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
|||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { defaultWindowState } from 'vs/code/electron-main/window';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { INativeWindowConfiguration, IWindowSettings } from 'vs/platform/windows/common/windows';
|
||||
import { ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows';
|
||||
import { defaultWindowState, ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows';
|
||||
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
|
||||
export interface IWindowState {
|
||||
|
|
50
src/vs/vscode.proposed.d.ts
vendored
50
src/vs/vscode.proposed.d.ts
vendored
|
@ -2077,10 +2077,12 @@ declare module 'vscode' {
|
|||
export function registerTestProvider<T extends TestItem>(testProvider: TestProvider<T>): Disposable;
|
||||
|
||||
/**
|
||||
* Runs tests with the given options. If no options are given, then
|
||||
* all tests are run. Returns the resulting test run.
|
||||
* Runs tests. The "run" contains the list of tests to run as well as a
|
||||
* method that can be used to update their state. At the point in time
|
||||
* that "run" is called, all tests given in the run have their state
|
||||
* automatically set to {@link TestRunState.Queued}.
|
||||
*/
|
||||
export function runTests<T extends TestItem>(options: TestRunOptions<T>, cancellationToken?: CancellationToken): Thenable<void>;
|
||||
export function runTests<T extends TestItem>(run: TestRunOptions<T>, cancellationToken?: CancellationToken): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Returns an observer that retrieves tests in the given workspace folder.
|
||||
|
@ -2194,6 +2196,14 @@ declare module 'vscode' {
|
|||
*/
|
||||
readonly discoveredInitialTests?: Thenable<unknown>;
|
||||
|
||||
/**
|
||||
* An event that fires when a test becomes outdated, as a result of
|
||||
* file changes, for example. In "watch" mode, tests that are outdated
|
||||
* will be automatically re-run after a short delay. Firing a test
|
||||
* with children will mark the entire subtree as outdated.
|
||||
*/
|
||||
readonly onDidInvalidateTest?: Event<T>;
|
||||
|
||||
/**
|
||||
* Dispose will be called when there are no longer observers interested
|
||||
* in the hierarchy.
|
||||
|
@ -2238,11 +2248,11 @@ declare module 'vscode' {
|
|||
* @todo this will eventually need to be able to return a summary report, coverage for example.
|
||||
*/
|
||||
// eslint-disable-next-line vscode-dts-provider-naming
|
||||
runTests?(options: TestRunOptions<T>, cancellationToken: CancellationToken): ProviderResult<void>;
|
||||
runTests?(options: TestRun<T>, cancellationToken: CancellationToken): ProviderResult<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options given to `TestProvider.runTests`
|
||||
* Options given to {@link test.runTests}
|
||||
*/
|
||||
export interface TestRunOptions<T extends TestItem = TestItem> {
|
||||
/**
|
||||
|
@ -2257,6 +2267,17 @@ declare module 'vscode' {
|
|||
debug: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options given to `TestProvider.runTests`
|
||||
*/
|
||||
export interface TestRun<T extends TestItem = TestItem> extends TestRunOptions<T> {
|
||||
/**
|
||||
* Updates the state of the test in the run. By default, all tests involved
|
||||
* in the run will have a "queued" state until they are updated by this method.
|
||||
*/
|
||||
setState(test: T, state: TestState): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test item is an item shown in the "test explorer" view. It encompasses
|
||||
* both a suite and a test, since they have almost or identical capabilities.
|
||||
|
@ -2305,12 +2326,6 @@ declare module 'vscode' {
|
|||
* Optional list of nested tests for this item.
|
||||
*/
|
||||
children?: TestItem[];
|
||||
|
||||
/**
|
||||
* Test run state. Will generally be {@link TestRunState.Unset} by
|
||||
* default.
|
||||
*/
|
||||
state: TestState;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2345,11 +2360,11 @@ declare module 'vscode' {
|
|||
* in order to update it. This allows consumers to quickly and easily check
|
||||
* for changes via object identity.
|
||||
*/
|
||||
export class TestState {
|
||||
export interface TestState {
|
||||
/**
|
||||
* Current state of the test.
|
||||
*/
|
||||
readonly runState: TestRunState;
|
||||
readonly state: TestRunState;
|
||||
|
||||
/**
|
||||
* Optional duration of the test run, in milliseconds.
|
||||
|
@ -2360,14 +2375,7 @@ declare module 'vscode' {
|
|||
* Associated test run message. Can, for example, contain assertion
|
||||
* failure information if the test fails.
|
||||
*/
|
||||
readonly messages: ReadonlyArray<Readonly<TestMessage>>;
|
||||
|
||||
/**
|
||||
* @param state Run state to hold in the test state
|
||||
* @param messages List of associated messages for the test
|
||||
* @param duration Length of time the test run took, if appropriate.
|
||||
*/
|
||||
constructor(runState: TestRunState, messages?: TestMessage[], duration?: number);
|
||||
readonly messages?: ReadonlyArray<Readonly<TestMessage>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -214,13 +214,6 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
$logout(providerId: string, sessionId: string): Promise<void> {
|
||||
return this.authenticationService.logout(providerId, sessionId);
|
||||
}
|
||||
|
||||
private isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean {
|
||||
const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
const extensionData = allowList.find(extension => extension.id === extensionId);
|
||||
return !!extensionData;
|
||||
}
|
||||
|
||||
private async loginPrompt(providerName: string, extensionName: string): Promise<boolean> {
|
||||
const { choice } = await this.dialogService.show(
|
||||
Severity.Info,
|
||||
|
@ -257,10 +250,15 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
if (existingSessionPreference) {
|
||||
const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference);
|
||||
if (matchingSession) {
|
||||
const allowed = await this.authenticationService.showGetSessionPrompt(providerId, matchingSession.account.label, extensionId, extensionName);
|
||||
if (allowed) {
|
||||
return matchingSession;
|
||||
const allowed = this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId);
|
||||
if (!allowed) {
|
||||
const didAcceptPrompt = await this.authenticationService.showGetSessionPrompt(providerId, matchingSession.account.label, extensionId, extensionName);
|
||||
if (!didAcceptPrompt) {
|
||||
throw new Error('User did not consent to login.');
|
||||
}
|
||||
}
|
||||
|
||||
return matchingSession;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -270,14 +268,14 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
|
||||
async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise<modes.AuthenticationSession | undefined> {
|
||||
const orderedScopes = scopes.sort().join(' ');
|
||||
const sessions = (await this.authenticationService.getSessions(providerId)).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes);
|
||||
const sessions = (await this.authenticationService.getSessions(providerId, true)).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes);
|
||||
|
||||
const silent = !options.createIfNone;
|
||||
let session: modes.AuthenticationSession | undefined;
|
||||
if (sessions.length) {
|
||||
if (!this.authenticationService.supportsMultipleAccounts(providerId)) {
|
||||
session = sessions[0];
|
||||
const allowed = this.isAccessAllowed(providerId, session.account.label, extensionId);
|
||||
const allowed = this.authenticationService.isAccessAllowed(providerId, session.account.label, extensionId);
|
||||
if (!allowed) {
|
||||
if (!silent) {
|
||||
const didAcceptPrompt = await this.authenticationService.showGetSessionPrompt(providerId, session.account.label, extensionId, extensionName);
|
||||
|
@ -302,7 +300,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
throw new Error('User did not consent to login.');
|
||||
}
|
||||
|
||||
session = await this.authenticationService.login(providerId, scopes);
|
||||
session = await this.authenticationService.login(providerId, scopes, true);
|
||||
await this.setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
|
||||
} else {
|
||||
await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName);
|
||||
|
|
|
@ -129,13 +129,13 @@ export class MainThreadFileSystemEventService {
|
|||
}
|
||||
} else {
|
||||
if (operation === FileOperation.CREATE) {
|
||||
message = localize('ask.N.create', "{0} extensions want to make refactoring changes with this file creation", data.extensionNames.length);
|
||||
message = localize({ key: 'ask.N.create', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file creation", data.extensionNames.length);
|
||||
} else if (operation === FileOperation.COPY) {
|
||||
message = localize('ask.N.copy', "{0} extensions want to make refactoring changes with this file copy", data.extensionNames.length);
|
||||
message = localize({ key: 'ask.N.copy', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file copy", data.extensionNames.length);
|
||||
} else if (operation === FileOperation.MOVE) {
|
||||
message = localize('ask.N.move', "{0} extensions want to make refactoring changes with this file move", data.extensionNames.length);
|
||||
message = localize({ key: 'ask.N.move', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file move", data.extensionNames.length);
|
||||
} else /* if (operation === FileOperation.DELETE) */ {
|
||||
message = localize('ask.N.delete', "{0} extensions want to make refactoring changes with this file deletion", data.extensionNames.length);
|
||||
message = localize({ key: 'ask.N.delete', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file deletion", data.extensionNames.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { getTestSubscriptionKey, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
|
||||
|
||||
|
@ -19,12 +19,6 @@ const reviveDiff = (diff: TestsDiff) => {
|
|||
if (item.item.location) {
|
||||
item.item.location.uri = URI.revive(item.item.location.uri);
|
||||
}
|
||||
|
||||
for (const message of item.item.state.messages) {
|
||||
if (message.location) {
|
||||
message.location.uri = URI.revive(message.location.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -37,30 +31,42 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
|
|||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@ITestResultService resultService: ITestResultService,
|
||||
@ITestResultService private readonly resultService: ITestResultService,
|
||||
) {
|
||||
super();
|
||||
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting);
|
||||
this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri)));
|
||||
this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri)));
|
||||
|
||||
const testCompleteListener = this._register(new MutableDisposable());
|
||||
this._register(resultService.onNewTestResult(results => {
|
||||
testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests }));
|
||||
}));
|
||||
// const testCompleteListener = this._register(new MutableDisposable());
|
||||
// todo(@connor4312): reimplement, maybe
|
||||
// this._register(resultService.onResultsChanged(results => {
|
||||
// testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: [] }));
|
||||
// }));
|
||||
|
||||
testService.updateRootProviderCount(1);
|
||||
|
||||
const lastCompleted = resultService.results.find(r => !r.isComplete);
|
||||
if (lastCompleted) {
|
||||
this.proxy.$publishTestResults({ tests: lastCompleted.tests });
|
||||
}
|
||||
|
||||
for (const { resource, uri } of this.testService.subscriptions) {
|
||||
this.proxy.$subscribeToTests(resource, uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
$updateTestStateInRun(runId: string, testId: string, state: ITestState): void {
|
||||
const r = this.resultService.getResult(runId);
|
||||
if (r && r instanceof LiveTestResult) {
|
||||
for (const message of state.messages) {
|
||||
if (message.location) {
|
||||
message.location.uri = URI.revive(message.location.uri);
|
||||
}
|
||||
}
|
||||
|
||||
r.updateState(testId, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -105,8 +111,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
|
|||
this.testService.publishDiff(resource, URI.revive(uri), diff);
|
||||
}
|
||||
|
||||
public $runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult> {
|
||||
return this.testService.runTests(req, token);
|
||||
public async $runTests(req: RunTestsRequest, token: CancellationToken): Promise<string> {
|
||||
const result = await this.testService.runTests(req, token);
|
||||
return result.id;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
|
|
@ -1297,10 +1297,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
// checkProposedApiEnabled(extension);
|
||||
return extHostTypes.TestMessageSeverity;
|
||||
},
|
||||
get TestState() {
|
||||
// checkProposedApiEnabled(extension);
|
||||
return extHostTypes.TestState;
|
||||
},
|
||||
get WorkspaceTrustState() {
|
||||
// checkProposedApiEnabled(extension);
|
||||
return extHostTypes.WorkspaceTrustState;
|
||||
|
|
|
@ -58,7 +58,7 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib
|
|||
import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync';
|
||||
import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { InternalTestItem, InternalTestResults, ITestState, RunTestForProviderRequest, RunTestsRequest, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
|
||||
import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust';
|
||||
|
||||
|
@ -1826,7 +1826,7 @@ export const enum ExtHostTestingResource {
|
|||
}
|
||||
|
||||
export interface ExtHostTestingShape {
|
||||
$runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<RunTestsResult>;
|
||||
$runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<void>;
|
||||
$subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||
$unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||
$lookupTest(test: TestIdWithProvider): Promise<InternalTestItem | undefined>;
|
||||
|
@ -1840,7 +1840,8 @@ export interface MainThreadTestingShape {
|
|||
$subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||
$unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
|
||||
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult>;
|
||||
$updateTestStateInRun(runId: string, testId: string, state: ITestState): void;
|
||||
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<string>;
|
||||
}
|
||||
|
||||
// --- proxy identifiers
|
||||
|
|
|
@ -16,11 +16,11 @@ import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTes
|
|||
import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData';
|
||||
import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
||||
import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { TestItem, TestState } from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { Disposable } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
|
||||
import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
|
||||
import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import type * as vscode from 'vscode';
|
||||
|
||||
const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`;
|
||||
|
@ -93,9 +93,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
// Find workspace items first, then owned tests, then document tests.
|
||||
// If a test instance exists in both the workspace and document, prefer
|
||||
// the workspace because it's less ephemeral.
|
||||
.map(test => this.workspaceObservers.getMirroredTestDataByReference(test)
|
||||
?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test))
|
||||
?? this.textDocumentObservers.getMirroredTestDataByReference(test))
|
||||
.map(this.getInternalTestForReference, this)
|
||||
.filter(isDefined)
|
||||
.map(item => ({ providerId: item.providerId, testId: item.id })),
|
||||
debug: req.debug
|
||||
|
@ -219,10 +217,10 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
* providers to be run.
|
||||
* @override
|
||||
*/
|
||||
public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise<RunTestsResult> {
|
||||
public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise<void> {
|
||||
const provider = this.providers.get(req.providerId);
|
||||
if (!provider || !provider.runTests) {
|
||||
return EMPTY_TEST_RESULT;
|
||||
return;
|
||||
}
|
||||
|
||||
const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual)
|
||||
|
@ -230,16 +228,25 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
// Only send the actual TestItem's to the user to run.
|
||||
.map(t => t instanceof TestItemFilteredWrapper ? t.actual : t);
|
||||
if (!tests.length) {
|
||||
return EMPTY_TEST_RESULT;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await provider.runTests({ tests, debug: req.debug }, cancellation);
|
||||
await provider.runTests({
|
||||
setState: (test, state) => {
|
||||
const internal = this.getInternalTestForReference(test);
|
||||
if (internal) {
|
||||
this.flushCollectionDiffs();
|
||||
this.proxy.$updateTestStateInRun(req.runId, internal.id, TestState.from(state));
|
||||
}
|
||||
}, tests, debug: req.debug
|
||||
}, cancellation);
|
||||
|
||||
for (const { collection } of this.testSubscriptions.values()) {
|
||||
collection.flushDiff(); // ensure all states are updated
|
||||
}
|
||||
|
||||
return EMPTY_TEST_RESULT;
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error(e); // so it appears to attached debuggers
|
||||
throw e;
|
||||
|
@ -256,6 +263,28 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
return Promise.resolve(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes diff information for all collections to ensure state in the
|
||||
* main thread is updated.
|
||||
*/
|
||||
private flushCollectionDiffs() {
|
||||
for (const { collection } of this.testSubscriptions.values()) {
|
||||
collection.flushDiff();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal test item associated with the reference from the extension.
|
||||
*/
|
||||
private getInternalTestForReference(test: vscode.TestItem) {
|
||||
// Find workspace items first, then owned tests, then document tests.
|
||||
// If a test instance exists in both the workspace and document, prefer
|
||||
// the workspace because it's less ephemeral.
|
||||
return this.workspaceObservers.getMirroredTestDataByReference(test)
|
||||
?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test))
|
||||
?? this.textDocumentObservers.getMirroredTestDataByReference(test);
|
||||
}
|
||||
|
||||
private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy<vscode.TestItem> | undefined {
|
||||
if (!folder) {
|
||||
return;
|
||||
|
@ -361,10 +390,6 @@ export class TestItemFilteredWrapper implements vscode.TestItem {
|
|||
return this.actual.runnable;
|
||||
}
|
||||
|
||||
public get state() {
|
||||
return this.actual.state;
|
||||
}
|
||||
|
||||
public get children() {
|
||||
// We only want children that match the filter.
|
||||
return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter);
|
||||
|
@ -645,7 +670,6 @@ class TestItemFromMirror implements vscode.RequiredTestItem {
|
|||
public get id() { return this.#internal.revived.id!; }
|
||||
public get label() { return this.#internal.revived.label; }
|
||||
public get description() { return this.#internal.revived.description; }
|
||||
public get state() { return this.#internal.revived.state; }
|
||||
public get location() { return this.#internal.revived.location; }
|
||||
public get runnable() { return this.#internal.revived.runnable ?? true; }
|
||||
public get debuggable() { return this.#internal.revived.debuggable ?? false; }
|
||||
|
@ -665,7 +689,6 @@ class TestItemFromMirror implements vscode.RequiredTestItem {
|
|||
id: this.id,
|
||||
label: this.label,
|
||||
description: this.description,
|
||||
state: this.state,
|
||||
location: this.location,
|
||||
runnable: this.runnable,
|
||||
debuggable: this.debuggable,
|
||||
|
|
|
@ -1478,22 +1478,22 @@ export namespace NotebookDecorationRenderOptions {
|
|||
export namespace TestState {
|
||||
export function from(item: vscode.TestState): ITestState {
|
||||
return {
|
||||
runState: item.runState,
|
||||
state: item.state,
|
||||
duration: item.duration,
|
||||
messages: item.messages.map(message => ({
|
||||
messages: item.messages?.map(message => ({
|
||||
message: MarkdownString.fromStrict(message.message) || '',
|
||||
severity: message.severity,
|
||||
expectedOutput: message.expectedOutput,
|
||||
actualOutput: message.actualOutput,
|
||||
location: message.location ? location.from(message.location) : undefined,
|
||||
})),
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export function to(item: ITestState): vscode.TestState {
|
||||
return new types.TestState(
|
||||
item.runState,
|
||||
item.messages.map(message => ({
|
||||
return {
|
||||
state: item.state,
|
||||
messages: item.messages.map(message => ({
|
||||
message: typeof message.message === 'string' ? message.message : MarkdownString.to(message.message),
|
||||
severity: message.severity,
|
||||
expectedOutput: message.expectedOutput,
|
||||
|
@ -1503,8 +1503,8 @@ export namespace TestState {
|
|||
uri: URI.revive(message.location.uri)
|
||||
}),
|
||||
})),
|
||||
item.duration,
|
||||
);
|
||||
duration: item.duration,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1518,7 +1518,6 @@ export namespace TestItem {
|
|||
debuggable: item.debuggable ?? false,
|
||||
description: item.description,
|
||||
runnable: item.runnable ?? true,
|
||||
state: TestState.from(item.state),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1533,7 +1532,6 @@ export namespace TestItem {
|
|||
debuggable: item.debuggable,
|
||||
description: item.description,
|
||||
runnable: item.runnable,
|
||||
state: TestState.to(item.state),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2849,6 +2849,33 @@ export enum ColorThemeKind {
|
|||
|
||||
//#region Notebook
|
||||
|
||||
export class NotebookCellMetadata {
|
||||
|
||||
readonly readonly: boolean;
|
||||
readonly inputCollapsed: boolean;
|
||||
readonly outputCollapsed: boolean;
|
||||
|
||||
[key: string]: unknown;
|
||||
|
||||
constructor(readonly: boolean, inputCollapsed: boolean, outputCollapsed: boolean) {
|
||||
this.readonly = readonly;
|
||||
this.inputCollapsed = inputCollapsed;
|
||||
this.outputCollapsed = outputCollapsed;
|
||||
}
|
||||
|
||||
with(change: Partial<{ readonly: boolean, inputCollapsed: boolean, outputCollapsed: boolean, [key: string]: unknown }>): NotebookCellMetadata {
|
||||
const thisAndChange = { ...this, ...change };
|
||||
const res = new NotebookCellMetadata(thisAndChange.readonly, thisAndChange.inputCollapsed, thisAndChange.outputCollapsed);
|
||||
for (const key in change) {
|
||||
if (Object.prototype.hasOwnProperty.call(change, key)) {
|
||||
res[key] = change[key];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class NotebookCellOutputItem {
|
||||
|
||||
static isNotebookCellOutputItem(obj: unknown): obj is vscode.NotebookCellOutputItem {
|
||||
|
@ -3018,31 +3045,6 @@ export enum TestMessageSeverity {
|
|||
Hint = 3
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class TestState {
|
||||
#runState: TestRunState;
|
||||
#duration?: number;
|
||||
#messages: ReadonlyArray<Readonly<vscode.TestMessage>>;
|
||||
|
||||
public get runState() {
|
||||
return this.#runState;
|
||||
}
|
||||
|
||||
public get duration() {
|
||||
return this.#duration;
|
||||
}
|
||||
|
||||
public get messages() {
|
||||
return this.#messages;
|
||||
}
|
||||
|
||||
constructor(runState: TestRunState, messages: vscode.TestMessage[] = [], duration?: number) {
|
||||
this.#runState = runState;
|
||||
this.#messages = Object.freeze(messages.map(m => Object.freeze(m)));
|
||||
this.#duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
export type RequiredTestItem = vscode.RequiredTestItem;
|
||||
|
||||
export type TestItem = vscode.TestItem;
|
||||
|
|
|
@ -182,6 +182,11 @@ const apiMenus: IAPIMenu[] = [
|
|||
id: MenuId.TimelineItemContext,
|
||||
description: localize('view.timelineContext', "The Timeline view item context menu")
|
||||
},
|
||||
{
|
||||
key: 'ports/item/context',
|
||||
id: MenuId.TunnelContext,
|
||||
description: localize('view.tunnelContext', "The Ports view item context menu")
|
||||
}
|
||||
];
|
||||
|
||||
namespace schema {
|
||||
|
|
|
@ -104,7 +104,7 @@ export class BreadcrumbsModel {
|
|||
}
|
||||
|
||||
const breadcrumbsElements = this._currentOutline.value.config.breadcrumbsDataSource.getBreadcrumbElements();
|
||||
for (let i = this._cfgSymbolPath.getValue() === 'last' ? breadcrumbsElements.length - 1 : 0; i < breadcrumbsElements.length; i++) {
|
||||
for (let i = this._cfgSymbolPath.getValue() === 'last' && breadcrumbsElements.length > 0 ? breadcrumbsElements.length - 1 : 0; i < breadcrumbsElements.length; i++) {
|
||||
result.push(new OutlineElement2(breadcrumbsElements[i], this._currentOutline.value));
|
||||
}
|
||||
|
||||
|
|
|
@ -469,7 +469,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.
|
|||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All") }, group: '5_close', order: 10 });
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeAllSaved', "Close Saved") }, group: '5_close', order: 20 });
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.TOGGLE_KEEP_EDITORS_COMMAND_ID, title: nls.localize('toggleKeepEditors', "Keep Editors Open"), toggled: ContextKeyExpr.not('config.workbench.editor.enablePreview') }, group: '7_settings', order: 10 });
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { submenu: MenuId.EditorTitleRun, title: { value: nls.localize('run', "Run"), original: 'Run', }, icon: Codicon.run, group: 'navigation' });
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { submenu: MenuId.EditorTitleRun, title: { value: nls.localize('run', "Run"), original: 'Run', }, icon: Codicon.run, group: 'navigation', order: -1 });
|
||||
|
||||
interface IEditorToolItem { id: string; title: string; icon?: { dark?: URI; light?: URI; } | ThemeIcon; }
|
||||
|
||||
|
|
|
@ -64,13 +64,7 @@
|
|||
}
|
||||
|
||||
.monaco-workbench .pane > .pane-body > .welcome-view .monaco-button {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.monaco-workbench .pane > .pane-body.wide > .welcome-view .monaco-button {
|
||||
margin-left: inherit;
|
||||
max-width: 260px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.monaco-workbench .pane > .pane-body .welcome-view-content {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchCo
|
|||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { IDisplayMainService } from 'vs/platform/display/common/displayMainService';
|
||||
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { clearAllFontInfos } from 'vs/editor/browser/config/configuration';
|
||||
|
||||
class DisplayChangeRemeasureFonts extends Disposable implements IWorkbenchContribution {
|
||||
|
@ -18,7 +18,7 @@ class DisplayChangeRemeasureFonts extends Disposable implements IWorkbenchContri
|
|||
@IMainProcessService mainProcessService: IMainProcessService
|
||||
) {
|
||||
super();
|
||||
const displayMainService = createChannelSender<IDisplayMainService>(mainProcessService.getChannel('display'));
|
||||
const displayMainService = ProxyChannel.toService<IDisplayMainService>(mainProcessService.getChannel('display'));
|
||||
displayMainService.onDidDisplayChanged(() => {
|
||||
clearAllFontInfos();
|
||||
});
|
||||
|
|
|
@ -76,7 +76,6 @@ function revealLastElement(tree: WorkbenchAsyncDataTree<any, any, any>) {
|
|||
|
||||
const sessionsToIgnore = new Set<IDebugSession>();
|
||||
const identityProvider = { getId: (element: IReplElement) => element.getId() };
|
||||
const diffIdentityProvider = { getId: (element: IReplElement) => element.getId() + element.toString() };
|
||||
|
||||
export class Repl extends ViewPane implements IHistoryNavigationWidget {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
@ -513,7 +512,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
|
|||
}
|
||||
|
||||
const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight;
|
||||
await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider });
|
||||
await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider: identityProvider });
|
||||
|
||||
const session = this.tree.getInput();
|
||||
if (session) {
|
||||
|
|
|
@ -704,6 +704,7 @@ declare module DebugProtocol {
|
|||
The request configures the debuggers response to thrown exceptions.
|
||||
If an exception is configured to break, a 'stopped' event is fired (with reason 'exception').
|
||||
Clients should only call this request if the capability 'exceptionBreakpointFilters' returns one or more filters.
|
||||
If a filter or filter option is invalid (e.g. due to an invalid 'condition'), the request should fail with an 'ErrorResponse' explaining the problem(s).
|
||||
*/
|
||||
export interface SetExceptionBreakpointsRequest extends Request {
|
||||
// command: 'setExceptionBreakpoints';
|
||||
|
@ -1629,10 +1630,14 @@ declare module DebugProtocol {
|
|||
filter: string;
|
||||
/** The name of the filter option. This will be shown in the UI. */
|
||||
label: string;
|
||||
/** An optional help text providing additional information about the exception filter. This string is typically shown as a hover and must be translated. */
|
||||
description?: string;
|
||||
/** Initial value of the filter option. If not specified a value 'false' is assumed. */
|
||||
default?: boolean;
|
||||
/** Controls whether a condition can be specified for this filter option. If false or missing, a condition can not be set. */
|
||||
supportsCondition?: boolean;
|
||||
/** An optional help text providing information about the condition. This string is shown as the placeholder text for a text box and must be translated. */
|
||||
conditionDescription?: string;
|
||||
}
|
||||
|
||||
/** A structured message object. Used to return errors from requests. */
|
||||
|
@ -1774,6 +1779,8 @@ declare module DebugProtocol {
|
|||
endLine?: number;
|
||||
/** An optional end column of the range covered by the stack frame. */
|
||||
endColumn?: number;
|
||||
/** Indicates whether this frame can be restarted with the 'restart' request. Clients should only use this if the debug adapter supports the 'restart' request (capability 'supportsRestartRequest' is true). */
|
||||
canRestart?: boolean;
|
||||
/** Optional memory reference for the current instruction pointer in this frame. */
|
||||
instructionPointerReference?: string;
|
||||
/** The module associated with this frame, if any. */
|
||||
|
|
|
@ -16,6 +16,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
|||
|
||||
const MAX_REPL_LENGTH = 10000;
|
||||
let topReplElementCounter = 0;
|
||||
const getUniqueId = () => `topReplElement:${topReplElementCounter++}`;
|
||||
|
||||
export class SimpleReplElement implements IReplElement {
|
||||
|
||||
|
@ -234,13 +235,14 @@ export class ReplModel {
|
|||
return;
|
||||
}
|
||||
if (!previousElement.value.endsWith('\n') && !previousElement.value.endsWith('\r\n') && previousElement.count === 1) {
|
||||
previousElement.value += data;
|
||||
this.replElements[this.replElements.length - 1] = new SimpleReplElement(
|
||||
session, getUniqueId(), previousElement.value + data, sev, source);
|
||||
this._onDidChangeElements.fire();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const element = new SimpleReplElement(session, `topReplElement:${topReplElementCounter++}`, data, sev, source);
|
||||
const element = new SimpleReplElement(session, getUniqueId(), data, sev, source);
|
||||
this.addReplElement(element);
|
||||
} else {
|
||||
// TODO@Isidor hack, we should introduce a new type which is an output that can fetch children like an expression
|
||||
|
@ -315,7 +317,7 @@ export class ReplModel {
|
|||
}
|
||||
|
||||
// show object
|
||||
this.appendToRepl(session, new RawObjectReplElement(`topReplElement:${topReplElementCounter++}`, (<any>a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source);
|
||||
this.appendToRepl(session, new RawObjectReplElement(getUniqueId(), (<any>a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source);
|
||||
}
|
||||
|
||||
// string: watch out for % replacement directive
|
||||
|
|
|
@ -2193,7 +2193,7 @@ export class SystemDisabledWarningAction extends ExtensionAction {
|
|||
}
|
||||
if (this.workspaceTrustService.isWorkspaceTrustEnabled() && this.extension.enablementState === EnablementState.DisabledByTrustRequirement) {
|
||||
this.class = `${SystemDisabledWarningAction.TRUST_CLASS}`;
|
||||
this.tooltip = localize('extension disabled because of trust requirement', "This extension has been disabled as it requires a trusted workspace");
|
||||
this.tooltip = localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,5 @@ export const starEmptyIcon = registerIcon('extensions-star-empty', Codicon.starE
|
|||
|
||||
export const warningIcon = registerIcon('extensions-warning-message', Codicon.warning, localize('warningIcon', 'Icon shown with a warning message in the extensions editor.'));
|
||||
export const infoIcon = registerIcon('extensions-info-message', Codicon.info, localize('infoIcon', 'Icon shown with an info message in the extensions editor.'));
|
||||
export const trustIcon = registerIcon('extension-trust-message', Codicon.shield, localize('trustIcon', 'Icon shown with a message in the extension editor.'));
|
||||
|
||||
export const trustIcon = registerIcon('extension-workspace-trust', Codicon.shield, localize('trustIcon', 'Icon shown with a workspace trust message in the extension editor.'));
|
||||
|
|
|
@ -34,6 +34,9 @@
|
|||
}
|
||||
|
||||
.extension-install-count .codicon,
|
||||
.extension-action.codicon-extensions-info-message,
|
||||
.extension-action.codicon-extensions-warning-message,
|
||||
.extension-action.codicon-extension-workspace-trust,
|
||||
.extension-action.codicon-extensions-manage {
|
||||
color: inherit;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import { ILogService } from 'vs/platform/log/common/log';
|
|||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { IExplorerService } from 'vs/workbench/contrib/files/browser/files';
|
||||
import { listenStream } from 'vs/base/common/stream';
|
||||
|
||||
export const NEW_FILE_COMMAND_ID = 'explorer.newFile';
|
||||
export const NEW_FILE_LABEL = nls.localize('newFile', "New File");
|
||||
|
@ -1034,22 +1035,22 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => {
|
|||
reject();
|
||||
}));
|
||||
|
||||
sourceStream.on('data', data => {
|
||||
if (!disposed) {
|
||||
target.write(data.buffer);
|
||||
reportProgress(contents.name, contents.size, data.byteLength, operation);
|
||||
listenStream(sourceStream, {
|
||||
onData: data => {
|
||||
if (!disposed) {
|
||||
target.write(data.buffer);
|
||||
reportProgress(contents.name, contents.size, data.byteLength, operation);
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
disposables.dispose();
|
||||
reject(error);
|
||||
},
|
||||
onEnd: () => {
|
||||
disposables.dispose();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
sourceStream.on('error', error => {
|
||||
disposables.dispose();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
sourceStream.on('end', () => {
|
||||
disposables.dispose();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1624,7 +1624,10 @@ export class ChangeCellLanguageAction extends NotebookCellAction {
|
|||
const modelService = accessor.get(IModelService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
|
||||
const providerLanguages = [...context.notebookEditor.viewModel.notebookDocument.resolvedLanguages, 'markdown'];
|
||||
const providerLanguages = [
|
||||
...(context.notebookEditor.activeKernel?.supportedLanguages ?? context.notebookEditor.viewModel.notebookDocument.resolvedLanguages),
|
||||
'markdown'
|
||||
];
|
||||
providerLanguages.forEach(languageId => {
|
||||
let description: string;
|
||||
if (context.cell.cellKind === CellKind.Markdown ? (languageId === 'markdown') : (languageId === context.cell.language)) {
|
||||
|
|
|
@ -91,8 +91,8 @@ registerAction2(class extends Action2 {
|
|||
) : undefined;
|
||||
|
||||
if (selectedKernel) {
|
||||
editor.activeKernel = selectedKernel!;
|
||||
return selectedKernel!.resolve(editor.uri!, editor.getId(), tokenSource.token);
|
||||
editor.activeKernel = selectedKernel;
|
||||
return selectedKernel.resolve(editor.uri!, editor.getId(), tokenSource.token);
|
||||
} else {
|
||||
picker.show();
|
||||
}
|
||||
|
@ -224,4 +224,3 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution {
|
|||
}
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(KernelStatus, LifecyclePhase.Ready);
|
||||
|
||||
|
|
|
@ -1470,6 +1470,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
|||
const nextIndex = ui ? this.viewModel.getNextVisibleCellIndex(index) : index + 1;
|
||||
let language;
|
||||
if (type === CellKind.Code) {
|
||||
const supportedLanguages = this._activeKernel?.supportedLanguages ?? this.viewModel.notebookDocument.resolvedLanguages;
|
||||
const defaultLanguage = supportedLanguages[0] || 'plaintext';
|
||||
if (cell?.cellKind === CellKind.Code) {
|
||||
language = cell.language;
|
||||
} else if (cell?.cellKind === CellKind.Markdown) {
|
||||
|
@ -1477,20 +1479,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
|||
if (nearestCodeCellIndex > -1) {
|
||||
language = this.viewModel.viewCells[nearestCodeCellIndex].language;
|
||||
} else {
|
||||
language = this.viewModel.resolvedLanguages[0] || 'plaintext';
|
||||
language = defaultLanguage;
|
||||
}
|
||||
} else {
|
||||
if (cell === undefined && direction === 'above') {
|
||||
// insert cell at the very top
|
||||
language = this.viewModel.viewCells.find(cell => cell.cellKind === CellKind.Code)?.language || this.viewModel.resolvedLanguages[0] || 'plaintext';
|
||||
language = this.viewModel.viewCells.find(cell => cell.cellKind === CellKind.Code)?.language || defaultLanguage;
|
||||
} else {
|
||||
language = this.viewModel.resolvedLanguages[0] || 'plaintext';
|
||||
language = defaultLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.viewModel.resolvedLanguages.indexOf(language) < 0) {
|
||||
if (!supportedLanguages.includes(language)) {
|
||||
// the language no longer exists
|
||||
language = this.viewModel.resolvedLanguages[0] || 'plaintext';
|
||||
language = defaultLanguage;
|
||||
}
|
||||
} else {
|
||||
language = 'markdown';
|
||||
|
@ -1500,8 +1502,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
|
|||
(direction === 'above' ? index : nextIndex) :
|
||||
index;
|
||||
const focused = this._list.getFocusedElements();
|
||||
const newCell = this.viewModel.createCell(insertIndex, initialText, language, type, undefined, [], true, undefined, focused);
|
||||
return newCell as CellViewModel;
|
||||
return this.viewModel.createCell(insertIndex, initialText, language, type, undefined, [], true, undefined, focused);
|
||||
}
|
||||
|
||||
async splitNotebookCell(cell: ICellViewModel): Promise<CellViewModel[] | null> {
|
||||
|
|
|
@ -40,13 +40,9 @@ import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService } from
|
|||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
|
||||
export class NotebookKernelProviderInfoStore extends Disposable {
|
||||
export class NotebookKernelProviderInfoStore {
|
||||
private readonly _notebookKernelProviders: INotebookKernelProvider[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
add(provider: INotebookKernelProvider) {
|
||||
this._notebookKernelProviders.push(provider);
|
||||
this._updateProviderExtensionsInfo();
|
||||
|
@ -66,7 +62,7 @@ export class NotebookKernelProviderInfoStore extends Disposable {
|
|||
}
|
||||
|
||||
getContributedKernelProviders() {
|
||||
return [...this._notebookKernelProviders.values()];
|
||||
return [...this._notebookKernelProviders];
|
||||
}
|
||||
|
||||
private _updateProviderExtensionsInfo() {
|
||||
|
|
|
@ -168,10 +168,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
|
|||
return this._notebook;
|
||||
}
|
||||
|
||||
get resolvedLanguages() {
|
||||
return this._notebook.resolvedLanguages;
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this._notebook.uri;
|
||||
}
|
||||
|
|
|
@ -189,6 +189,8 @@ export interface INotebookTextModel {
|
|||
metadata: NotebookDocumentMetadata
|
||||
readonly uri: URI;
|
||||
readonly versionId: number;
|
||||
|
||||
/** @deprecated */
|
||||
languages: string[];
|
||||
readonly cells: readonly ICell[];
|
||||
onWillDispose(listener: () => void): IDisposable;
|
||||
|
@ -722,6 +724,7 @@ export interface INotebookKernelInfoDto2 {
|
|||
detail?: string;
|
||||
isPreferred?: boolean;
|
||||
preloads?: UriComponents[];
|
||||
supportedLanguages?: string[]
|
||||
}
|
||||
|
||||
export interface INotebookKernelInfo2 extends INotebookKernelInfoDto2 {
|
||||
|
|
|
@ -462,6 +462,7 @@ class OutputAutomaticPortForwarding extends Disposable {
|
|||
class ProcAutomaticPortForwarding extends Disposable {
|
||||
private candidateListener: IDisposable | undefined;
|
||||
private autoForwarded: Set<string> = new Set();
|
||||
private notifiedOnly: Set<string> = new Set();
|
||||
private notifier: OnAutoForwardedAction;
|
||||
private initialCandidates: Set<string> = new Set();
|
||||
private portsFeatures: IDisposable | undefined;
|
||||
|
@ -543,18 +544,18 @@ class ProcAutomaticPortForwarding extends Disposable {
|
|||
if (this.initialCandidates.has(address)) {
|
||||
return undefined;
|
||||
}
|
||||
const alreadyForwarded = mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, value.host, value.port);
|
||||
if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, value.host, value.port)) {
|
||||
return undefined;
|
||||
}
|
||||
if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, value.host, value.port)) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.portsAttributes.getAttributes(value.port)?.onAutoForward === OnPortForward.Ignore) {
|
||||
return undefined;
|
||||
}
|
||||
const forwarded = await this.remoteExplorerService.forward(value, undefined, undefined, undefined, undefined, undefined, false);
|
||||
if (forwarded) {
|
||||
if (!alreadyForwarded && forwarded) {
|
||||
this.autoForwarded.add(address);
|
||||
} else if (forwarded) {
|
||||
this.notifiedOnly.add(address);
|
||||
}
|
||||
return forwarded;
|
||||
}))).filter(tunnel => !!tunnel);
|
||||
|
@ -571,6 +572,9 @@ class ProcAutomaticPortForwarding extends Disposable {
|
|||
this.remoteExplorerService.close(value);
|
||||
this.autoForwarded.delete(key);
|
||||
removedPorts.push(value.port);
|
||||
} else if (this.notifiedOnly.has(key)) {
|
||||
this.notifiedOnly.delete(key);
|
||||
removedPorts.push(value.port);
|
||||
} else if (this.initialCandidates.has(key)) {
|
||||
this.initialCandidates.delete(key);
|
||||
}
|
||||
|
|
|
@ -812,23 +812,26 @@ namespace LabelTunnelAction {
|
|||
export const LABEL = nls.localize('remote.tunnel.label', "Set Label");
|
||||
|
||||
export function handler(): ICommandHandler {
|
||||
return async (accessor, arg) => {
|
||||
return async (accessor, arg): Promise<{ port: number, label: string } | undefined> => {
|
||||
const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
|
||||
if (context instanceof TunnelItem) {
|
||||
const remoteExplorerService = accessor.get(IRemoteExplorerService);
|
||||
remoteExplorerService.setEditable(context, {
|
||||
onFinish: async (value, success) => {
|
||||
if (success) {
|
||||
remoteExplorerService.tunnelModel.name(context.remoteHost, context.remotePort, value);
|
||||
}
|
||||
remoteExplorerService.setEditable(context, null);
|
||||
},
|
||||
validationMessage: () => null,
|
||||
placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
|
||||
startingValue: context.name
|
||||
return new Promise(resolve => {
|
||||
const remoteExplorerService = accessor.get(IRemoteExplorerService);
|
||||
remoteExplorerService.setEditable(context, {
|
||||
onFinish: async (value, success) => {
|
||||
if (success) {
|
||||
remoteExplorerService.tunnelModel.name(context.remoteHost, context.remotePort, value);
|
||||
}
|
||||
remoteExplorerService.setEditable(context, null);
|
||||
resolve(success ? { port: context.remotePort, label: value } : undefined);
|
||||
},
|
||||
validationMessage: () => null,
|
||||
placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
|
||||
startingValue: context.name
|
||||
});
|
||||
});
|
||||
}
|
||||
return;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
|
|||
registerThemingParticipant((theme, collector) => {
|
||||
const sashHoverBorderColor = theme.getColor(sashHoverBorder);
|
||||
collector.addRule(`
|
||||
.monaco-sash:hover,
|
||||
.monaco-sash.hover,
|
||||
.monaco-sash.active {
|
||||
background: ${sashHoverBorderColor}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
|||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer';
|
||||
import { collectContextMenuActions, getStatusBarActionViewItem } from 'vs/workbench/contrib/scm/browser/util';
|
||||
import { collectContextMenuActions, getActionViewItemProvider } from 'vs/workbench/contrib/scm/browser/util';
|
||||
import { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
|
||||
class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
|
||||
|
@ -62,7 +62,7 @@ export class SCMRepositoriesViewPane extends ViewPane {
|
|||
const listContainer = append(container, $('.scm-view.scm-repositories-view'));
|
||||
|
||||
const delegate = new ListDelegate();
|
||||
const renderer = this.instantiationService.createInstance(RepositoryRenderer, getStatusBarActionViewItem);
|
||||
const renderer = this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService));
|
||||
const identityProvider = { getId: (r: ISCMRepository) => r.provider.id };
|
||||
|
||||
this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, listContainer, delegate, [renderer], {
|
||||
|
|
|
@ -23,7 +23,7 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options,
|
|||
import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IThemeService, registerThemingParticipant, IFileIconTheme } from 'vs/platform/theme/common/themeService';
|
||||
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getStatusBarActionViewItem } from './util';
|
||||
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider } from './util';
|
||||
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
|
||||
import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService';
|
||||
import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
|
@ -1831,10 +1831,10 @@ export class SCMViewPane extends ViewPane {
|
|||
this._register(actionRunner.onBeforeRun(() => this.tree.domFocus()));
|
||||
|
||||
const renderers: ICompressibleTreeRenderer<any, any, any>[] = [
|
||||
this.instantiationService.createInstance(RepositoryRenderer, getStatusBarActionViewItem),
|
||||
this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)),
|
||||
this.inputRenderer,
|
||||
this.instantiationService.createInstance(ResourceGroupRenderer, getStatusBarActionViewItem),
|
||||
this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getStatusBarActionViewItem, actionRunner)
|
||||
this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)),
|
||||
this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)
|
||||
];
|
||||
|
||||
const filter = new SCMTreeFilter();
|
||||
|
|
|
@ -7,14 +7,15 @@ import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput } from 'vs/w
|
|||
import { IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IDisposable, Disposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Action, IAction, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { Action, IAction, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { Command } from 'vs/editor/common/modes';
|
||||
import { reset } from 'vs/base/browser/dom';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export function isSCMRepository(element: any): element is ISCMRepository {
|
||||
return !!(element as ISCMRepository).provider && !!(element as ISCMRepository).input;
|
||||
|
@ -107,10 +108,12 @@ class StatusBarActionViewItem extends ActionViewItem {
|
|||
}
|
||||
}
|
||||
|
||||
export function getStatusBarActionViewItem(action: IAction): IActionViewItem | undefined {
|
||||
if (action instanceof StatusBarAction) {
|
||||
return new StatusBarActionViewItem(action);
|
||||
}
|
||||
export function getActionViewItemProvider(instaService: IInstantiationService): IActionViewItemProvider {
|
||||
return action => {
|
||||
if (action instanceof StatusBarAction) {
|
||||
return new StatusBarActionViewItem(action);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return createActionViewItem(instaService, action);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { Action2, ICommandAction, MenuId, MenuRegistry, registerAction2, SyncAct
|
|||
import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { ContextKeyAndExpr, ContextKeyEqualsExpr, ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyEqualsExpr, ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
|
@ -637,8 +637,6 @@ const viewDescriptor: IViewDescriptor = {
|
|||
mnemonicTitle: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search"),
|
||||
keybindings: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F,
|
||||
// View: Show Search is used for the keybindings in the View menu and the sidebar #115556, but it should only be enabled when search.mode == view, or else it will steal priority over opening a search editor #115511
|
||||
when: ContextKeyAndExpr.create([Constants.SearchViewVisibleKey.toNegated(), ContextKeyEqualsExpr.create(`config.${SEARCH_MODE_CONFIG}`, 'view')]),
|
||||
},
|
||||
order: 1
|
||||
}
|
||||
|
@ -712,7 +710,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
|
|||
]
|
||||
},
|
||||
id: Constants.FindInFilesActionId,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
// Give more weightage to this keybinding than of `View: Show Search` keybinding. See #116188, #115556, #115511
|
||||
weight: KeybindingWeight.WorkbenchContrib + 1,
|
||||
when: null,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F,
|
||||
handler: FindInFilesCommand
|
||||
|
|
|
@ -678,12 +678,18 @@ export class SearchView extends ViewPane {
|
|||
private clearMessage(): HTMLElement {
|
||||
this.searchWithoutFolderMessageElement = undefined;
|
||||
|
||||
const wasHidden = this.messagesElement.style.display === 'none';
|
||||
dom.clearNode(this.messagesElement);
|
||||
dom.show(this.messagesElement);
|
||||
dispose(this.messageDisposables);
|
||||
this.messageDisposables = [];
|
||||
|
||||
return dom.append(this.messagesElement, $('.message'));
|
||||
const newMessage = dom.append(this.messagesElement, $('.message'));
|
||||
if (wasHidden) {
|
||||
this.reLayout();
|
||||
}
|
||||
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
private createSearchResultsView(container: HTMLElement): void {
|
||||
|
@ -1085,6 +1091,7 @@ export class SearchView extends ViewPane {
|
|||
this.tree.ariaLabel = nls.localize('emptySearch', "Empty Search");
|
||||
|
||||
aria.status(nls.localize('ariaSearchResultsClearStatus', "The search results have been cleared"));
|
||||
this.reLayout();
|
||||
}
|
||||
|
||||
clearFilePatternFields(): void {
|
||||
|
|
|
@ -237,12 +237,12 @@ export function escapeNonWindowsPath(path: string): string {
|
|||
if (newPath.indexOf('\\') !== 0) {
|
||||
newPath = newPath.replace(/\\/g, '\\\\');
|
||||
}
|
||||
if (!newPath && (newPath.indexOf('"') !== -1)) {
|
||||
newPath = '\'' + newPath + '\'';
|
||||
} else if (newPath.indexOf(' ') !== -1) {
|
||||
if (newPath.indexOf(' ') !== -1) {
|
||||
newPath = newPath.replace(/ /g, '\\ ');
|
||||
}
|
||||
return newPath;
|
||||
const bannedChars = /[\`\$\|\&\>\~\#\!\^\*\;\<\"\']/g;
|
||||
newPath = newPath.replace(bannedChars, '');
|
||||
return `'${newPath}'`;
|
||||
}
|
||||
|
||||
export type TerminalShellSetting = (
|
||||
|
|
|
@ -10,13 +10,28 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
|||
import { URI } from 'vs/base/common/uri';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
|
||||
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
|
||||
import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
||||
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
|
||||
import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
|
||||
const computedStateAccessor: IComputedStateAccessor<ITestTreeElement> = {
|
||||
getOwnState: i => i.state,
|
||||
getCurrentComputedState: i => i.state,
|
||||
setComputedState: (i, s) => i.state = s,
|
||||
getChildren: i => i.children.values(),
|
||||
*getParents(i) {
|
||||
for (let parent = i.parentItem; parent; parent = parent.parentItem) {
|
||||
yield parent;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Projection that lists tests in their traditional tree view.
|
||||
*/
|
||||
|
@ -40,11 +55,42 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
|
|||
*/
|
||||
public readonly onUpdate = this.updateEmitter.event;
|
||||
|
||||
constructor(listener: TestSubscriptionListener) {
|
||||
constructor(listener: TestSubscriptionListener, @ITestResultService private readonly results: ITestResultService) {
|
||||
super();
|
||||
this._register(listener.onDiff(([folder, diff]) => this.applyDiff(folder, diff)));
|
||||
this._register(listener.onFolderChange(this.applyFolderChange, this));
|
||||
|
||||
// when test results are cleared, recalculate all state
|
||||
this._register(results.onResultsChanged((evt) => {
|
||||
if (!('removed' in evt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const inTree of [...this.items.values()].sort((a, b) => b.depth - a.depth)) {
|
||||
const lookup = this.results.getStateByExtId(inTree.test.item.extId)?.[1];
|
||||
inTree.ownState = lookup?.state.state ?? TestRunState.Unset;
|
||||
const computed = lookup?.computedState ?? TestRunState.Unset;
|
||||
if (computed !== inTree.state) {
|
||||
inTree.state = computed;
|
||||
this.addUpdated(inTree);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateEmitter.fire();
|
||||
}));
|
||||
|
||||
// when test states change, reflect in the tree
|
||||
this._register(results.onTestChanged(([, { item, state, computedState }]) => {
|
||||
for (const i of this.items.values()) {
|
||||
if (i.test.item.extId === item.extId) {
|
||||
i.ownState = state.state;
|
||||
refreshComputedState(computedStateAccessor, i, this.addUpdated, computedState);
|
||||
this.updateEmitter.fire();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
for (const [folder, collection] of listener.workspaceFolderCollections) {
|
||||
for (const node of collection.all) {
|
||||
this.storeItem(this.createItem(node, folder.folder));
|
||||
|
@ -96,7 +142,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
|
|||
|
||||
const locationChanged = !locationsEqual(existing.location, item.item.location);
|
||||
if (locationChanged) { this.locations.remove(existing); }
|
||||
existing.update(item, this.addUpdated);
|
||||
existing.update(item);
|
||||
if (locationChanged) { this.locations.add(existing); }
|
||||
this.addUpdated(existing);
|
||||
break;
|
||||
|
@ -172,5 +218,11 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
|
|||
item.parentItem.children.add(item);
|
||||
this.items.set(item.test.id, item);
|
||||
this.locations.add(item);
|
||||
|
||||
const prevState = this.results.getStateByExtId(item.test.item.extId)?.[1];
|
||||
if (prevState) {
|
||||
item.ownState = prevState.state.state;
|
||||
refreshComputedState(computedStateAccessor, item, this.addUpdated, prevState.computedState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { HierarchicalByLocationProjection as HierarchicalByLocationProjection }
|
|||
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
|
||||
import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
||||
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
|
||||
/**
|
||||
|
@ -64,9 +65,9 @@ export class HierarchicalByNameElement extends HierarchicalElement {
|
|||
/**
|
||||
* @override
|
||||
*/
|
||||
public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) {
|
||||
public update(actual: InternalTestItem) {
|
||||
const wasRunnable = this.test.item.runnable;
|
||||
super.update(actual, addUpdated);
|
||||
super.update(actual);
|
||||
|
||||
if (this.test.item.runnable !== wasRunnable) {
|
||||
this.updateLeafTestState();
|
||||
|
@ -117,8 +118,8 @@ export class HierarchicalByNameElement extends HierarchicalElement {
|
|||
* test root rather than the heirarchal parent.
|
||||
*/
|
||||
export class HierarchicalByNameProjection extends HierarchicalByLocationProjection {
|
||||
constructor(listener: TestSubscriptionListener) {
|
||||
super(listener);
|
||||
constructor(listener: TestSubscriptionListener, @ITestResultService results: ITestResultService) {
|
||||
super(listener, results);
|
||||
|
||||
const originalRenderNode = this.renderNode.bind(this);
|
||||
this.renderNode = (node, recurse) => {
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Iterable } from 'vs/base/common/iterator';
|
|||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
|
||||
/**
|
||||
|
@ -15,7 +14,6 @@ import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testi
|
|||
*/
|
||||
export class HierarchicalElement implements ITestTreeElement {
|
||||
public readonly children = new Set<HierarchicalElement>();
|
||||
public computedState: TestRunState | undefined;
|
||||
public readonly depth: number = this.parentItem.depth + 1;
|
||||
|
||||
public get treeId() {
|
||||
|
@ -26,10 +24,6 @@ export class HierarchicalElement implements ITestTreeElement {
|
|||
return this.test.item.label;
|
||||
}
|
||||
|
||||
public get state() {
|
||||
return this.test.item.state.runState;
|
||||
}
|
||||
|
||||
public get location() {
|
||||
return this.test.item.location;
|
||||
}
|
||||
|
@ -46,16 +40,15 @@ export class HierarchicalElement implements ITestTreeElement {
|
|||
: Iterable.empty();
|
||||
}
|
||||
|
||||
public state = TestRunState.Unset;
|
||||
public ownState = TestRunState.Unset;
|
||||
|
||||
constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) {
|
||||
this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese
|
||||
}
|
||||
|
||||
public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) {
|
||||
const stateChange = actual.item.state.runState !== this.state;
|
||||
public update(actual: InternalTestItem) {
|
||||
Object.assign(this.test, actual);
|
||||
if (stateChange) {
|
||||
refreshComputedState(this, addUpdated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,67 +73,12 @@ export class HierarchicalFolder implements ITestTreeElement {
|
|||
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
|
||||
}
|
||||
|
||||
public state = TestRunState.Unset;
|
||||
public ownState = TestRunState.Unset;
|
||||
|
||||
constructor(private readonly folder: IWorkspaceFolder) { }
|
||||
|
||||
public get label() {
|
||||
return this.folder.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the computed state for the node.
|
||||
*/
|
||||
export const getComputedState = (node: ITestTreeElement) => {
|
||||
if (node.computedState === undefined) {
|
||||
node.computedState = node.state ?? TestRunState.Unset;
|
||||
for (const child of node.children) {
|
||||
node.computedState = maxPriority(node.computedState, getComputedState(child));
|
||||
}
|
||||
}
|
||||
|
||||
return node.computedState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes the computed state for the node and its parents. Any changes
|
||||
* elements cause `addUpdated` to be called.
|
||||
*/
|
||||
export const refreshComputedState = (node: ITestTreeElement, addUpdated: (n: ITestTreeElement) => void) => {
|
||||
if (node.computedState === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPriority = statePriority[node.computedState];
|
||||
node.computedState = undefined;
|
||||
const newState = getComputedState(node);
|
||||
const newPriority = statePriority[getComputedState(node)];
|
||||
if (newPriority === oldPriority) {
|
||||
return;
|
||||
}
|
||||
|
||||
addUpdated(node);
|
||||
if (newPriority > oldPriority) {
|
||||
// Update all parents to ensure they're at least this priority.
|
||||
for (let parent = node.parentItem; parent; parent = parent.parentItem) {
|
||||
const prev = parent.computedState;
|
||||
if (prev !== undefined && statePriority[prev] >= newPriority) {
|
||||
break;
|
||||
}
|
||||
|
||||
parent.computedState = newState;
|
||||
addUpdated(parent);
|
||||
}
|
||||
} else if (newPriority < oldPriority) {
|
||||
// Re-render all parents of this node whose computed priority might have come from this node
|
||||
for (let parent = node.parentItem; parent; parent = parent.parentItem) {
|
||||
const prev = parent.computedState;
|
||||
if (prev === undefined || statePriority[prev] > oldPriority) {
|
||||
break;
|
||||
}
|
||||
|
||||
parent.computedState = undefined;
|
||||
parent.computedState = getComputedState(parent);
|
||||
addUpdated(parent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -40,13 +40,6 @@ export interface ITestTreeProjection extends IDisposable {
|
|||
|
||||
|
||||
export interface ITestTreeElement {
|
||||
/**
|
||||
* Computed element state. Will be set automatically if not initially provided.
|
||||
* The projection is responsible for clearing (or updating) this if it
|
||||
* becomes invalid.
|
||||
*/
|
||||
computedState: TestRunState | undefined;
|
||||
|
||||
readonly children: Set<ITestTreeElement>;
|
||||
|
||||
/**
|
||||
|
@ -85,9 +78,11 @@ export interface ITestTreeElement {
|
|||
readonly debuggable: Iterable<TestIdWithProvider>;
|
||||
|
||||
/**
|
||||
* State of of the tree item. Mostly used for deriving the computed state.
|
||||
* Element state to display.
|
||||
*/
|
||||
readonly state?: TestRunState;
|
||||
state: TestRunState;
|
||||
|
||||
readonly ownState: TestRunState;
|
||||
readonly label: string;
|
||||
readonly parentItem: ITestTreeElement | null;
|
||||
}
|
||||
|
|
|
@ -1,321 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { FuzzyScore } from 'vs/base/common/filters';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Location as ModeLocation } from 'vs/editor/common/modes';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
|
||||
import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
||||
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
|
||||
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { isRunningState, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
|
||||
interface IStatusTestItem extends IncrementalTestCollectionItem {
|
||||
treeElements: Map<TestRunState, TestStateElement>;
|
||||
previousState: TestRunState;
|
||||
depth: number;
|
||||
parentItem?: IStatusTestItem;
|
||||
location?: ModeLocation;
|
||||
}
|
||||
|
||||
type TreeElement = StateElement<TestStateElement> | TestStateElement;
|
||||
|
||||
class TestStateElement implements ITestTreeElement {
|
||||
public computedState = this.state;
|
||||
|
||||
public get treeId() {
|
||||
return `sltest:${this.test.id}`;
|
||||
}
|
||||
|
||||
public get label() {
|
||||
return this.test.item.label;
|
||||
}
|
||||
|
||||
public get location() {
|
||||
return this.test.item.location;
|
||||
}
|
||||
|
||||
public get runnable(): Iterable<TestIdWithProvider> {
|
||||
// if this item is runnable and all its children are in the same state,
|
||||
// we can run all of them in one go. This will eventually be true
|
||||
// for leaf nodes, whose treeElements contain only their own state.
|
||||
if (this.test.item.runnable && this.test.treeElements.size === 1) {
|
||||
return [{ testId: this.test.id, providerId: this.test.providerId }];
|
||||
}
|
||||
|
||||
return Iterable.concatNested(Iterable.map(this.children, c => c.runnable));
|
||||
}
|
||||
|
||||
public get debuggable(): Iterable<TestIdWithProvider> {
|
||||
// same logic as runnable above
|
||||
if (this.test.item.debuggable && this.test.treeElements.size === 1) {
|
||||
return [{ testId: this.test.id, providerId: this.test.providerId }];
|
||||
}
|
||||
|
||||
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
|
||||
}
|
||||
|
||||
public readonly depth = this.test.depth;
|
||||
public readonly children = new Set<TestStateElement>();
|
||||
|
||||
constructor(
|
||||
public readonly state: TestRunState,
|
||||
public readonly test: IStatusTestItem,
|
||||
public readonly parentItem: TestStateElement | StateElement<TestStateElement>,
|
||||
) {
|
||||
parentItem.children.add(this);
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.parentItem.children.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows tests in a hierarchical way, but grouped by status. This is more
|
||||
* complex than it may look at first glance, because nodes can appear in
|
||||
* multiple places if they have children with different statuses.
|
||||
*/
|
||||
export class StateByLocationProjection extends AbstractIncrementalTestCollection<IStatusTestItem> implements ITestTreeProjection {
|
||||
private readonly updateEmitter = new Emitter<void>();
|
||||
private readonly changes = new NodeChangeList<TreeElement>();
|
||||
private readonly locations = new TestLocationStore<IStatusTestItem>();
|
||||
private readonly disposable = new DisposableStore();
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public readonly onUpdate = this.updateEmitter.event;
|
||||
|
||||
/**
|
||||
* Root elements for states in the tree.
|
||||
*/
|
||||
protected readonly stateRoots = new Map<TestRunState, StateElement<TestStateElement>>();
|
||||
|
||||
constructor(listener: TestSubscriptionListener) {
|
||||
super();
|
||||
|
||||
this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff)));
|
||||
|
||||
const firstDiff: TestsDiff = [];
|
||||
for (const [, collection] of listener.workspaceFolderCollections) {
|
||||
firstDiff.push(...collection.getReviverDiff());
|
||||
}
|
||||
|
||||
this.apply(firstDiff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees listeners associated with the projection.
|
||||
*/
|
||||
public dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public getTestAtPosition(uri: URI, position: Position) {
|
||||
const item = this.locations.getTestAtPosition(uri, position);
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const state of statesInOrder) {
|
||||
const element = item.treeElements.get(state);
|
||||
if (element) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public applyTo(tree: ObjectTree<ITestTreeElement, FuzzyScore>) {
|
||||
this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values());
|
||||
}
|
||||
|
||||
private readonly renderNode: NodeRenderFn<TreeElement> = (node, recurse) => {
|
||||
if (node.depth === 1 /* test provider */) {
|
||||
if (node.children.size === 0) {
|
||||
return NodeRenderDirective.Omit;
|
||||
} else if (!peersHaveChildren(node, () => this.stateRoots.values())) {
|
||||
return NodeRenderDirective.Concat;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
element: node,
|
||||
children: recurse(node.children),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
protected createChangeCollector(): IncrementalChangeCollector<IStatusTestItem> {
|
||||
return {
|
||||
add: node => {
|
||||
this.resolveNodesRecursive(node);
|
||||
this.locations.add(node);
|
||||
},
|
||||
remove: (node, isNested) => {
|
||||
this.locations.remove(node);
|
||||
|
||||
if (!isNested) {
|
||||
for (const state of node.treeElements.keys()) {
|
||||
this.pruneStateElements(node, state, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
update: node => {
|
||||
const isRunning = isRunningState(node.item.state.runState);
|
||||
if (node.item.state.runState !== node.previousState) {
|
||||
if (isRunning && node.treeElements.has(node.previousState)) {
|
||||
node.treeElements.get(node.previousState)!.computedState = TestRunState.Running;
|
||||
} else {
|
||||
this.pruneStateElements(node, node.previousState);
|
||||
this.resolveNodesRecursive(node);
|
||||
}
|
||||
} else if (!isRunning) {
|
||||
const previous = node.treeElements.get(node.item.state.runState);
|
||||
if (previous) {
|
||||
previous.computedState = node.item.state.runState;
|
||||
}
|
||||
}
|
||||
|
||||
const locationChanged = !locationsEqual(node.location, node.item.location);
|
||||
if (locationChanged) {
|
||||
this.locations.remove(node);
|
||||
node.location = node.item.location;
|
||||
this.locations.add(node);
|
||||
}
|
||||
|
||||
const treeNode = node.treeElements.get(node.previousState)!;
|
||||
this.changes.updated(treeNode);
|
||||
},
|
||||
complete: () => {
|
||||
this.updateEmitter.fire();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures tree nodes for the item state are present in the tree.
|
||||
*/
|
||||
protected resolveNodesRecursive(item: IStatusTestItem) {
|
||||
const state = item.item.state.runState;
|
||||
item.previousState = item.item.state.runState;
|
||||
|
||||
// Create a list of items until the current item who don't have a tree node for the status yet
|
||||
let chain: IStatusTestItem[] = [];
|
||||
for (let i: IStatusTestItem | undefined = item; i && !i.treeElements.has(state); i = i.parentItem) {
|
||||
chain.push(i);
|
||||
}
|
||||
|
||||
for (let i = chain.length - 1; i >= 0; i--) {
|
||||
const item2 = chain[i];
|
||||
// the loop would have stopped pushing parents when either it reaches
|
||||
// the root, or it reaches a parent who already has a node for this state.
|
||||
const parent = item2.parentItem?.treeElements.get(state) ?? this.getOrCreateStateElement(state);
|
||||
const node = this.createElement(state, item2, parent);
|
||||
|
||||
item2.treeElements.set(state, node);
|
||||
parent.children.add(node);
|
||||
|
||||
if (i === chain.length - 1) {
|
||||
this.changes.added(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected createElement(state: TestRunState, item: IStatusTestItem, parent: TreeElement) {
|
||||
return new TestStateElement(state, item, parent);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively (from the leaf to the root) removes tree elements if there's
|
||||
* no children who have the given state left.
|
||||
*
|
||||
* Returns true if it resulted in a node being removed.
|
||||
*/
|
||||
protected pruneStateElements(item: IStatusTestItem | undefined, state: TestRunState, force = false) {
|
||||
if (!item) {
|
||||
const stateRoot = this.stateRoots.get(state);
|
||||
if (stateRoot?.children.size === 0) {
|
||||
this.changes.removed(stateRoot);
|
||||
this.stateRoots.delete(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const node = item.treeElements.get(state);
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check to make sure we aren't in the state, and there's no child with the
|
||||
// state. For the unset state, only show the node if it's a leaf or it
|
||||
// has children in the unset state.
|
||||
if (!force) {
|
||||
if (item.item.state.runState === state && !(state === TestRunState.Unset && item.children.size > 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const childId of item.children) {
|
||||
if (this.items.get(childId)?.treeElements.has(state)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If so, proceed to deletion and recurse upwards.
|
||||
item.treeElements.delete(state);
|
||||
node.remove();
|
||||
|
||||
if (!this.pruneStateElements(item.parentItem, state)) {
|
||||
this.changes.removed(node);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected getOrCreateStateElement(state: TestRunState) {
|
||||
let s = this.stateRoots.get(state);
|
||||
if (!s) {
|
||||
s = new StateElement(state);
|
||||
this.changes.added(s);
|
||||
this.stateRoots.set(state, s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
protected createItem(item: InternalTestItem, parentItem?: IStatusTestItem): IStatusTestItem {
|
||||
return {
|
||||
...item,
|
||||
depth: parentItem ? parentItem.depth + 1 : 1,
|
||||
parentItem: parentItem,
|
||||
previousState: item.item.state.runState,
|
||||
location: item.item.location,
|
||||
children: new Set(),
|
||||
treeElements: new Map(),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,289 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { FuzzyScore } from 'vs/base/common/filters';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Location as ModeLocation } from 'vs/editor/common/modes';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
import { ListElementType } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName';
|
||||
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
|
||||
import { NodeChangeList, NodeRenderFn } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
||||
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
|
||||
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { isRunningState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
|
||||
class ListTestStateElement implements ITestTreeElement {
|
||||
public computedState = this.test.item.state.runState;
|
||||
|
||||
public get treeId() {
|
||||
return `sntest:${this.test.id}`;
|
||||
}
|
||||
|
||||
public get label() {
|
||||
return this.test.item.label;
|
||||
}
|
||||
|
||||
public get location() {
|
||||
return this.test.item.location;
|
||||
}
|
||||
|
||||
public get runnable(): Iterable<TestIdWithProvider> {
|
||||
return this.test.item.runnable
|
||||
? [{ testId: this.test.id, providerId: this.test.providerId }]
|
||||
: Iterable.empty();
|
||||
}
|
||||
|
||||
public get debuggable(): Iterable<TestIdWithProvider> {
|
||||
return this.test.item.debuggable
|
||||
? [{ testId: this.test.id, providerId: this.test.providerId }]
|
||||
: Iterable.empty();
|
||||
}
|
||||
|
||||
public get description() {
|
||||
let description: string | undefined;
|
||||
for (let parent = this.test.parentItem; parent && parent.depth > 0; parent = parent.parentItem) {
|
||||
description = description ? `${parent.item.label} › ${description}` : parent.item.label;
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
public readonly depth = 1;
|
||||
public readonly children = new Set<never>();
|
||||
|
||||
constructor(
|
||||
public readonly test: IStatusListTestItem,
|
||||
public readonly parentItem: StateElement<ListTestStateElement>,
|
||||
) {
|
||||
parentItem.children.add(this);
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.parentItem.children.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
interface IStatusListTestItem extends IncrementalTestCollectionItem {
|
||||
node?: ListTestStateElement;
|
||||
type: ListElementType;
|
||||
previousState: TestRunState;
|
||||
depth: number;
|
||||
parentItem?: IStatusListTestItem;
|
||||
location?: ModeLocation;
|
||||
}
|
||||
|
||||
type TreeElement = StateElement<ListTestStateElement> | ListTestStateElement;
|
||||
|
||||
/**
|
||||
* Projection that shows tests in a flat list (grouped by status).
|
||||
*/
|
||||
export class StateByNameProjection extends AbstractIncrementalTestCollection<IStatusListTestItem> implements ITestTreeProjection {
|
||||
private readonly updateEmitter = new Emitter<void>();
|
||||
private readonly changes = new NodeChangeList<TreeElement>();
|
||||
private readonly locations = new TestLocationStore<IStatusListTestItem>();
|
||||
private readonly disposable = new DisposableStore();
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public readonly onUpdate = this.updateEmitter.event;
|
||||
|
||||
/**
|
||||
* Root elements for states in the tree.
|
||||
*/
|
||||
protected readonly stateRoots = new Map<TestRunState, StateElement<ListTestStateElement>>();
|
||||
|
||||
constructor(listener: TestSubscriptionListener) {
|
||||
super();
|
||||
|
||||
this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff)));
|
||||
|
||||
const firstDiff: TestsDiff = [];
|
||||
for (const [, collection] of listener.workspaceFolderCollections) {
|
||||
firstDiff.push(...collection.getReviverDiff());
|
||||
}
|
||||
|
||||
this.apply(firstDiff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees listeners associated with the projection.
|
||||
*/
|
||||
public dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public getTestAtPosition(uri: URI, position: Position) {
|
||||
return this.locations.getTestAtPosition(uri, position)?.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public applyTo(tree: ObjectTree<ITestTreeElement, FuzzyScore>) {
|
||||
this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values());
|
||||
}
|
||||
|
||||
private readonly renderNode: NodeRenderFn<TreeElement> = (node, recurse) => {
|
||||
return {
|
||||
element: node,
|
||||
children: node instanceof StateElement ? recurse(node.children) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
protected createChangeCollector(): IncrementalChangeCollector<IStatusListTestItem> {
|
||||
return {
|
||||
add: node => {
|
||||
this.resolveNodesRecursive(node);
|
||||
this.locations.add(node);
|
||||
},
|
||||
remove: (node, isNested) => {
|
||||
if (node.node) {
|
||||
this.locations.remove(node);
|
||||
}
|
||||
|
||||
// for the top node being deleted, we need to update parents. For
|
||||
// others we only need to remove them from the locations cache.
|
||||
if (isNested) {
|
||||
this.removeNodeSingle(node);
|
||||
} else {
|
||||
this.removeNode(node);
|
||||
}
|
||||
},
|
||||
update: node => {
|
||||
if (node.item.state.runState !== node.previousState && node.node) {
|
||||
if (isRunningState(node.item.state.runState)) {
|
||||
node.node.computedState = node.item.state.runState;
|
||||
} else {
|
||||
this.removeNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
node.previousState = node.item.state.runState;
|
||||
this.resolveNodesRecursive(node);
|
||||
|
||||
const locationChanged = !locationsEqual(node.location, node.item.location);
|
||||
if (locationChanged) {
|
||||
this.locations.remove(node);
|
||||
node.location = node.item.location;
|
||||
this.locations.add(node);
|
||||
}
|
||||
|
||||
if (node.node) {
|
||||
this.changes.updated(node.node);
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this.updateEmitter.fire();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures tree nodes for the item state are present in the tree.
|
||||
*/
|
||||
protected resolveNodesRecursive(item: IStatusListTestItem) {
|
||||
const newType = Iterable.some(item.children, c => this.items.get(c)?.type !== ListElementType.BranchWithoutLeaf)
|
||||
? ListElementType.BranchWithLeaf
|
||||
: item.item.runnable
|
||||
? ListElementType.TestLeaf
|
||||
: ListElementType.BranchWithoutLeaf;
|
||||
|
||||
if (newType === item.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isVisible = newType === ListElementType.TestLeaf;
|
||||
const wasVisible = item.type === ListElementType.TestLeaf;
|
||||
item.type = newType;
|
||||
|
||||
if (!isVisible && wasVisible && item.node) {
|
||||
this.removeNodeSingle(item);
|
||||
} else if (isVisible && !wasVisible) {
|
||||
const state = item.item.state.runState;
|
||||
item.node = item.node || new ListTestStateElement(item, this.getOrCreateStateElement(state));
|
||||
this.changes.added(item.node);
|
||||
}
|
||||
|
||||
if (item.parentItem) {
|
||||
this.resolveNodesRecursive(item.parentItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively (from the leaf to the root) removes tree elements if there's
|
||||
* no children who have the given state left.
|
||||
*
|
||||
* Returns true if it resulted in a node being removed.
|
||||
*/
|
||||
private removeNode(item: IStatusListTestItem) {
|
||||
if (!item.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeNodeSingle(item);
|
||||
|
||||
if (item.parentItem) {
|
||||
this.resolveNodesRecursive(item.parentItem);
|
||||
}
|
||||
}
|
||||
|
||||
private removeNodeSingle(item: IStatusListTestItem) {
|
||||
if (!item.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.node.remove();
|
||||
this.changes.removed(item.node);
|
||||
|
||||
const parent = item.node.parentItem;
|
||||
item.node = undefined;
|
||||
item.type = ListElementType.Unset;
|
||||
|
||||
if (parent.children.size === 0) {
|
||||
this.changes.removed(parent);
|
||||
this.stateRoots.delete(parent.state);
|
||||
}
|
||||
}
|
||||
|
||||
private getOrCreateStateElement(state: TestRunState) {
|
||||
let s = this.stateRoots.get(state);
|
||||
if (!s) {
|
||||
s = new StateElement(state);
|
||||
this.changes.added(s);
|
||||
this.stateRoots.set(state, s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
protected createItem(item: InternalTestItem, parentItem?: IStatusListTestItem): IStatusListTestItem {
|
||||
return {
|
||||
...item,
|
||||
type: ListElementType.Unset,
|
||||
depth: parentItem ? parentItem.depth + 1 : 0,
|
||||
parentItem: parentItem,
|
||||
previousState: item.item.state.runState,
|
||||
location: item.item.location,
|
||||
children: new Set(),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
import { testStateNames } from 'vs/workbench/contrib/testing/common/constants';
|
||||
|
||||
/**
|
||||
* Base state node element, used in both name and location grouping.
|
||||
*/
|
||||
export class StateElement<T extends ITestTreeElement> implements ITestTreeElement {
|
||||
public computedState = this.state;
|
||||
|
||||
public get treeId() {
|
||||
return `sestate:${this.state}`;
|
||||
}
|
||||
|
||||
public readonly depth = 0;
|
||||
public readonly label = testStateNames[this.state];
|
||||
public readonly parentItem = null;
|
||||
public readonly children = new Set<T>();
|
||||
|
||||
public get runnable() {
|
||||
return Iterable.concatNested(Iterable.map(this.children, c => c.runnable));
|
||||
}
|
||||
|
||||
public get debuggable() {
|
||||
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
|
||||
}
|
||||
|
||||
constructor(public readonly state: TestRunState) { }
|
||||
}
|
|
@ -20,10 +20,10 @@ export const testingShowAsList = registerIcon('testing-show-as-list-icon', Codic
|
|||
export const testingShowAsTree = registerIcon('testing-show-as-list-icon', Codicon.listFlat, localize('testingShowAsTree', 'Icon shown when the test explorer is disabled as a list.'));
|
||||
|
||||
export const testingStatesToIcons = new Map<TestRunState, ThemeIcon>([
|
||||
[TestRunState.Errored, registerIcon('testing-error-icon', Codicon.warning, localize('testingErrorIcon', 'Icon shown for tests that have an error.'))],
|
||||
[TestRunState.Failed, registerIcon('testing-failed-icon', Codicon.close, localize('testingFailedIcon', 'Icon shown for tests that failed.'))],
|
||||
[TestRunState.Errored, registerIcon('testing-error-icon', Codicon.issues, localize('testingErrorIcon', 'Icon shown for tests that have an error.'))],
|
||||
[TestRunState.Failed, registerIcon('testing-failed-icon', Codicon.error, localize('testingFailedIcon', 'Icon shown for tests that failed.'))],
|
||||
[TestRunState.Passed, registerIcon('testing-passed-icon', Codicon.pass, localize('testingPassedIcon', 'Icon shown for tests that passed.'))],
|
||||
[TestRunState.Queued, registerIcon('testing-queued-icon', Codicon.watch, localize('testingQueuedIcon', 'Icon shown for tests that are queued.'))],
|
||||
[TestRunState.Queued, registerIcon('testing-queued-icon', Codicon.history, localize('testingQueuedIcon', 'Icon shown for tests that are queued.'))],
|
||||
[TestRunState.Running, ThemeIcon.modify(Codicon.loading, 'spin')],
|
||||
[TestRunState.Skipped, registerIcon('testing-skipped-icon', Codicon.debugStepOver, localize('testingSkippedIcon', 'Icon shown for tests that are skipped.'))],
|
||||
[TestRunState.Unset, registerIcon('testing-unset-icon', Codicon.circleOutline, localize('testingUnsetIcon', 'Icon shown for tests that are in an unset state.'))],
|
||||
|
|
|
@ -21,9 +21,10 @@ import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
|
|||
import { FocusedViewContext } from 'vs/workbench/common/views';
|
||||
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
|
||||
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
||||
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestExplorerViewSorting, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { ITestResult, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
|
||||
|
@ -101,10 +102,10 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
|
|||
/**
|
||||
* @override
|
||||
*/
|
||||
public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise<RunTestsResult> {
|
||||
public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise<ITestResult | undefined> {
|
||||
const tests = this.getActionableTests(accessor.get(IWorkspaceTestCollectionService), view.viewModel);
|
||||
if (!tests.length) {
|
||||
return Promise.resolve(EMPTY_TEST_RESULT);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return accessor.get(ITestService).runTests({ tests, debug: this.debug });
|
||||
|
@ -327,18 +328,18 @@ export class TestingViewAsTreeAction extends ViewAction<TestingExplorerView> {
|
|||
}
|
||||
|
||||
|
||||
export class TestingGroupByLocationAction extends ViewAction<TestingExplorerView> {
|
||||
export class TestingSortByNameAction extends ViewAction<TestingExplorerView> {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'testing.groupByLocation',
|
||||
id: 'testing.sortByName',
|
||||
viewId: Testing.ExplorerViewId,
|
||||
title: localize('testing.groupByLocation', "Sort by Name"),
|
||||
title: localize('testing.sortByName', "Sort by Name"),
|
||||
f1: false,
|
||||
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByLocation),
|
||||
toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByName),
|
||||
menu: {
|
||||
id: MenuId.ViewTitle,
|
||||
order: 10,
|
||||
group: 'groupBy',
|
||||
group: 'sortBy',
|
||||
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
|
||||
}
|
||||
});
|
||||
|
@ -348,22 +349,22 @@ export class TestingGroupByLocationAction extends ViewAction<TestingExplorerView
|
|||
* @override
|
||||
*/
|
||||
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
|
||||
view.viewModel.viewGrouping = TestExplorerViewGrouping.ByLocation;
|
||||
view.viewModel.viewSorting = TestExplorerViewSorting.ByName;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestingGroupByStatusAction extends ViewAction<TestingExplorerView> {
|
||||
export class TestingSortByLocationAction extends ViewAction<TestingExplorerView> {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'testing.groupByStatus',
|
||||
id: 'testing.sortByLocation',
|
||||
viewId: Testing.ExplorerViewId,
|
||||
title: localize('testing.groupByStatus', "Sort by Status"),
|
||||
title: localize('testing.sortByLocation', "Sort by Location"),
|
||||
f1: false,
|
||||
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByStatus),
|
||||
toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByLocation),
|
||||
menu: {
|
||||
id: MenuId.ViewTitle,
|
||||
order: 10,
|
||||
group: 'groupBy',
|
||||
group: 'sortBy',
|
||||
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
|
||||
}
|
||||
});
|
||||
|
@ -373,7 +374,7 @@ export class TestingGroupByStatusAction extends ViewAction<TestingExplorerView>
|
|||
* @override
|
||||
*/
|
||||
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
|
||||
view.viewModel.viewGrouping = TestExplorerViewGrouping.ByStatus;
|
||||
view.viewModel.viewSorting = TestExplorerViewSorting.ByLocation;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -427,6 +428,24 @@ export class RefreshTestsAction extends Action2 {
|
|||
}
|
||||
}
|
||||
|
||||
export class ClearTestResultsAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'testing.clearTestResults',
|
||||
title: localize('testing.clearResults', "Clear All Results"),
|
||||
category,
|
||||
f1: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public run(accessor: ServicesAccessor) {
|
||||
accessor.get(ITestResultService).clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditFocusedTest extends ViewAction<TestingExplorerView> {
|
||||
constructor() {
|
||||
super({
|
||||
|
|
|
@ -83,13 +83,14 @@ registerAction2(Action.TestingViewAsTreeAction);
|
|||
registerAction2(Action.CancelTestRunAction);
|
||||
registerAction2(Action.RunSelectedAction);
|
||||
registerAction2(Action.DebugSelectedAction);
|
||||
registerAction2(Action.TestingGroupByLocationAction);
|
||||
registerAction2(Action.TestingGroupByStatusAction);
|
||||
registerAction2(Action.TestingSortByNameAction);
|
||||
registerAction2(Action.TestingSortByLocationAction);
|
||||
registerAction2(Action.RefreshTestsAction);
|
||||
registerAction2(Action.CollapseAllAction);
|
||||
registerAction2(Action.RunAllAction);
|
||||
registerAction2(Action.DebugAllAction);
|
||||
registerAction2(Action.EditFocusedTest);
|
||||
registerAction2(Action.ClearTestResultsAction);
|
||||
registerAction2(CloseTestPeek);
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
|
||||
|
|
|
@ -28,8 +28,8 @@ import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/work
|
|||
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
||||
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
|
||||
import { IncrementalTestCollectionItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
|
||||
export class TestingDecorations extends Disposable implements IEditorContribution {
|
||||
|
@ -39,6 +39,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio
|
|||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@ITestResultService private readonly results: ITestResultService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
@ -62,6 +63,15 @@ export class TestingDecorations extends Disposable implements IEditorContributio
|
|||
}
|
||||
|
||||
this.collection.value = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri, () => this.setDecorations(uri));
|
||||
this._register(this.results.onTestChanged(([, changed]) => {
|
||||
if (changed.item.location?.uri.toString() === uri.toString()) {
|
||||
this.setDecorations(uri);
|
||||
}
|
||||
}));
|
||||
this._register(this.results.onResultsChanged(() => {
|
||||
this.setDecorations(uri);
|
||||
}));
|
||||
|
||||
this.setDecorations(uri);
|
||||
}
|
||||
|
||||
|
@ -74,19 +84,25 @@ export class TestingDecorations extends Disposable implements IEditorContributio
|
|||
this.editor.changeDecorations(accessor => {
|
||||
const newDecorations: ITestDecoration[] = [];
|
||||
for (const test of ref.object.all) {
|
||||
const stateLookup = this.results.getStateByExtId(test.item.extId);
|
||||
if (hasValidLocation(uri, test.item)) {
|
||||
newDecorations.push(this.instantiationService.createInstance(
|
||||
RunTestDecoration, test, ref.object, test.item.location, this.editor));
|
||||
RunTestDecoration, test, ref.object, test.item.location, this.editor, stateLookup?.[1].computedState));
|
||||
}
|
||||
|
||||
for (let i = 0; i < test.item.state.messages.length; i++) {
|
||||
const m = test.item.state.messages[i];
|
||||
if (!stateLookup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [result, stateItem] = stateLookup;
|
||||
for (let i = 0; i < stateItem.state.messages.length; i++) {
|
||||
const m = stateItem.state.messages[i];
|
||||
if (hasValidLocation(uri, m)) {
|
||||
const uri = buildTestUri({
|
||||
type: TestUriType.LiveMessage,
|
||||
type: TestUriType.ResultActualOutput,
|
||||
messageIndex: i,
|
||||
providerId: test.providerId,
|
||||
testId: test.id,
|
||||
resultId: result.id,
|
||||
testId: stateItem.item.extId,
|
||||
});
|
||||
|
||||
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
|
||||
|
@ -138,7 +154,7 @@ const firstLineRange = (originalRange: IRange) => ({
|
|||
endColumn: 1,
|
||||
});
|
||||
|
||||
class RunTestDecoration implements ITestDecoration {
|
||||
class RunTestDecoration extends Disposable implements ITestDecoration {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -156,25 +172,16 @@ class RunTestDecoration implements ITestDecoration {
|
|||
private readonly collection: IMainThreadTestCollection,
|
||||
private readonly location: ModeLocation,
|
||||
private readonly editor: ICodeEditor,
|
||||
computedState: TestRunState | undefined,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super();
|
||||
this.line = location.range.startLineNumber;
|
||||
|
||||
const queue = [test.children];
|
||||
let state = this.test.item.state.runState;
|
||||
while (queue.length) {
|
||||
for (const child of queue.pop()!) {
|
||||
const node = collection.getNodeById(child);
|
||||
if (node) {
|
||||
state = maxPriority(node.item.state.runState, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const icon = state !== TestRunState.Unset
|
||||
? testingStatesToIcons.get(state)!
|
||||
const icon = computedState !== undefined && computedState !== TestRunState.Unset
|
||||
? testingStatesToIcons.get(computedState)!
|
||||
: test.children.size > 0 ? testingRunAllIcon : testingRunIcon;
|
||||
|
||||
this.editorDecoration = {
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import * as aria from 'vs/base/browser/ui/aria/aria';
|
||||
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
||||
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
|
||||
import * as aria from 'vs/base/browser/ui/aria/aria';
|
||||
import { Action, IAction, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { DeferredPromise } from 'vs/base/common/async';
|
||||
import { Color, RGBA } from 'vs/base/common/color';
|
||||
|
@ -47,14 +47,10 @@ import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/comm
|
|||
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation';
|
||||
import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName';
|
||||
import { getComputedState } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
|
||||
import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation';
|
||||
import { StateByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByName';
|
||||
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
|
||||
import { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
|
||||
import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
|
||||
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
||||
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
|
@ -188,7 +184,7 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
public projection!: ITestTreeProjection;
|
||||
|
||||
private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService);
|
||||
private readonly _viewGrouping = TestingContextKeys.viewGrouping.bindTo(this.contextKeyService);
|
||||
private readonly _viewSorting = TestingContextKeys.viewSorting.bindTo(this.contextKeyService);
|
||||
|
||||
/**
|
||||
* Fires when the selected tests change.
|
||||
|
@ -210,18 +206,18 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
}
|
||||
|
||||
|
||||
public get viewGrouping() {
|
||||
return this._viewGrouping.get() ?? TestExplorerViewGrouping.ByLocation;
|
||||
public get viewSorting() {
|
||||
return this._viewSorting.get() ?? TestExplorerViewSorting.ByLocation;
|
||||
}
|
||||
|
||||
public set viewGrouping(newGrouping: TestExplorerViewGrouping) {
|
||||
if (newGrouping === this._viewGrouping.get()) {
|
||||
public set viewSorting(newSorting: TestExplorerViewSorting) {
|
||||
if (newSorting === this._viewSorting.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._viewGrouping.set(newGrouping);
|
||||
this.updatePreferredProjection();
|
||||
this.storageService.store('testing.viewGrouping', newGrouping, StorageScope.WORKSPACE, StorageTarget.USER);
|
||||
this._viewSorting.set(newSorting);
|
||||
this.tree.resort(null);
|
||||
this.storageService.store('testing.viewSorting', newSorting, StorageScope.WORKSPACE, StorageTarget.USER);
|
||||
}
|
||||
|
||||
constructor(
|
||||
|
@ -229,16 +225,17 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
onDidChangeVisibility: Event<boolean>,
|
||||
private listener: TestSubscriptionListener | undefined,
|
||||
@ITestExplorerFilterState filterState: TestExplorerFilterState,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@ITestResultService private readonly testResults: ITestResultService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._viewMode.set(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, TestExplorerViewMode.Tree) as TestExplorerViewMode);
|
||||
this._viewGrouping.set(this.storageService.get('testing.viewGrouping', StorageScope.WORKSPACE, TestExplorerViewGrouping.ByLocation) as TestExplorerViewGrouping);
|
||||
this._viewSorting.set(this.storageService.get('testing.viewSorting', StorageScope.WORKSPACE, TestExplorerViewSorting.ByLocation) as TestExplorerViewSorting);
|
||||
|
||||
const labels = this._register(instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: onDidChangeVisibility }));
|
||||
|
||||
|
@ -261,7 +258,7 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
simpleKeyboardNavigation: true,
|
||||
identityProvider: instantiationService.createInstance(IdentityProvider),
|
||||
hideTwistiesOfChildlessElements: true,
|
||||
sorter: instantiationService.createInstance(TreeSorter),
|
||||
sorter: instantiationService.createInstance(TreeSorter, this),
|
||||
keyboardNavigationLabelProvider: instantiationService.createInstance(TreeKeyboardNavigationLabelProvider),
|
||||
accessibilityProvider: instantiationService.createInstance(ListAccessibilityProvider),
|
||||
filter: this.filter,
|
||||
|
@ -293,6 +290,10 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
tracker.deactivate();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(testResults.onResultsChanged(() => {
|
||||
this.tree.resort(null);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -380,16 +381,18 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
* Tries to peek the first test error, if the item is in a failed state.
|
||||
*/
|
||||
private async tryPeekError(item: ITestTreeElement) {
|
||||
if (!item.test || !isFailedState(item.test.item.state.runState)) {
|
||||
const lookup = item.test && this.testResults.getStateByExtId(item.test.item.extId);
|
||||
if (!lookup || !isFailedState(lookup[1].state.state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const index = item.test.item.state.messages.findIndex(m => !!m.location);
|
||||
const [result, test] = lookup;
|
||||
const index = test.state.messages.findIndex(m => !!m.location);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = item.test.item.state.messages[index];
|
||||
const message = test.state.messages[index];
|
||||
const pane = await this.editorService.openEditor({
|
||||
resource: message.location!.uri,
|
||||
options: { selection: message.location!.range, preserveFocus: true }
|
||||
|
@ -401,10 +404,10 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
}
|
||||
|
||||
TestingOutputPeekController.get(control).show(buildTestUri({
|
||||
type: TestUriType.LiveMessage,
|
||||
type: TestUriType.ResultMessage,
|
||||
messageIndex: index,
|
||||
providerId: item.test.providerId,
|
||||
testId: item.test.id,
|
||||
resultId: result.id,
|
||||
testId: item.test!.item.extId,
|
||||
}));
|
||||
|
||||
return true;
|
||||
|
@ -417,18 +420,10 @@ export class TestingExplorerViewModel extends Disposable {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this._viewGrouping.get() === TestExplorerViewGrouping.ByLocation) {
|
||||
if (this._viewMode.get() === TestExplorerViewMode.List) {
|
||||
this.projection = new HierarchicalByNameProjection(this.listener);
|
||||
} else {
|
||||
this.projection = new HierarchicalByLocationProjection(this.listener);
|
||||
}
|
||||
if (this._viewMode.get() === TestExplorerViewMode.List) {
|
||||
this.projection = this.instantiationService.createInstance(HierarchicalByNameProjection, this.listener);
|
||||
} else {
|
||||
if (this._viewMode.get() === TestExplorerViewMode.List) {
|
||||
this.projection = new StateByNameProjection(this.listener);
|
||||
} else {
|
||||
this.projection = new StateByLocationProjection(this.listener);
|
||||
}
|
||||
this.projection = this.instantiationService.createInstance(HierarchicalByLocationProjection, this.listener);
|
||||
}
|
||||
|
||||
this.projection.onUpdate(this.deferUpdate, this);
|
||||
|
@ -569,9 +564,19 @@ class TestsFilter implements ITreeFilter<ITestTreeElement> {
|
|||
}
|
||||
|
||||
class TreeSorter implements ITreeSorter<ITestTreeElement> {
|
||||
constructor(private readonly viewModel: TestingExplorerViewModel) { }
|
||||
|
||||
public compare(a: ITestTreeElement, b: ITestTreeElement): number {
|
||||
if (a instanceof StateElement && b instanceof StateElement) {
|
||||
return cmpPriority(a.computedState, b.computedState);
|
||||
let delta = cmpPriority(a.state, b.state);
|
||||
if (delta !== 0) {
|
||||
return delta;
|
||||
}
|
||||
|
||||
if (this.viewModel.viewSorting === TestExplorerViewSorting.ByLocation && a.location && b.location && a.location.uri.toString() === b.location.uri.toString()) {
|
||||
delta = a.location.range.startLineNumber - b.location.range.startLineNumber;
|
||||
if (delta !== 0) {
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
return a.label.localeCompare(b.label);
|
||||
|
@ -587,7 +592,7 @@ class ListAccessibilityProvider implements IListAccessibilityProvider<ITestTreeE
|
|||
return localize({
|
||||
key: 'testing.treeElementLabel',
|
||||
comment: ['label then the unit tests state, for example "Addition Tests (Running)"'],
|
||||
}, '{0} ({1})', element.label, testStateNames[getComputedState(element)]);
|
||||
}, '{0} ({1})', element.label, testStateNames[element.state]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -662,8 +667,7 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
|
|||
const options: IResourceLabelOptions = {};
|
||||
data.actionBar.clear();
|
||||
|
||||
const state = getComputedState(element);
|
||||
const icon = testingStatesToIcons.get(state);
|
||||
const icon = testingStatesToIcons.get(element.state);
|
||||
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
|
||||
const test = element.test;
|
||||
if (test) {
|
||||
|
@ -683,7 +687,7 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
|
|||
options.fileKind = FileKind.ROOT_FOLDER;
|
||||
}
|
||||
|
||||
const running = state === TestRunState.Running;
|
||||
const running = element.state === TestRunState.Running;
|
||||
if (!Iterable.isEmpty(element.runnable)) {
|
||||
data.actionBar.push(
|
||||
this.instantiationService.createInstance(RunAction, element.runnable, running),
|
||||
|
@ -740,12 +744,16 @@ const getProgressText = ({ passed, runSoFar, skipped, failed }: CountSummary) =>
|
|||
class TestRunProgress {
|
||||
private current?: { update: IProgress<IProgressStep>; deferred: DeferredPromise<void> };
|
||||
private badge = new MutableDisposable();
|
||||
private readonly resultLister = this.resultService.onNewTestResult(result => {
|
||||
private readonly resultLister = this.resultService.onResultsChanged(result => {
|
||||
if (!('started' in result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateProgress();
|
||||
this.updateBadge();
|
||||
|
||||
result.onChange(this.throttledProgressUpdate, this);
|
||||
result.onComplete(() => {
|
||||
result.started.onChange(this.throttledProgressUpdate, this);
|
||||
result.started.onComplete(() => {
|
||||
this.throttledProgressUpdate();
|
||||
this.updateBadge();
|
||||
});
|
||||
|
|
|
@ -27,15 +27,15 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic
|
|||
import { EditorModel } from 'vs/workbench/common/editor';
|
||||
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
|
||||
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { InternalTestItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ITestItem, ITestMessage, ITestState } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
|
||||
interface ITestDto {
|
||||
test: ITestItem,
|
||||
messageIndex: number;
|
||||
test: InternalTestItem;
|
||||
state: ITestState;
|
||||
expectedUri: URI;
|
||||
actualUri: URI;
|
||||
messageUri: URI;
|
||||
|
@ -66,7 +66,6 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
|
|||
private readonly editor: ICodeEditor,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ITestResultService private readonly testResults: ITestResultService,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super();
|
||||
|
@ -83,7 +82,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
|
|||
return;
|
||||
}
|
||||
|
||||
const message = dto.test.item.state.messages[dto.messageIndex];
|
||||
const message = dto.state.messages[dto.messageIndex];
|
||||
if (!message?.location) {
|
||||
return;
|
||||
}
|
||||
|
@ -120,28 +119,14 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if ('resultId' in parts) {
|
||||
const test = this.testResults.lookup(parts.resultId)?.tests.find(t => t.id === parts.testId);
|
||||
return test && {
|
||||
test,
|
||||
messageIndex: parts.messageIndex,
|
||||
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
|
||||
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
|
||||
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
|
||||
};
|
||||
}
|
||||
|
||||
const test = await this.testService.lookupTest({ providerId: parts.providerId, testId: parts.testId });
|
||||
if (!test) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
test,
|
||||
const test = this.testResults.getResult(parts.resultId)?.getStateByExtId(parts.testId);
|
||||
return test && {
|
||||
test: test.item,
|
||||
state: test.state,
|
||||
messageIndex: parts.messageIndex,
|
||||
expectedUri: buildTestUri({ ...parts, type: TestUriType.LiveActualOutput }),
|
||||
actualUri: buildTestUri({ ...parts, type: TestUriType.LiveExpectedOutput }),
|
||||
messageUri: buildTestUri({ ...parts, type: TestUriType.LiveMessage }),
|
||||
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
|
||||
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
|
||||
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -236,14 +221,14 @@ class TestingDiffOutputPeek extends TestingOutputPeek {
|
|||
/**
|
||||
* @override
|
||||
*/
|
||||
public async setModel({ test, messageIndex, expectedUri, actualUri }: ITestDto) {
|
||||
const message = test.item.state.messages[messageIndex];
|
||||
public async setModel({ test, state, messageIndex, expectedUri, actualUri }: ITestDto) {
|
||||
const message = state.messages[messageIndex];
|
||||
if (!message?.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.show(message.location.range, hintDiffPeekHeight(message));
|
||||
this.setTitle(message.message.toString(), test.item.label);
|
||||
this.setTitle(message.message.toString(), test.label);
|
||||
|
||||
const [original, modified] = await Promise.all([
|
||||
this.modelService.createModelReference(expectedUri),
|
||||
|
@ -285,14 +270,14 @@ class TestingMessageOutputPeek extends TestingOutputPeek {
|
|||
/**
|
||||
* @override
|
||||
*/
|
||||
public async setModel({ test, messageIndex, messageUri }: ITestDto) {
|
||||
const message = test.item.state.messages[messageIndex];
|
||||
public async setModel({ state, test, messageIndex, messageUri }: ITestDto) {
|
||||
const message = state.messages[messageIndex];
|
||||
if (!message?.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.show(message.location.range, hintPeekStrHeight(message.message.toString()));
|
||||
this.setTitle(message.message.toString(), test.item.label);
|
||||
this.setTitle(message.message.toString(), test.label);
|
||||
|
||||
const modelRef = this.model.value = await this.modelService.createModelReference(messageUri);
|
||||
if (this.preview.value) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue