mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-13 23:44:09 +01:00
Merge branch 'main' into joh/nbSerialize
This commit is contained in:
@@ -19,7 +19,7 @@ parameters:
|
||||
- name: ENABLE_TERRAPIN
|
||||
displayName: "Enable Terrapin"
|
||||
type: boolean
|
||||
default: false
|
||||
default: true
|
||||
- name: VSCODE_BUILD_WIN32
|
||||
displayName: "🎯 Windows x64"
|
||||
type: boolean
|
||||
|
||||
@@ -199,7 +199,38 @@ exports.watchExtensionsTask = watchExtensionsTask;
|
||||
const compileExtensionsBuildLegacyTask = task.define('compile-extensions-build-legacy', task.parallel(...tasks.map(t => t.compileBuildTask)));
|
||||
gulp.task(compileExtensionsBuildLegacyTask);
|
||||
|
||||
// Azure Pipelines
|
||||
//#region Extension media
|
||||
|
||||
// Additional projects to webpack. These typically build code for webviews
|
||||
const mediaCompilations = [
|
||||
'markdown-language-features/webpack.config.js',
|
||||
'markdown-language-features/webpack.notebook.js',
|
||||
'notebook-markdown-extensions/webpack.notebook.js',
|
||||
];
|
||||
|
||||
const compileExtensionMediaTask = task.define('compile-extension-media', () => buildExtensionMedia(false));
|
||||
gulp.task(compileExtensionMediaTask);
|
||||
exports.compileExtensionMediaTask = compileExtensionMediaTask;
|
||||
|
||||
const watchExtensionMedia = task.define('watch-extension-media', () => buildExtensionMedia(true));
|
||||
gulp.task(watchExtensionMedia);
|
||||
exports.watchExtensionMedia = watchExtensionMedia;
|
||||
|
||||
function buildExtensionMedia(isWatch, outputRoot) {
|
||||
const webpackConfigLocations = mediaCompilations.map(p => {
|
||||
return {
|
||||
configPath: path.join(extensionsPath, p),
|
||||
outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined
|
||||
};
|
||||
});
|
||||
return webpackExtensions('packaging extension media', isWatch, webpackConfigLocations);
|
||||
}
|
||||
const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => buildExtensionMedia(false, '.build/extensions'));
|
||||
gulp.task(compileExtensionMediaBuildTask);
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Azure Pipelines
|
||||
|
||||
const cleanExtensionsBuildTask = task.define('clean-extensions-build', util.rimraf('.build/extensions'));
|
||||
const compileExtensionsBuildTask = task.define('compile-extensions-build', task.series(
|
||||
@@ -209,10 +240,12 @@ const compileExtensionsBuildTask = task.define('compile-extensions-build', task.
|
||||
));
|
||||
|
||||
gulp.task(compileExtensionsBuildTask);
|
||||
gulp.task(task.define('extensions-ci', task.series(compileExtensionsBuildTask)));
|
||||
gulp.task(task.define('extensions-ci', task.series(compileExtensionsBuildTask, compileExtensionMediaBuildTask)));
|
||||
|
||||
exports.compileExtensionsBuildTask = compileExtensionsBuildTask;
|
||||
|
||||
//#endregion
|
||||
|
||||
const compileWebExtensionsTask = task.define('compile-web', () => buildWebExtensions(false));
|
||||
gulp.task(compileWebExtensionsTask);
|
||||
exports.compileWebExtensionsTask = compileWebExtensionsTask;
|
||||
@@ -222,23 +255,39 @@ gulp.task(watchWebExtensionsTask);
|
||||
exports.watchWebExtensionsTask = watchWebExtensionsTask;
|
||||
|
||||
async function buildWebExtensions(isWatch) {
|
||||
const webpack = require('webpack');
|
||||
|
||||
const webpackConfigLocations = await nodeUtil.promisify(glob)(
|
||||
path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'),
|
||||
{ ignore: ['**/node_modules'] }
|
||||
);
|
||||
return webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath })));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} taskName
|
||||
* @param {boolean} isWatch
|
||||
* @param {{ configPath: string, outputRoot?: boolean}} webpackConfigLocations
|
||||
*/
|
||||
async function webpackExtensions(taskName, isWatch, webpackConfigLocations) {
|
||||
const webpack = require('webpack');
|
||||
|
||||
const webpackConfigs = [];
|
||||
|
||||
for (const webpackConfigPath of webpackConfigLocations) {
|
||||
const configOrFnOrArray = require(webpackConfigPath);
|
||||
for (const { configPath, outputRoot } of webpackConfigLocations) {
|
||||
const configOrFnOrArray = require(configPath);
|
||||
function addConfig(configOrFn) {
|
||||
let config;
|
||||
if (typeof configOrFn === 'function') {
|
||||
webpackConfigs.push(configOrFn({}, {}));
|
||||
config = configOrFn({}, {});
|
||||
webpackConfigs.push(config);
|
||||
} else {
|
||||
webpackConfigs.push(configOrFn);
|
||||
config = configOrFn;
|
||||
}
|
||||
|
||||
if (outputRoot) {
|
||||
config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path));
|
||||
}
|
||||
|
||||
webpackConfigs.push(configOrFn);
|
||||
}
|
||||
addConfig(configOrFnOrArray);
|
||||
}
|
||||
@@ -249,7 +298,7 @@ async function buildWebExtensions(isWatch) {
|
||||
if (outputPath) {
|
||||
const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/');
|
||||
const match = relativePath.match(/[^\/]+(\/server|\/client)?/);
|
||||
fancyLog(`Finished ${ansiColors.green('packaging web extension')} ${ansiColors.cyan(match[0])} with ${stats.errors.length} errors.`);
|
||||
fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match[0])} with ${stats.errors.length} errors.`);
|
||||
}
|
||||
if (Array.isArray(stats.errors)) {
|
||||
stats.errors.forEach(error => {
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ const util = require('./lib/util');
|
||||
const task = require('./lib/task');
|
||||
const compilation = require('./lib/compilation');
|
||||
const { monacoTypecheckTask/* , monacoTypecheckWatchTask */ } = require('./gulpfile.editor');
|
||||
const { compileExtensionsTask, watchExtensionsTask } = require('./gulpfile.extensions');
|
||||
const { compileExtensionsTask, watchExtensionsTask, compileExtensionMediaTask } = require('./gulpfile.extensions');
|
||||
|
||||
// Fast compile for development time
|
||||
const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.compileTask('src', 'out', false)));
|
||||
@@ -23,7 +23,7 @@ const watchClientTask = task.define('watch-client', task.series(util.rimraf('out
|
||||
gulp.task(watchClientTask);
|
||||
|
||||
// All
|
||||
const compileTask = task.define('compile', task.parallel(monacoTypecheckTask, compileClientTask, compileExtensionsTask));
|
||||
const compileTask = task.define('compile', task.parallel(monacoTypecheckTask, compileClientTask, compileExtensionsTask, compileExtensionMediaTask));
|
||||
gulp.task(compileTask);
|
||||
|
||||
gulp.task(task.define('watch', task.parallel(/* monacoTypecheckWatchTask, */ watchClientTask, watchExtensionsTask)));
|
||||
|
||||
@@ -3,35 +3,91 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { PushErrorHandler, GitErrorCodes, Repository, Remote } from './typings/git';
|
||||
import { window, ProgressLocation, commands, Uri } from 'vscode';
|
||||
import { commands, env, ProgressLocation, UIKind, Uri, window } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { getOctokit } from './auth';
|
||||
import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
|
||||
|
||||
export function isInCodespaces(): boolean {
|
||||
return env.remoteName === 'codespaces';
|
||||
}
|
||||
|
||||
async function handlePushError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
|
||||
const inCodespaces = isInCodespaces();
|
||||
let codespace: string | undefined;
|
||||
if (inCodespaces) {
|
||||
if (env.uiKind === UIKind.Web) {
|
||||
// TODO@eamodio Find a better way to get the codespace id
|
||||
// HACK to get the codespace id
|
||||
try {
|
||||
const codespaceUrl = (await env.asExternalUri(Uri.parse(`${env.uriScheme}://codespace/`))).authority;
|
||||
if (codespaceUrl.endsWith('.github.dev')) {
|
||||
codespace = codespaceUrl.slice(0, -11);
|
||||
} else {
|
||||
[codespace] = codespaceUrl.split('.');
|
||||
}
|
||||
} catch { }
|
||||
} else {
|
||||
// TODO@eamodio Figure out how to get the codespace id when on the desktop
|
||||
}
|
||||
|
||||
if (!codespace) {
|
||||
const ok = localize('ok', "OK");
|
||||
await window.showErrorMessage(localize('fork unable', "You don't have permissions to push to '{0}/{1}' on GitHub.", owner, repo), ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const yes = localize('create a fork', "Create Fork");
|
||||
const no = localize('no', "No");
|
||||
|
||||
const answer = await window.showInformationMessage(localize('fork', "You don't have permissions to push to '{0}/{1}' on GitHub. Would you like to create a fork and push to it instead?", owner, repo), yes, no);
|
||||
|
||||
if (answer === no) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /^([^:]*):([^:]*)$/.exec(refspec);
|
||||
const localName = match ? match[1] : refspec;
|
||||
const remoteName = match ? match[2] : refspec;
|
||||
let remoteName = match ? match[2] : refspec;
|
||||
|
||||
const [octokit, ghRepository] = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: localize('create fork', 'Create GitHub fork') }, async progress => {
|
||||
progress.report({ message: localize('forking', "Forking '{0}/{1}'...", owner, repo), increment: 33 });
|
||||
|
||||
const octokit = await getOctokit();
|
||||
|
||||
type CreateForkResponseData = Awaited<ReturnType<typeof octokit.repos.createFork>>['data'];
|
||||
|
||||
// Issue: what if the repo already exists?
|
||||
const res = await octokit.repos.createFork({ owner, repo });
|
||||
const ghRepository = res.data;
|
||||
let ghRepository: CreateForkResponseData;
|
||||
try {
|
||||
if (inCodespaces) {
|
||||
const userResp = await octokit.users.getAuthenticated();
|
||||
const user = userResp.data.login;
|
||||
|
||||
const resp = await octokit.request<{ repository: CreateForkResponseData, ref: string }>({ method: 'POST', url: `/vscs_internal/user/${user}/codespaces/${codespace}/fork_repo` });
|
||||
ghRepository = resp.data.repository;
|
||||
|
||||
if (resp.data.ref) {
|
||||
let ref = resp.data.ref;
|
||||
if (ref.startsWith('refs/heads/')) {
|
||||
ref = ref.substr(11);
|
||||
}
|
||||
|
||||
remoteName = ref;
|
||||
}
|
||||
} else {
|
||||
const resp = await octokit.repos.createFork({ owner, repo });
|
||||
ghRepository = resp.data;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
|
||||
progress.report({ message: localize('forking_pushing', "Pushing changes..."), increment: 33 });
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
|
||||
|
||||
languages.setLanguageConfiguration('html', {
|
||||
indentationRules: {
|
||||
increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
|
||||
increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|keygen|link|menuitem|meta|param|source|track|wbr)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
|
||||
decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/
|
||||
},
|
||||
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var s=t[o]={i:o,l:!1,exports:{}};return e[o].call(s.exports,s,s.exports,n),s.l=!0,s.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 s in e)n.d(o,s,function(t){return e[t]}.bind(null,s));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=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getSettings=t.getData=void 0;let o=void 0;function s(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error("Could not load data for "+e)}t.getData=s,t.getSettings=function(){if(o)return o;if(o=s("data-settings"),o)return o;throw new Error("Could not load settings")}},,,,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(12),s=n(14);window.cspAlerter=new o.CspAlerter,window.styleLoadingMonitor=new s.StyleLoadingMonitor},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CspAlerter=void 0;const o=n(0),s=n(13);t.CspAlerter=class{constructor(){this.didShow=!1,this.didHaveCspWarning=!1,document.addEventListener("securitypolicyviolation",()=>{this.onCspWarning()}),window.addEventListener("message",e=>{e&&e.data&&"vscode-did-block-svg"===e.data.name&&this.onCspWarning()})}setPoster(e){this.messaging=e,this.didHaveCspWarning&&this.showCspWarning()}onCspWarning(){this.didHaveCspWarning=!0,this.showCspWarning()}showCspWarning(){const e=s.getStrings(),t=o.getSettings();if(this.didShow||t.disableSecurityWarnings||!this.messaging)return;this.didShow=!0;const n=document.createElement("a");n.innerText=e.cspAlertMessageText,n.setAttribute("id","code-csp-warning"),n.setAttribute("title",e.cspAlertMessageTitle),n.setAttribute("role","button"),n.setAttribute("aria-label",e.cspAlertMessageLabel),n.onclick=()=>{this.messaging.postMessage("showPreviewSecuritySelector",{source:t.source})},document.body.appendChild(n)}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getStrings=void 0,t.getStrings=function(){const e=document.getElementById("vscode-markdown-preview-data");if(e){const t=e.getAttribute("data-strings");if(t)return JSON.parse(t)}throw new Error("Could not load strings")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.StyleLoadingMonitor=void 0;t.StyleLoadingMonitor=class{constructor(){this.unloadedStyles=[],this.finishedLoading=!1;const e=e=>{const t=e.target.dataset.source;this.unloadedStyles.push(t)};window.addEventListener("DOMContentLoaded",()=>{for(const t of document.getElementsByClassName("code-user-style"))t.dataset.source&&(t.onerror=e)}),window.addEventListener("load",()=>{this.unloadedStyles.length&&(this.finishedLoading=!0,this.poster&&this.poster.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles}))})}setPoster(e){this.poster=e,this.finishedLoading&&e.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles})}}}]);
|
||||
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@ const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
index: './preview-src/index.ts',
|
||||
pre: './preview-src/pre.ts'
|
||||
index: path.join(__dirname, 'preview-src', 'index.ts'),
|
||||
pre: path.join(__dirname, 'preview-src', 'pre.ts'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
index: './notebook/index.ts'
|
||||
index: path.join(__dirname, 'notebook', 'index.ts')
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
"strict": true,
|
||||
"target": "es2019"
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"scripts": {
|
||||
"compile": "npm run build-notebook",
|
||||
"watch": "npm run build-notebook",
|
||||
"build-notebook": "npx webpack-cli --config webpack.notebook.js --mode production",
|
||||
"postinstall": "npm run build-notebook"
|
||||
"build-notebook": "npx webpack-cli --config webpack.notebook.js --mode production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iktakahiro/markdown-it-katex": "^4.0.1",
|
||||
|
||||
@@ -6,6 +6,13 @@ const path = require('path');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
context: path.resolve(__dirname),
|
||||
mode: 'production',
|
||||
performance: {
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000
|
||||
},
|
||||
entry: {
|
||||
katex: './notebook/katex.ts',
|
||||
emoji: './notebook/emoji.ts',
|
||||
|
||||
+2
-1
@@ -14,7 +14,7 @@
|
||||
"preinstall": "node build/npm/preinstall.js",
|
||||
"postinstall": "node build/npm/postinstall.js",
|
||||
"compile": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile",
|
||||
"watch": "npm-run-all -lp watch-client watch-extensions",
|
||||
"watch": "npm-run-all -lp watch-client watch-extensions watch-extension-media",
|
||||
"watchd": "deemon yarn watch",
|
||||
"watch-webd": "deemon yarn watch-web",
|
||||
"kill-watchd": "deemon --kill yarn watch",
|
||||
@@ -25,6 +25,7 @@
|
||||
"watch-clientd": "deemon yarn watch-client",
|
||||
"kill-watch-clientd": "deemon --kill yarn watch-client",
|
||||
"watch-extensions": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extensions",
|
||||
"watch-extension-media": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extension-media",
|
||||
"watch-extensionsd": "deemon yarn watch-extensions",
|
||||
"kill-watch-extensionsd": "deemon --kill yarn watch-extensions",
|
||||
"mocha": "mocha test/unit/node/all.js --delay",
|
||||
|
||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
||||
// - Windows: call `process.chdir()` to always set application folder as cwd
|
||||
// - Posix: allow to change the current working dir via `VSCODE_CWD` if defined
|
||||
// - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups
|
||||
// // TODO@bpasero revisit if chdir() on Windows is needed in the future still
|
||||
// TODO@bpasero revisit if chdir() on Windows is needed in the future still
|
||||
function setupCurrentWorkingDirectory() {
|
||||
const path = require('path');
|
||||
|
||||
|
||||
@@ -232,9 +232,12 @@ export class IconLabel extends Disposable {
|
||||
}
|
||||
tokenSource = new CancellationTokenSource();
|
||||
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any {
|
||||
if ((e.type === dom.EventType.MOUSE_DOWN) || (<any>e).fromElement === htmlElement) {
|
||||
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
|
||||
if (isMouseDown) {
|
||||
hoverDisposable?.dispose();
|
||||
hoverDisposable = undefined;
|
||||
}
|
||||
if (isMouseDown || (<any>e).fromElement === htmlElement) {
|
||||
isHovering = false;
|
||||
hoverOptions = undefined;
|
||||
tokenSource.dispose(true);
|
||||
|
||||
@@ -1406,6 +1406,7 @@ export class QuickInputController extends Disposable {
|
||||
resolve(undefined);
|
||||
}),
|
||||
];
|
||||
input.title = options.title;
|
||||
input.canSelectMany = !!options.canPickMany;
|
||||
input.placeholder = options.placeHolder;
|
||||
input.ignoreFocusOut = !!options.ignoreFocusLost;
|
||||
@@ -1499,6 +1500,8 @@ export class QuickInputController extends Disposable {
|
||||
resolve(undefined);
|
||||
}),
|
||||
];
|
||||
|
||||
input.title = options.title;
|
||||
input.value = options.value || '';
|
||||
input.valueSelection = options.valueSelection;
|
||||
input.prompt = options.prompt;
|
||||
|
||||
@@ -59,6 +59,11 @@ export interface IQuickNavigateConfiguration {
|
||||
|
||||
export interface IPickOptions<T extends IQuickPickItem> {
|
||||
|
||||
/**
|
||||
* an optional string to show as the title of the quick input
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* an optional string to show as placeholder in the input box to guide the user what she picks on
|
||||
*/
|
||||
@@ -116,6 +121,11 @@ export interface IPickOptions<T extends IQuickPickItem> {
|
||||
|
||||
export interface IInputOptions {
|
||||
|
||||
/**
|
||||
* an optional string to show as the title of the quick input
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* the value to prefill in the input box
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
performance.mark('code/didStartRenderer')
|
||||
</script>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!-- Disable pinch zooming -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
|
||||
|
||||
<!-- Workbench Configuration -->
|
||||
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">
|
||||
|
||||
<!-- Workbench Auth Session -->
|
||||
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="{{WORKBENCH_WEB_BASE_URL}}/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="{{WORKBENCH_WEB_BASE_URL}}/manifest.json">
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.web.api.css">
|
||||
|
||||
</head>
|
||||
|
||||
<body aria-label="">
|
||||
</body>
|
||||
|
||||
<!-- Startup (do not modify order of script tags!) -->
|
||||
<script>
|
||||
var baseUrl = '{{WORKBENCH_WEB_BASE_URL}}';
|
||||
self.require = {
|
||||
baseUrl: `${baseUrl}/out`,
|
||||
recordStats: true,
|
||||
trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', {
|
||||
createScriptURL(value) {
|
||||
if(value.startsWith(baseUrl)) {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Invalid script url: ${value}`)
|
||||
}
|
||||
}),
|
||||
paths: {
|
||||
'vscode-textmate': `${baseUrl}/node_modules/vscode-textmate/release/main`,
|
||||
'vscode-oniguruma': `${baseUrl}/node_modules/vscode-oniguruma/release/main`,
|
||||
'xterm': `${baseUrl}/node_modules/xterm/lib/xterm.js`,
|
||||
'xterm-addon-search': `${baseUrl}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
|
||||
'xterm-addon-unicode11': `${baseUrl}/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
|
||||
'xterm-addon-webgl': `${baseUrl}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
|
||||
'tas-client-umd': `${baseUrl}/node_modules/tas-client-umd/lib/tas-client-umd.js`,
|
||||
'iconv-lite-umd': `${baseUrl}/node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||
'jschardet': `${baseUrl}/node_modules/jschardet/dist/jschardet.min.js`,
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/loader.js"></script>
|
||||
<script>
|
||||
performance.mark('code/willLoadWorkbenchMain');
|
||||
</script>
|
||||
<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.web.api.nls.js"></script>
|
||||
<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.web.api.js"></script>
|
||||
<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/code/browser/workbench/workbench.js"></script>
|
||||
</html>
|
||||
@@ -106,7 +106,7 @@ export class View extends ViewEventHandler {
|
||||
|
||||
// The view context is passed on to most classes (basically to reduce param. counts in ctors)
|
||||
this._context = new ViewContext(configuration, themeService.getColorTheme(), model);
|
||||
this._configPixelRatio = this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio);
|
||||
this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio);
|
||||
|
||||
// Ensure the view is the first event handler in order to update the layout
|
||||
this._context.addEventHandler(this);
|
||||
|
||||
@@ -1058,7 +1058,7 @@ export class SnakeCaseAction extends AbstractCaseAction {
|
||||
protected _modifyText(text: string, wordSeparators: string): string {
|
||||
return (text
|
||||
.replace(/(\p{Ll})(\p{Lu})/gmu, '$1_$2')
|
||||
.replace(/([^\b_])(\p{Lu})(\p{Ll})/gmu, '$1_$2$3')
|
||||
.replace(/(\p{Lu}|\p{N})(\p{Lu})(\p{Ll})/gmu, '$1_$2$3')
|
||||
.toLocaleLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -549,7 +549,10 @@ suite('Editor Contrib - Line Operations', () => {
|
||||
`function helloWorld() {
|
||||
return someGlobalObject.printHelloWorld("en", "utf-8");
|
||||
}
|
||||
helloWorld();`.replace(/^\s+/gm, '')
|
||||
helloWorld();`.replace(/^\s+/gm, ''),
|
||||
`'JavaScript'`,
|
||||
'parseHTML4String',
|
||||
'_accessor: ServicesAccessor'
|
||||
], {}, (editor) => {
|
||||
let model = editor.getModel()!;
|
||||
let uppercaseAction = new UpperCaseAction();
|
||||
@@ -659,6 +662,21 @@ suite('Editor Contrib - Line Operations', () => {
|
||||
}
|
||||
hello_world();`.replace(/^\s+/gm, ''));
|
||||
assertSelection(editor, new Selection(14, 1, 17, 15));
|
||||
|
||||
editor.setSelection(new Selection(18, 1, 18, 13));
|
||||
executeAction(snakecaseAction, editor);
|
||||
assert.strictEqual(model.getLineContent(18), `'java_script'`);
|
||||
assertSelection(editor, new Selection(18, 1, 18, 14));
|
||||
|
||||
editor.setSelection(new Selection(19, 1, 19, 17));
|
||||
executeAction(snakecaseAction, editor);
|
||||
assert.strictEqual(model.getLineContent(19), 'parse_html4_string');
|
||||
assertSelection(editor, new Selection(19, 1, 19, 19));
|
||||
|
||||
editor.setSelection(new Selection(20, 1, 20, 28));
|
||||
executeAction(snakecaseAction, editor);
|
||||
assert.strictEqual(model.getLineContent(20), '_accessor: services_accessor');
|
||||
assertSelection(editor, new Selection(20, 1, 20, 29));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
|
||||
// Location valid: indicate this as picker label
|
||||
if (this.isValidLineNumber(editor, lineNumber)) {
|
||||
if (this.isValidColumn(editor, lineNumber, column)) {
|
||||
return localize('gotoLineColumnLabel', "Go to line {0} and column {1}.", lineNumber, column);
|
||||
return localize('gotoLineColumnLabel', "Go to line {0} and character {1}.", lineNumber, column);
|
||||
}
|
||||
|
||||
return localize('gotoLineLabel', "Go to line {0}.", lineNumber);
|
||||
|
||||
@@ -26,6 +26,8 @@ import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
|
||||
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
|
||||
@@ -100,7 +102,7 @@ export class ExtensionsScanner extends Disposable {
|
||||
|
||||
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise<ILocalExtension> {
|
||||
const folderName = identifierWithVersion.key();
|
||||
const tempPath = path.join(this.extensionsPath, `.${folderName}`);
|
||||
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
|
||||
const extensionPath = path.join(this.extensionsPath, folderName);
|
||||
|
||||
try {
|
||||
@@ -117,11 +119,15 @@ export class ExtensionsScanner extends Disposable {
|
||||
await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
|
||||
this.logService.info('Renamed to', extensionPath);
|
||||
} catch (error) {
|
||||
this.logService.info('Rename failed. Deleting from extracted location', tempPath);
|
||||
try {
|
||||
pfs.rimraf(tempPath);
|
||||
await pfs.rimraf(tempPath);
|
||||
} catch (e) { /* ignore */ }
|
||||
throw error;
|
||||
if (error.code === 'ENOTEMPTY') {
|
||||
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id);
|
||||
} else {
|
||||
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let local: ILocalExtension | null = null;
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ import { WorkspacesManagementMainService, IStoredWorkspace, getSingleFolderWorks
|
||||
import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { normalizeDriveLetter } from 'vs/base/common/labels';
|
||||
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
|
||||
@@ -25,7 +25,7 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
suite('WorkspacesManagementMainService', () => {
|
||||
flakySuite('WorkspacesManagementMainService', () => {
|
||||
|
||||
class TestDialogMainService implements IDialogMainService {
|
||||
|
||||
|
||||
Vendored
+18
@@ -924,6 +924,24 @@ declare module 'vscode' {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region allow title property to QuickPickOptions/InputBoxOptions: https://github.com/microsoft/vscode/issues/77423
|
||||
|
||||
interface QuickPickOptions {
|
||||
/**
|
||||
* An optional string that represents the tile of the quick pick.
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface InputBoxOptions {
|
||||
/**
|
||||
* An optional string that represents the tile of the input box.
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Provide a way for custom editors to process untitled files without relying on textDocument https://github.com/microsoft/vscode/issues/115631
|
||||
/**
|
||||
* Additional information about the opening custom document.
|
||||
|
||||
@@ -89,6 +89,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
|
||||
const inputOptions: IInputOptions = Object.create(null);
|
||||
|
||||
if (options) {
|
||||
inputOptions.title = options.title;
|
||||
inputOptions.password = options.password;
|
||||
inputOptions.placeHolder = options.placeHolder;
|
||||
inputOptions.valueSelection = options.valueSelection;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ITerminalExternalLinkProvider, ITerminalInstance, ITerminalInstanceServ
|
||||
import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
|
||||
import { IAvailableProfilesRequest as IAvailableProfilesRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
|
||||
@@ -32,6 +33,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private readonly _terminalProcessProxies = new Map<number, ITerminalProcessExtHostProxy>();
|
||||
private _dataEventTracker: TerminalDataEventTracker | undefined;
|
||||
private _extHostKind: ExtensionHostKind;
|
||||
/**
|
||||
* A single shared terminal link provider for the exthost. When an ext registers a link
|
||||
* provider, this is registered with the terminal on the renderer side and all links are
|
||||
@@ -58,6 +60,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
|
||||
this._onInstanceDimensionsChanged(instance);
|
||||
}));
|
||||
|
||||
this._extHostKind = extHostContext.extensionHostKind;
|
||||
|
||||
this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance)));
|
||||
this._toDispose.add(_terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance)));
|
||||
this._toDispose.add(_terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance)));
|
||||
@@ -350,7 +354,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
|
||||
}
|
||||
|
||||
private async _onRequestAvailableProfiles(req: IAvailableProfilesRequest): Promise<void> {
|
||||
if (this._isPrimaryExtHost()) {
|
||||
if (this._isPrimaryExtHost() && this._extHostKind !== ExtensionHostKind.LocalWebWorker) {
|
||||
req.callback(await this._proxy.$getAvailableProfiles(req.quickLaunchOnly));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +505,8 @@ export interface BaseTransferQuickInput {
|
||||
|
||||
id: number;
|
||||
|
||||
title?: string;
|
||||
|
||||
type?: 'quickPick' | 'inputBox';
|
||||
|
||||
enabled?: boolean;
|
||||
@@ -559,6 +561,7 @@ export interface TransferInputBox extends BaseTransferQuickInput {
|
||||
}
|
||||
|
||||
export interface IInputBoxOptions {
|
||||
title?: string;
|
||||
value?: string;
|
||||
valueSelection?: [number, number];
|
||||
prompt?: string;
|
||||
|
||||
@@ -566,7 +566,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
|
||||
const extensionTestsPath = originalFSPath(extensionTestsLocationURI);
|
||||
|
||||
// Require the test runner via node require from the provided path
|
||||
const testRunner: ITestRunner | INewTestRunner | undefined = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false));
|
||||
const testRunner: ITestRunner | INewTestRunner | undefined = await this._loadCommonJSModule(null, extensionTestsLocationURI, new ExtensionActivationTimesBuilder(false));
|
||||
|
||||
if (!testRunner || typeof testRunner.run !== 'function') {
|
||||
throw new Error(nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsPath));
|
||||
|
||||
@@ -68,11 +68,12 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx
|
||||
const instance = ++this._instances;
|
||||
|
||||
const quickPickWidget = proxy.$show(instance, {
|
||||
placeHolder: options && options.placeHolder,
|
||||
matchOnDescription: options && options.matchOnDescription,
|
||||
matchOnDetail: options && options.matchOnDetail,
|
||||
ignoreFocusLost: options && options.ignoreFocusOut,
|
||||
canPickMany: options && options.canPickMany
|
||||
title: options?.title,
|
||||
placeHolder: options?.placeHolder,
|
||||
matchOnDescription: options?.matchOnDescription,
|
||||
matchOnDetail: options?.matchOnDetail,
|
||||
ignoreFocusLost: options?.ignoreFocusOut,
|
||||
canPickMany: options?.canPickMany,
|
||||
}, token);
|
||||
|
||||
const widgetClosedMarker = {};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterc
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
|
||||
namespace TrustedFunction {
|
||||
|
||||
@@ -68,6 +69,9 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
||||
private _fakeModules?: WorkerRequireInterceptor;
|
||||
|
||||
protected async _beforeAlmostReadyToRunExtensions(): Promise<void> {
|
||||
const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole);
|
||||
wrapConsoleMethods(mainThreadConsole);
|
||||
|
||||
// initialize API and register actors
|
||||
const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors);
|
||||
this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry);
|
||||
@@ -167,3 +171,74 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
||||
function ensureSuffix(path: string, suffix: string): string {
|
||||
return path.endsWith(suffix) ? path : path + suffix;
|
||||
}
|
||||
|
||||
// copied from bootstrap-fork.js
|
||||
function wrapConsoleMethods(service: MainThreadConsoleShape) {
|
||||
wrap('info', 'log');
|
||||
wrap('log', 'log');
|
||||
wrap('warn', 'warn');
|
||||
wrap('error', 'error');
|
||||
|
||||
function wrap(method: 'error' | 'warn' | 'info' | 'log', severity: 'error' | 'warn' | 'log') {
|
||||
console[method] = function () {
|
||||
service.$logExtensionHostMessage({ type: '__$console', severity, arguments: safeToArray(arguments) });
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_LENGTH = 100000;
|
||||
|
||||
function safeToArray(args: IArguments) {
|
||||
const seen: any[] = [];
|
||||
const argsArray = [];
|
||||
|
||||
// Massage some arguments with special treatment
|
||||
if (args.length) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
|
||||
// Any argument of type 'undefined' needs to be specially treated because
|
||||
// JSON.stringify will simply ignore those. We replace them with the string
|
||||
// 'undefined' which is not 100% right, but good enough to be logged to console
|
||||
if (typeof args[i] === 'undefined') {
|
||||
args[i] = 'undefined';
|
||||
}
|
||||
|
||||
// Any argument that is an Error will be changed to be just the error stack/message
|
||||
// itself because currently cannot serialize the error over entirely.
|
||||
else if (args[i] instanceof Error) {
|
||||
const errorObj = args[i];
|
||||
if (errorObj.stack) {
|
||||
args[i] = errorObj.stack;
|
||||
} else {
|
||||
args[i] = errorObj.toString();
|
||||
}
|
||||
}
|
||||
|
||||
argsArray.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = JSON.stringify(argsArray, function (key, value) {
|
||||
|
||||
// Objects get special treatment to prevent circles
|
||||
if (value && typeof value === 'object') {
|
||||
if (seen.indexOf(value) !== -1) {
|
||||
return '[Circular]';
|
||||
}
|
||||
|
||||
seen.push(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
if (res.length > MAX_LENGTH) {
|
||||
return 'Output omitted for a large object that exceeds the limits';
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
return `Output omitted for an object that cannot be inspected ('${error.toString()}')`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,29 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { DefaultSettingsEditorContribution } from 'vs/workbench/contrib/preferences/browser/preferencesEditor';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
const transientWordWrapState = 'transientWordWrapState';
|
||||
const isWordWrapMinifiedKey = 'isWordWrapMinified';
|
||||
const isDominatedByLongLinesKey = 'isDominatedByLongLines';
|
||||
const CAN_TOGGLE_WORD_WRAP = new RawContextKey<boolean>('canToggleWordWrap', false, true);
|
||||
const EDITOR_WORD_WRAP = new RawContextKey<boolean>('editorWordWrap', false, nls.localize('editorWordWrap', 'Whether the editor is currently using word wrapping.'));
|
||||
|
||||
/**
|
||||
* State written/read by the toggle word wrap action and associated with a particular model.
|
||||
@@ -63,21 +68,13 @@ class ToggleWordWrapAction extends EditorAction {
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
if (editor.getContribution(DefaultSettingsEditorContribution.ID)) {
|
||||
// in the settings editor...
|
||||
return;
|
||||
}
|
||||
if (!editor.hasModel()) {
|
||||
if (!canToggleWordWrap(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const codeEditorService = accessor.get(ICodeEditorService);
|
||||
const model = editor.getModel();
|
||||
|
||||
if (!canToggleWordWrap(model.uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the current state
|
||||
const transientState = readTransientState(model, codeEditorService);
|
||||
|
||||
@@ -137,25 +134,11 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution
|
||||
}));
|
||||
|
||||
const ensureWordWrapSettings = () => {
|
||||
if (this._editor.getContribution(DefaultSettingsEditorContribution.ID)) {
|
||||
// in the settings editor...
|
||||
return;
|
||||
}
|
||||
if (this._editor.isSimpleWidget) {
|
||||
// in a simple widget...
|
||||
return;
|
||||
}
|
||||
// Ensure correct word wrap settings
|
||||
const newModel = this._editor.getModel();
|
||||
if (!newModel) {
|
||||
if (!canToggleWordWrap(this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canToggleWordWrap(newModel.uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transientState = readTransientState(newModel, this._codeEditorService);
|
||||
const transientState = readTransientState(this._editor.getModel(), this._codeEditorService);
|
||||
|
||||
// Apply the state
|
||||
try {
|
||||
@@ -175,13 +158,89 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution
|
||||
}
|
||||
}
|
||||
|
||||
function canToggleWordWrap(uri: URI): boolean {
|
||||
if (!uri) {
|
||||
function canToggleWordWrap(editor: ICodeEditor | null): editor is IActiveCodeEditor {
|
||||
if (!editor) {
|
||||
return false;
|
||||
}
|
||||
return (uri.scheme !== 'output');
|
||||
if (editor.getContribution(DefaultSettingsEditorContribution.ID)) {
|
||||
// in the settings editor...
|
||||
return false;
|
||||
}
|
||||
if (editor.isSimpleWidget) {
|
||||
// in a simple widget...
|
||||
return false;
|
||||
}
|
||||
// Ensure correct word wrap settings
|
||||
const model = editor.getModel();
|
||||
if (!model) {
|
||||
return false;
|
||||
}
|
||||
if (model.uri.scheme === 'output') {
|
||||
// in output editor
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class EditorWordWrapContextKeyTracker implements IWorkbenchContribution {
|
||||
|
||||
private readonly _canToggleWordWrap: IContextKey<boolean>;
|
||||
private readonly _editorWordWrap: IContextKey<boolean>;
|
||||
private _activeEditor: ICodeEditor | null;
|
||||
private readonly _activeEditorListener: DisposableStore;
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
|
||||
@IContextKeyService private readonly _contextService: IContextKeyService,
|
||||
) {
|
||||
window.addEventListener('focus', () => this._update(), true);
|
||||
window.addEventListener('blur', () => this._update(), true);
|
||||
this._editorService.onDidActiveEditorChange(() => this._update());
|
||||
this._canToggleWordWrap = CAN_TOGGLE_WORD_WRAP.bindTo(this._contextService);
|
||||
this._editorWordWrap = EDITOR_WORD_WRAP.bindTo(this._contextService);
|
||||
this._activeEditor = null;
|
||||
this._activeEditorListener = new DisposableStore();
|
||||
this._update();
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
const activeEditor = this._codeEditorService.getFocusedCodeEditor() || this._codeEditorService.getActiveCodeEditor();
|
||||
if (this._activeEditor === activeEditor) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
this._activeEditorListener.clear();
|
||||
this._activeEditor = activeEditor;
|
||||
|
||||
if (activeEditor) {
|
||||
this._activeEditorListener.add(activeEditor.onDidChangeModel(() => this._updateFromCodeEditor()));
|
||||
this._activeEditorListener.add(activeEditor.onDidChangeConfiguration((e) => {
|
||||
if (e.hasChanged(EditorOption.wrappingInfo)) {
|
||||
this._updateFromCodeEditor();
|
||||
}
|
||||
}));
|
||||
this._updateFromCodeEditor();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateFromCodeEditor(): void {
|
||||
if (!canToggleWordWrap(this._activeEditor)) {
|
||||
return this._setValues(false, false);
|
||||
} else {
|
||||
const wrappingInfo = this._activeEditor.getOption(EditorOption.wrappingInfo);
|
||||
this._setValues(true, wrappingInfo.wrappingColumn !== -1);
|
||||
}
|
||||
}
|
||||
|
||||
private _setValues(canToggleWordWrap: boolean, isWordWrap: boolean): void {
|
||||
this._canToggleWordWrap.set(canToggleWordWrap);
|
||||
this._editorWordWrap.set(isWordWrap);
|
||||
}
|
||||
}
|
||||
|
||||
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench);
|
||||
workbenchRegistry.registerWorkbenchContribution(EditorWordWrapContextKeyTracker, LifecyclePhase.Ready);
|
||||
|
||||
registerEditorContribution(ToggleWordWrapController.ID, ToggleWordWrapController);
|
||||
|
||||
@@ -221,7 +280,9 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
|
||||
group: '5_editor',
|
||||
command: {
|
||||
id: TOGGLE_WORD_WRAP_ID,
|
||||
title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap")
|
||||
title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"),
|
||||
toggled: EDITOR_WORD_WRAP,
|
||||
precondition: CAN_TOGGLE_WORD_WRAP
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
||||
@@ -2491,7 +2491,7 @@ registerThemingParticipant((theme, collector) => {
|
||||
collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${CELL_TOP_MARGIN}px; }`);
|
||||
collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${MARKDOWN_CELL_BOTTOM_MARGIN}px; padding-top: ${MARKDOWN_CELL_TOP_MARGIN}px; }`);
|
||||
collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container.webview-backed-markdown-cell { padding: 0; }`);
|
||||
collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .webview-backed-markdown-cell .cell.code { padding-top: ${MARKDOWN_CELL_TOP_MARGIN}px; }`);
|
||||
collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .webview-backed-markdown-cell.markdown-cell-edit-mode .cell.code { padding-top: ${MARKDOWN_CELL_TOP_MARGIN}px; }`);
|
||||
collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`);
|
||||
collector.addRule(`.notebookOverlay .output { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + (CELL_MARGIN * 2)}px); }`);
|
||||
|
||||
|
||||
@@ -319,6 +319,7 @@ export class StatefulMarkdownCell extends Disposable {
|
||||
}
|
||||
|
||||
this.templateData.container.classList.toggle('collapsed', false);
|
||||
this.templateData.container.classList.toggle('markdown-cell-edit-mode', true);
|
||||
|
||||
if (this.editor) {
|
||||
editorHeight = this.editor.getContentHeight();
|
||||
@@ -401,6 +402,7 @@ export class StatefulMarkdownCell extends Disposable {
|
||||
DOM.hide(this.templateData.collapsedPart);
|
||||
DOM.show(this.markdownContainer);
|
||||
this.templateData.container.classList.toggle('collapsed', false);
|
||||
this.templateData.container.classList.toggle('markdown-cell-edit-mode', false);
|
||||
|
||||
this.renderedEditors.delete(this.viewCell);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Event } from 'vs/base/common/event';
|
||||
import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export const VIEWLET_ID = 'workbench.view.remote';
|
||||
|
||||
@@ -184,7 +185,8 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon
|
||||
@IDebugService readonly debugService: IDebugService,
|
||||
@IRemoteAgentService readonly remoteAgentService: IRemoteAgentService,
|
||||
@ITunnelService readonly tunnelService: ITunnelService,
|
||||
@IHostService readonly hostService: IHostService
|
||||
@IHostService readonly hostService: IHostService,
|
||||
@ILogService readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
if (!this.environmentService.remoteAuthority) {
|
||||
@@ -196,15 +198,15 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
|
||||
.registerDefaultConfigurations([{ 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT }]);
|
||||
this._register(new OutputAutomaticPortForwarding(terminalService, notificationService, openerService, externalOpenerService,
|
||||
remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, false));
|
||||
remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, logService, false));
|
||||
} else {
|
||||
const useProc = (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS);
|
||||
if (useProc) {
|
||||
this._register(new ProcAutomaticPortForwarding(configurationService, remoteExplorerService, notificationService,
|
||||
openerService, externalOpenerService, tunnelService, hostService));
|
||||
openerService, externalOpenerService, tunnelService, hostService, logService));
|
||||
}
|
||||
this._register(new OutputAutomaticPortForwarding(terminalService, notificationService, openerService, externalOpenerService,
|
||||
remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, useProc));
|
||||
remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, logService, useProc));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -222,17 +224,22 @@ class OnAutoForwardedAction extends Disposable {
|
||||
private readonly openerService: IOpenerService,
|
||||
private readonly externalOpenerService: IExternalUriOpenerService,
|
||||
private readonly tunnelService: ITunnelService,
|
||||
private readonly hostService: IHostService) {
|
||||
private readonly hostService: IHostService,
|
||||
private readonly logService: ILogService) {
|
||||
super();
|
||||
this.lastNotifyTime = new Date();
|
||||
this.lastNotifyTime.setFullYear(this.lastNotifyTime.getFullYear() - 1);
|
||||
}
|
||||
|
||||
public async doAction(tunnels: RemoteTunnel[]): Promise<void> {
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Starting action for ${tunnels[0]?.tunnelRemotePort}`);
|
||||
this.doActionTunnels = tunnels;
|
||||
const tunnel = await this.portNumberHeuristicDelay();
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Heuristic chose ${tunnel?.tunnelRemotePort}`);
|
||||
if (tunnel) {
|
||||
switch ((await this.remoteExplorerService.tunnelModel.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort)?.onAutoForward) {
|
||||
const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort)?.onAutoForward;
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) onAutoForward action is ${attributes}`);
|
||||
switch (attributes) {
|
||||
case OnPortForward.OpenBrowser: {
|
||||
const address = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
|
||||
await OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address);
|
||||
@@ -245,7 +252,9 @@ class OnAutoForwardedAction extends Disposable {
|
||||
}
|
||||
case OnPortForward.Silent: break;
|
||||
default:
|
||||
if (Date.now() - this.lastNotifyTime.getTime() > OnAutoForwardedAction.NOTIFY_COOL_DOWN) {
|
||||
const elapsed = new Date().getTime() - this.lastNotifyTime.getTime();
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) time elapsed since last notification ${elapsed} ms`);
|
||||
if (elapsed > OnAutoForwardedAction.NOTIFY_COOL_DOWN) {
|
||||
await this.showNotification(tunnel);
|
||||
}
|
||||
}
|
||||
@@ -263,6 +272,7 @@ class OnAutoForwardedAction extends Disposable {
|
||||
|
||||
private newerTunnel: RemoteTunnel | undefined;
|
||||
private async portNumberHeuristicDelay(): Promise<RemoteTunnel | undefined> {
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Starting heuristic delay`);
|
||||
if (!this.doActionTunnels || this.doActionTunnels.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -270,14 +280,17 @@ class OnAutoForwardedAction extends Disposable {
|
||||
const firstTunnel = this.doActionTunnels.shift()!;
|
||||
// Heuristic.
|
||||
if (firstTunnel.tunnelRemotePort % 1000 === 0) {
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Heuristic chose tunnel because % 1000: ${firstTunnel.tunnelRemotePort}`);
|
||||
this.newerTunnel = firstTunnel;
|
||||
return firstTunnel;
|
||||
// 9229 is the node inspect port
|
||||
} else if (firstTunnel.tunnelRemotePort < 10000 && firstTunnel.tunnelRemotePort !== 9229) {
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Heuristic chose tunnel because < 10000: ${firstTunnel.tunnelRemotePort}`);
|
||||
this.newerTunnel = firstTunnel;
|
||||
return firstTunnel;
|
||||
}
|
||||
|
||||
this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Waiting for "better" tunnel than ${firstTunnel.tunnelRemotePort}`);
|
||||
this.newerTunnel = undefined;
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
@@ -391,10 +404,11 @@ class OutputAutomaticPortForwarding extends Disposable {
|
||||
readonly tunnelService: ITunnelService,
|
||||
private readonly remoteAgentService: IRemoteAgentService,
|
||||
readonly hostService: IHostService,
|
||||
readonly logService: ILogService,
|
||||
readonly privilegedOnly: boolean
|
||||
) {
|
||||
super();
|
||||
this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService);
|
||||
this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService, logService);
|
||||
this._register(configurationService.onDidChangeConfiguration((e) => {
|
||||
if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) {
|
||||
this.tryStartStopUrlFinder();
|
||||
@@ -463,10 +477,11 @@ class ProcAutomaticPortForwarding extends Disposable {
|
||||
readonly openerService: IOpenerService,
|
||||
readonly externalOpenerService: IExternalUriOpenerService,
|
||||
readonly tunnelService: ITunnelService,
|
||||
readonly hostService: IHostService
|
||||
readonly hostService: IHostService,
|
||||
readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService);
|
||||
this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService, logService);
|
||||
this._register(configurationService.onDidChangeConfiguration(async (e) => {
|
||||
if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) {
|
||||
await this.startStopCandidateListener();
|
||||
|
||||
@@ -24,13 +24,14 @@ import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IPickOptions, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { ILocalTerminalService } from 'vs/platform/terminal/common/terminal';
|
||||
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
|
||||
import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions';
|
||||
import { Direction, IRemoteTerminalService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess';
|
||||
import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_ACTION_CATEGORY, TERMINAL_COMMAND_ID, TERMINAL_VIEW_ID, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, QUICK_LAUNCH_PROFILE_CHOICE, TERMINAL_ACTION_CATEGORY, TERMINAL_COMMAND_ID, TERMINAL_VIEW_ID, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints';
|
||||
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
@@ -1440,6 +1441,7 @@ export function registerTerminalActions() {
|
||||
const terminalContributionService = accessor.get(ITerminalContributionService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const storageService = accessor.get(IStorageService);
|
||||
if (!item || !item.split) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
@@ -1464,37 +1466,42 @@ export function registerTerminalActions() {
|
||||
|
||||
// Remove 'New ' from the selected item to get the profile name
|
||||
const profileSelection = item.substring(4);
|
||||
|
||||
const customType = terminalContributionService.terminalTypes.find(t => t.title === item);
|
||||
if (customType) {
|
||||
return commandService.executeCommand(customType.command);
|
||||
}
|
||||
if (detectedProfiles) {
|
||||
let launchConfig = detectedProfiles?.find((profile: { profileName: string; }) => profile.profileName === profileSelection);
|
||||
if (launchConfig && !launchConfig.isWorkspaceProfile) {
|
||||
const instance = terminalService.createTerminal({ executable: launchConfig.path, name: launchConfig.profileName, args: launchConfig.args });
|
||||
const instance = terminalService.createTerminal({ executable: launchConfig.path, args: launchConfig.args });
|
||||
terminalService.setActiveInstance(instance);
|
||||
} else if (launchConfig && launchConfig.isWorkspaceProfile) {
|
||||
notificationService.prompt(Severity.Info, `Do you allow this workspace to modify your terminal profile? ${item}`,
|
||||
[{
|
||||
label: 'Allow',
|
||||
run: () => {
|
||||
const instance = terminalService.createTerminal({ executable: launchConfig?.path, name: launchConfig?.profileName, args: launchConfig?.args });
|
||||
terminalService.setActiveInstance(instance);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Disallow',
|
||||
run: () => {
|
||||
const activeInstance = terminalService.getActiveInstance();
|
||||
if (activeInstance) {
|
||||
terminalService.setActiveInstance(activeInstance);
|
||||
if (!storageService.getBoolean(`${QUICK_LAUNCH_PROFILE_CHOICE}-${item}-${launchConfig.path}`, StorageScope.WORKSPACE, false)) {
|
||||
notificationService.prompt(Severity.Info, `Do you allow this workspace to modify your terminal profile? ${item} with path ${launchConfig?.path}`,
|
||||
[{
|
||||
label: 'Allow',
|
||||
run: () => {
|
||||
const instance = terminalService.createTerminal({ executable: launchConfig?.path, args: launchConfig?.args });
|
||||
terminalService.setActiveInstance(instance);
|
||||
storageService.store(`${QUICK_LAUNCH_PROFILE_CHOICE}-${item}-${launchConfig?.path}`, true, StorageScope.WORKSPACE, StorageTarget.USER);
|
||||
}
|
||||
}
|
||||
}]
|
||||
);
|
||||
},
|
||||
{
|
||||
label: 'Disallow',
|
||||
run: () => {
|
||||
const activeInstance = terminalService.getActiveInstance();
|
||||
if (activeInstance) {
|
||||
terminalService.setActiveInstance(activeInstance);
|
||||
}
|
||||
}
|
||||
}]
|
||||
);
|
||||
} else {
|
||||
const instance = terminalService.createTerminal({ executable: launchConfig?.path, args: launchConfig?.args });
|
||||
terminalService.setActiveInstance(instance);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const customType = terminalContributionService.terminalTypes.find(t => t.title === item);
|
||||
if (customType) {
|
||||
return commandService.executeCommand(customType.command);
|
||||
}
|
||||
console.warn(`Unmatched terminal item: "${item}"`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -151,7 +151,7 @@ export class TerminalService implements ITerminalService {
|
||||
if (e.affectsConfiguration('terminal.integrated.profiles.windows') ||
|
||||
e.affectsConfiguration('terminal.integrated.profiles.osx') ||
|
||||
e.affectsConfiguration('terminal.integrated.profiles.linux') ||
|
||||
e.affectsConfiguration('terminal.integrated.detectWslProfiles')) {
|
||||
e.affectsConfiguration('terminal.integrated.quickLaunchWslProfiles')) {
|
||||
this._onProfilesConfigChanged.fire();
|
||||
}
|
||||
});
|
||||
@@ -559,16 +559,10 @@ export class TerminalService implements ITerminalService {
|
||||
public async initializeTerminals(): Promise<void> {
|
||||
if (this._remoteTerminalsInitPromise) {
|
||||
await this._remoteTerminalsInitPromise;
|
||||
|
||||
if (!this.terminalTabs.length) {
|
||||
this.createTerminal();
|
||||
}
|
||||
} else if (this._localTerminalsInitPromise) {
|
||||
await this._localTerminalsInitPromise;
|
||||
if (!this.terminalTabs.length) {
|
||||
this.createTerminal();
|
||||
}
|
||||
} else if (!this.terminalTabs.length) {
|
||||
}
|
||||
if (this.terminalTabs.length === 0 && this.isProcessSupportRegistered) {
|
||||
this.createTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export interface ITerminalConfiguration {
|
||||
windows: string[];
|
||||
};
|
||||
profiles: ITerminalProfiles;
|
||||
detectWslProfiles: boolean;
|
||||
quickLaunchWslProfiles: boolean;
|
||||
altClickMovesCursor: boolean;
|
||||
macOptionIsMeta: boolean;
|
||||
macOptionClickForcesSelection: boolean;
|
||||
@@ -233,8 +233,8 @@ export interface ITerminalProfile {
|
||||
}
|
||||
|
||||
export const enum ProfileSource {
|
||||
'Git Bash',
|
||||
'PowerShell'
|
||||
GitBash = 'Git Bash',
|
||||
Pwsh = 'PowerShell'
|
||||
}
|
||||
|
||||
export interface ITerminalExecutable {
|
||||
@@ -369,6 +369,8 @@ export enum TitleEventSource {
|
||||
Sequence
|
||||
}
|
||||
|
||||
export const QUICK_LAUNCH_PROFILE_CHOICE = 'workbench.action.terminal.profile.choice';
|
||||
|
||||
export const enum TERMINAL_COMMAND_ID {
|
||||
FIND_NEXT = 'workbench.action.terminal.findNext',
|
||||
FIND_PREVIOUS = 'workbench.action.terminal.findPrevious',
|
||||
|
||||
@@ -83,9 +83,9 @@ export const terminalConfiguration: IConfigurationNode = {
|
||||
'terminal.integrated.profiles.windows': {
|
||||
markdownDescription: localize({
|
||||
key: 'terminal.integrated.profiles.windows',
|
||||
comment: ['{0}, {1}, {2}, and {3} are the `source`, `path`, `profileName`, and optional `args` settings keys']
|
||||
comment: ['{0}, {1}, {2}, and {3} are the `source`, `pathOrPaths`, `profileName`, and optional `args` settings keys']
|
||||
},
|
||||
"The windows shell profiles to select from when creating a new terminal via the terminal dropdown. Set to null to exclude them, use the {0} property to use the default detected configuration. Or, set the {1}, {2}, and optional {3}", '`source`', '`path`', '`profileName`', '`args`.'),
|
||||
"The windows shell profiles to select from when creating a new terminal via the terminal dropdown. Set to null to exclude them, use the {0} property to use the default detected configuration. Or, set the {1}, {2}, and optional {3}", '`source`', '`pathOrPaths`', '`profileName`', '`args`.'),
|
||||
type: 'object',
|
||||
default: {
|
||||
'PowerShell': {
|
||||
@@ -118,49 +118,49 @@ export const terminalConfiguration: IConfigurationNode = {
|
||||
'terminal.integrated.profiles.osx': {
|
||||
markdownDescription: localize({
|
||||
key: 'terminal.integrated.profile.osx',
|
||||
comment: ['{0}, {1}, and {2} are the `path`, `profileName`, and optional `args` settings keys']
|
||||
comment: ['{0}, {1}, and {2} are the `pathOrPaths`, `profileName`, and optional `args` settings keys']
|
||||
},
|
||||
"The osx shell profiles to select from when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0}, {1}, and optional {2}", '`path`', '`profileName`', '`args`.'),
|
||||
"The osx shell profiles to select from when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0}, {1}, and optional {2}", '`pathOrPaths`', '`profileName`', '`args`.'),
|
||||
type: 'object',
|
||||
default: {
|
||||
'bash': {
|
||||
path: 'bash'
|
||||
pathOrPaths: 'bash'
|
||||
},
|
||||
'zsh': {
|
||||
path: 'zsh'
|
||||
pathOrPaths: 'zsh'
|
||||
},
|
||||
'fish': {
|
||||
path: 'fish'
|
||||
pathOrPaths: 'fish'
|
||||
},
|
||||
'tmux': {
|
||||
path: 'tmux'
|
||||
pathOrPaths: 'tmux'
|
||||
}
|
||||
},
|
||||
},
|
||||
'terminal.integrated.profiles.linux': {
|
||||
markdownDescription: localize({
|
||||
key: 'terminal.integrated.profile.linux',
|
||||
comment: ['{0}, {1}, and {2} are the `path`, `profileName`, and optional `args` settings keys']
|
||||
comment: ['{0}, {1}, and {2} are the `pathOrPaths`, `profileName`, and optional `args` settings keys']
|
||||
},
|
||||
"The linux shell profiles to select from when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0}, {1}, and optional {2}", '`path`', '`profileName`', '`args`.'),
|
||||
"The linux shell profiles to select from when creating a new terminal via the terminal dropdown. When set, these will override the default detected profiles. They are comprised of a {0}, {1}, and optional {2}", '`pathOrPaths`', '`profileName`', '`args`.'),
|
||||
type: 'object',
|
||||
default: {
|
||||
'bash': {
|
||||
path: 'bash'
|
||||
pathOrPaths: 'bash'
|
||||
},
|
||||
'zsh': {
|
||||
path: 'zsh'
|
||||
pathOrPaths: 'zsh'
|
||||
},
|
||||
'fish': {
|
||||
path: 'fish'
|
||||
pathOrPaths: 'fish'
|
||||
},
|
||||
'tmux': {
|
||||
path: 'tmux'
|
||||
pathOrPaths: 'tmux'
|
||||
}
|
||||
}
|
||||
},
|
||||
'terminal.integrated.detectWslProfiles': {
|
||||
description: localize('terminal.integrated.detectWslProfiles', 'Controls whether or not WSL distros are detected as default profiles'),
|
||||
'terminal.integrated.quickLaunchWslProfiles': {
|
||||
description: localize('terminal.integrated.quickLaunchWslProfiles', 'Controls whether or not WSL distros are shown in the quick launch dropdown'),
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
|
||||
@@ -28,10 +28,10 @@ interface IPotentialTerminalProfile {
|
||||
}
|
||||
|
||||
export function detectAvailableProfiles(quickLaunchOnly: boolean, logService?: ILogService, config?: ITerminalConfiguration | ITestTerminalConfig, variableResolver?: ExtHostVariableResolverService, workspaceFolder?: IWorkspaceFolder, statProvider?: IStatProvider): Promise<ITerminalProfile[]> {
|
||||
return platform.isWindows ? detectAvailableWindowsProfiles(quickLaunchOnly, logService, config?.detectWslProfiles, config?.profiles?.windows, variableResolver, workspaceFolder, statProvider) : detectAvailableUnixProfiles(quickLaunchOnly, platform.isMacintosh ? config?.profiles?.osx : config?.profiles?.linux);
|
||||
return platform.isWindows ? detectAvailableWindowsProfiles(quickLaunchOnly, logService, config?.quickLaunchWslProfiles, config?.profiles?.windows, variableResolver, workspaceFolder, statProvider) : detectAvailableUnixProfiles(quickLaunchOnly, platform.isMacintosh ? config?.profiles?.osx : config?.profiles?.linux);
|
||||
}
|
||||
|
||||
async function detectAvailableWindowsProfiles(quickLaunchOnly: boolean, logService?: ILogService, detectWslProfiles?: boolean, configProfiles?: any, variableResolver?: ExtHostVariableResolverService, workspaceFolder?: IWorkspaceFolder, statProvider?: IStatProvider): Promise<ITerminalProfile[]> {
|
||||
async function detectAvailableWindowsProfiles(quickLaunchOnly: boolean, logService?: ILogService, quickLaunchWslProfiles?: boolean, configProfiles?: any, variableResolver?: ExtHostVariableResolverService, workspaceFolder?: IWorkspaceFolder, statProvider?: IStatProvider): Promise<ITerminalProfile[]> {
|
||||
// Determine the correct System32 path. We want to point to Sysnative
|
||||
// when the 32-bit version of VS Code is running on a 64-bit machine.
|
||||
// The reason for this is because PowerShell's important PSReadline
|
||||
@@ -69,7 +69,7 @@ async function detectAvailableWindowsProfiles(quickLaunchOnly: boolean, logServi
|
||||
],
|
||||
args: ['-l']
|
||||
},
|
||||
... await getWslProfiles(`${system32Path}\\${useWSLexe ? 'wsl.exe' : 'bash.exe'}`, detectWslProfiles, logService),
|
||||
... await getWslProfiles(`${system32Path}\\${useWSLexe ? 'wsl.exe' : 'bash.exe'}`, quickLaunchWslProfiles, logService),
|
||||
... await getPowershellProfiles()
|
||||
];
|
||||
|
||||
@@ -150,6 +150,10 @@ async function detectAvailableWindowsProfiles(quickLaunchOnly: boolean, logServi
|
||||
if (validProfiles.find(p => p.path.endsWith('pwsh.exe'))) {
|
||||
validProfiles = validProfiles.filter(p => p.profileName !== 'Windows PowerShell');
|
||||
}
|
||||
|
||||
if (quickLaunchWslProfiles) {
|
||||
validProfiles.push(...detectedProfiles.filter(p => p.path.endsWith('wsl.exe')));
|
||||
}
|
||||
return validProfiles;
|
||||
}
|
||||
|
||||
@@ -162,33 +166,33 @@ async function getPowershellProfiles(): Promise<IPotentialTerminalProfile[]> {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
async function getWslProfiles(wslPath: string, detectWslProfiles?: boolean, logService?: ILogService): Promise<IPotentialTerminalProfile[]> {
|
||||
async function getWslProfiles(wslPath: string, quickLaunchWslProfiles?: boolean, logService?: ILogService): Promise<IPotentialTerminalProfile[]> {
|
||||
let profiles: IPotentialTerminalProfile[] = [];
|
||||
if (detectWslProfiles) {
|
||||
const distroOutput = await new Promise<string>(r => cp.exec('wsl.exe -l', (err, stdout) => err ? logService?.trace('problem occurred when getting wsl distros', err) : r(stdout)));
|
||||
if (quickLaunchWslProfiles) {
|
||||
const distroOutput = await new Promise<string>((resolve, reject) => {
|
||||
cp.exec('wsl.exe -l', (err, stdout) => {
|
||||
if (err) {
|
||||
return reject('Problem occurred when getting wsl distros');
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
if (distroOutput) {
|
||||
let regex = new RegExp(/[\r?\n]/);
|
||||
let distroNames = Buffer.from(distroOutput).toString('utf8').split(regex).filter(t => t.trim().length > 0 && t !== '');
|
||||
let distroNames = distroOutput.split(regex).filter(t => t.trim().length > 0 && t !== '');
|
||||
// don't need the Windows Subsystem for Linux Distributions header
|
||||
distroNames.shift();
|
||||
for (const distroName of distroNames) {
|
||||
let s = '';
|
||||
let counter = 0;
|
||||
for (const c of Array.from(distroName)) {
|
||||
if (counter % 2 === 1) {
|
||||
// every other character is junk / a rectangle
|
||||
s += c;
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
if (s.endsWith('(Default)')) {
|
||||
// Ubuntu (Default) -> Ubuntu bc (Default) won't work
|
||||
s = s.substring(0, s.length - 10);
|
||||
}
|
||||
for (let distroName of distroNames) {
|
||||
// HACK: For some reason wsl.exe -l returns the string in an encoding where each
|
||||
// character takes up 2 bytes, it's unclear how to decode this properly so instead
|
||||
// we expect ascii and just remove all NUL chars
|
||||
distroName = distroName
|
||||
.replace(/\u0000/g, '')
|
||||
.replace(/ \(Default\)$/, '');
|
||||
|
||||
// docker-desktop-data is used by docker-desktop to store container images and isn't a valid profile type
|
||||
if (s !== '' && s !== 'docker-desktop-data') {
|
||||
let profile = { profileName: `${s} (WSL)`, paths: [wslPath], args: [`-d`, `${s}`] };
|
||||
if (distroName !== '' && distroName !== 'docker-desktop-data') {
|
||||
const profile = { profileName: `${distroName} (WSL)`, paths: [wslPath], args: [`-d`, `${distroName}`] };
|
||||
profiles.push(profile);
|
||||
}
|
||||
}
|
||||
@@ -201,23 +205,29 @@ async function getWslProfiles(wslPath: string, detectWslProfiles?: boolean, logS
|
||||
async function detectAvailableUnixProfiles(quickLaunchOnly?: boolean, configProfiles?: any): Promise<ITerminalProfile[]> {
|
||||
const contents = await fs.promises.readFile('/etc/shells', 'utf8');
|
||||
const profiles = contents.split('\n').filter(e => e.trim().indexOf('#') !== 0 && e.trim().length > 0);
|
||||
const detectedProfiles = profiles.map(e => {
|
||||
return {
|
||||
profileName: basename(e),
|
||||
path: e
|
||||
};
|
||||
});
|
||||
|
||||
let detectedProfiles: ITerminalProfile[] = [];
|
||||
let quickLaunchProfiles: ITerminalProfile[] = [];
|
||||
for (const profile of profiles) {
|
||||
detectedProfiles.push({ profileName: basename(profile), path: profile });
|
||||
// choose only the first
|
||||
if (!quickLaunchProfiles.find(p => p.profileName === basename(profile))) {
|
||||
quickLaunchProfiles.push({ profileName: basename(profile), path: profile });
|
||||
}
|
||||
}
|
||||
|
||||
if (!quickLaunchOnly) {
|
||||
return detectedProfiles;
|
||||
}
|
||||
|
||||
const validProfiles: ITerminalProfile[] = [];
|
||||
|
||||
for (const [profileName, value] of Object.entries(configProfiles)) {
|
||||
if ((value as ITerminalExecutable).pathOrPaths) {
|
||||
const configProfile = (value as ITerminalExecutable);
|
||||
const path = configProfile.pathOrPaths;
|
||||
if (Array.isArray(path)) {
|
||||
for (const possiblePath of path) {
|
||||
const pathOrPaths = configProfile.pathOrPaths;
|
||||
if (Array.isArray(pathOrPaths)) {
|
||||
for (const possiblePath of pathOrPaths) {
|
||||
const profile = detectedProfiles.find(p => p.path.endsWith(possiblePath));
|
||||
if (profile) {
|
||||
validProfiles.push({ profileName, path: profile.path });
|
||||
@@ -225,7 +235,7 @@ async function detectAvailableUnixProfiles(quickLaunchOnly?: boolean, configProf
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const profile = detectedProfiles.find(p => p.path.endsWith(path));
|
||||
const profile = detectedProfiles.find(p => p.path.endsWith(pathOrPaths));
|
||||
if (profile) {
|
||||
validProfiles.push({ profileName, path: profile.path });
|
||||
}
|
||||
|
||||
@@ -4,63 +4,67 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// import * as assert from 'assert';
|
||||
import assert = require('assert');
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { ITerminalProfiles } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
// import { detectAvailableProfiles, IStatProvider } from 'vs/workbench/contrib/terminal/node/terminalProfiles';
|
||||
import { ITerminalProfiles, ProfileSource } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { detectAvailableProfiles, IStatProvider } from 'vs/workbench/contrib/terminal/node/terminalProfiles';
|
||||
|
||||
export interface ITestTerminalConfig {
|
||||
profiles: ITerminalProfiles;
|
||||
detectWslProfiles: boolean
|
||||
quickLaunchWslProfiles: boolean
|
||||
}
|
||||
|
||||
suite('Workbench - TerminalProfiles', () => {
|
||||
suite('detectAvailableProfiles', () => {
|
||||
if (isWindows) {
|
||||
suite('detectAvailableWindowsProfiles', async () => {
|
||||
// test('should detect cmd prompt', async () => {
|
||||
// const _paths = ['C:\\WINDOWS\\System32\\cmd.exe'];
|
||||
// let config: ITestTerminalConfig = {
|
||||
// profiles: {
|
||||
// windows: {
|
||||
// 'Command Prompt': { path: _paths }
|
||||
// }
|
||||
// },
|
||||
// detectWslProfiles: false
|
||||
// };
|
||||
// const profiles = await detectAvailableProfiles(true, undefined, config, undefined, undefined, createStatProvider(_paths));
|
||||
// const expected = [{ profileName: 'Command Prompt', path: _paths[0] }];
|
||||
// assert.deepStrictEqual(expected, profiles);
|
||||
// });
|
||||
test('should detect Git Bash and provide login args', async () => {
|
||||
// const _paths = [`C:\\Program Files\\Git\\bin\\bash.exe`];
|
||||
// let config: ITestTerminalConfig = {
|
||||
// profiles: {
|
||||
// windows: {
|
||||
// 'Git Bash': {
|
||||
// source: ProfileSource['Git Bash']
|
||||
// },
|
||||
// },
|
||||
// linux: {},
|
||||
// osx: {}
|
||||
// },
|
||||
// detectWslProfiles: false
|
||||
// };
|
||||
// const profiles = await detectAvailableProfiles(true, undefined, config, undefined, undefined, createStatProvider(_paths));
|
||||
// const expected = [{ profileName: 'Git Bash', path: _paths[0], args: ['--login'] }];
|
||||
// assert.deepStrictEqual(profiles, expected);
|
||||
const _paths = [`C:\\Program Files\\Git\\bin\\bash.exe`];
|
||||
let config: ITestTerminalConfig = {
|
||||
profiles: {
|
||||
windows: {
|
||||
'Git Bash': { source: ProfileSource.GitBash }
|
||||
},
|
||||
linux: {},
|
||||
osx: {}
|
||||
},
|
||||
quickLaunchWslProfiles: false
|
||||
};
|
||||
const profiles = await detectAvailableProfiles(true, undefined, config, undefined, undefined, createStatProvider(_paths));
|
||||
const expected = [{ profileName: 'Git Bash', path: _paths[0], args: ['--login'] }];
|
||||
assert.deepStrictEqual(profiles, expected);
|
||||
});
|
||||
});
|
||||
// test('should detect cmd prompt', async () => {
|
||||
// const _paths = ['C:\\WINDOWS\\System32\\cmd.exe'];
|
||||
// let config: ITestTerminalConfig = {
|
||||
// profiles: {
|
||||
// windows: {
|
||||
// 'Command Prompt': { pathOrPaths: _paths }
|
||||
// },
|
||||
// linux: {},
|
||||
// osx: {},
|
||||
// },
|
||||
// quickLaunchWslProfiles: false
|
||||
// };
|
||||
// const profiles = await detectAvailableProfiles(true, undefined, config, undefined, undefined, createStatProvider(_paths));
|
||||
// const expected = [{ profileName: 'Command Prompt', path: _paths[0] }];
|
||||
// assert.deepStrictEqual(expected, profiles);
|
||||
// });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function createStatProvider(expectedPaths: string[]): IStatProvider {
|
||||
const provider = {
|
||||
stat(path: string) {
|
||||
return expectedPaths.includes(path);
|
||||
},
|
||||
lstat(path: string) {
|
||||
return expectedPaths.includes(path);
|
||||
}
|
||||
};
|
||||
return provider;
|
||||
}
|
||||
});
|
||||
// function createStatProvider(expectedPaths: string[]): IStatProvider {
|
||||
// const provider = {
|
||||
// stat(path: string) {
|
||||
// return expectedPaths.includes(path);
|
||||
// },
|
||||
// lstat(path: string) {
|
||||
// return expectedPaths.includes(path);
|
||||
// }
|
||||
// };
|
||||
// return provider;
|
||||
// }
|
||||
|
||||
@@ -111,9 +111,9 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .category-title {
|
||||
margin-bottom: 4px;
|
||||
margin: 2px 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .category-description-container {
|
||||
@@ -134,7 +134,7 @@
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
line-height: 24px;
|
||||
padding-left: 10px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .categories-split-view li {
|
||||
@@ -154,12 +154,16 @@
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .categories-split-view .start-container .codicon {
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .categories-split-view .keybinding-label {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.categories .progress-bar-outer {
|
||||
height: 8px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -188,8 +192,9 @@
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
line-height: normal;
|
||||
margin-bottom: 6px;
|
||||
padding: 6px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px 12px;
|
||||
left: 1px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
}
|
||||
@@ -200,15 +205,14 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon {
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon.hide-category-button {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
font-size: 12px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
font-size: 16px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@@ -250,8 +254,8 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.detail .getting-started-category .codicon {
|
||||
margin-left:0;
|
||||
font-size: 22pt;
|
||||
margin-right: 8px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.detail .getting-started-detail-columns {
|
||||
@@ -465,3 +469,15 @@
|
||||
top: -2px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .getting-started-category .codicon {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .codicon-close {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer .getting-started-category:hover .codicon-close {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { assertIsDefined } from 'vs/base/common/types';
|
||||
import { $, addDisposableListener, Dimension, reset } from 'vs/base/browser/dom';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IGettingStartedCategoryWithProgress, IGettingStartedService } from 'vs/workbench/services/gettingStarted/common/gettingStartedService';
|
||||
import { IGettingStartedCategory, IGettingStartedCategoryDescriptor, IGettingStartedCategoryWithProgress, IGettingStartedService } from 'vs/workbench/services/gettingStarted/common/gettingStartedService';
|
||||
import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { welcomePageBackground, welcomePageProgressBackground, welcomePageProgressForeground, welcomePageTileBackground, welcomePageTileHoverBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors';
|
||||
import { activeContrastBorder, buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, descriptionForeground, focusBorder, foreground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
@@ -27,7 +27,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IGettingStartedCategory, IGettingStartedCategoryDescriptor } from 'vs/workbench/services/gettingStarted/common/gettingStartedRegistry';
|
||||
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
|
||||
import { IRecentFolder, IRecentlyOpened, IRecentWorkspace, isRecentFolder, isRecentWorkspace, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
||||
@@ -39,6 +38,7 @@ import { splitName } from 'vs/base/common/labels';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
|
||||
const SLIDE_TRANSITION_TIME_MS = 250;
|
||||
const configurationKey = 'workbench.startupEditor';
|
||||
|
||||
@@ -155,7 +155,33 @@ export class GettingStartedPage extends EditorPane {
|
||||
this.buildCategoriesSlide();
|
||||
}));
|
||||
|
||||
this._register(this.gettingStartedService.onDidAddCategory(category => console.log('added new category', category, 'that isnt being rendered yet')));
|
||||
this._register(this.gettingStartedService.onDidChangeTask(task => {
|
||||
const ourCategory = this.gettingStartedCategories.find(c => c.id === task.category);
|
||||
if (!ourCategory || ourCategory.content.type === 'startEntry') {
|
||||
console.error('Attempting to modify category that does not exist or is invlid type', task);
|
||||
return;
|
||||
}
|
||||
const ourTask = ourCategory.content.items.find(item => item.id === task.id);
|
||||
if (!ourTask) {
|
||||
console.error('Attempting to modify task that cannot be found', task);
|
||||
return;
|
||||
}
|
||||
ourTask.title = task.title;
|
||||
ourTask.description = task.description;
|
||||
ourTask.media.path = task.media.path;
|
||||
}));
|
||||
|
||||
this._register(this.gettingStartedService.onDidChangeCategory(category => {
|
||||
const ourCategory = this.gettingStartedCategories.find(c => c.id === category.id);
|
||||
if (!ourCategory) {
|
||||
console.error('Attempting to modify category that does not exist or is invlid type', category);
|
||||
return;
|
||||
}
|
||||
|
||||
ourCategory.title = category.title;
|
||||
ourCategory.description = category.description;
|
||||
}));
|
||||
|
||||
this._register(this.gettingStartedService.onDidProgressTask(task => {
|
||||
const category = this.gettingStartedCategories.find(category => category.id === task.category);
|
||||
if (!category) { throw Error('Could not find category with ID: ' + task.category); }
|
||||
@@ -709,7 +735,7 @@ export class GettingStartedPage extends EditorPane {
|
||||
|
||||
const keybindingLabel = (task.button.command && this.getKeybindingLabel(task.button.command));
|
||||
if (keybindingLabel) {
|
||||
taskDescription.appendChild($('span.shortcut-message', {}, 'Pro Tip: Use keyboard shortcut ', $('span.keybinding', {}, keybindingLabel)));
|
||||
taskDescription.appendChild($('span.shortcut-message', {}, 'Tip: Use keyboard shortcut ', $('span.keybinding', {}, keybindingLabel)));
|
||||
}
|
||||
|
||||
return $('button.getting-started-task',
|
||||
@@ -852,7 +878,7 @@ registerThemingParticipant((theme, collector) => {
|
||||
|
||||
const iconColor = theme.getColor(textLinkForeground);
|
||||
if (iconColor) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .getting-started-category .codicon { color: ${iconColor} }`);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .getting-started-category .codicon:not(.codicon-close) { color: ${iconColor} }`);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .codicon.complete { color: ${iconColor} } `);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task.expanded .codicon { color: ${iconColor} } `);
|
||||
}
|
||||
@@ -892,18 +918,18 @@ registerThemingParticipant((theme, collector) => {
|
||||
|
||||
const link = theme.getColor(textLinkForeground);
|
||||
if (link) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a { color: ${link}; }`);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.codicon-close) { color: ${link}; }`);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link { color: ${link}; }`);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .button-link .scroll-button { color: ${link}; }`);
|
||||
}
|
||||
const activeLink = theme.getColor(textLinkActiveForeground);
|
||||
if (activeLink) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a:hover,
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.codicon-close):hover,
|
||||
.monaco-workbench .part.editor > .content .gettingStartedContainer a:active { color: ${activeLink}; }`);
|
||||
}
|
||||
const focusColor = theme.getColor(focusBorder);
|
||||
if (focusColor) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a:focus { outline-color: ${focusColor}; }`);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.codicon-close):focus { outline-color: ${focusColor}; }`);
|
||||
}
|
||||
const border = theme.getColor(contrastBorder);
|
||||
if (border) {
|
||||
|
||||
@@ -185,6 +185,8 @@ export class ExperimentService implements ITASExperimentService {
|
||||
private telemetry: ExperimentServiceTelemetry | undefined;
|
||||
private static MEMENTO_ID = 'experiment.service.memento';
|
||||
|
||||
private overrideInitDelay: Promise<void>;
|
||||
|
||||
private get experimentsEnabled(): boolean {
|
||||
return this.configurationService.getValue('workbench.enableExperiments') === true;
|
||||
}
|
||||
@@ -199,9 +201,18 @@ export class ExperimentService implements ITASExperimentService {
|
||||
if (productService.tasConfig && this.experimentsEnabled && this.telemetryService.isOptedIn) {
|
||||
this.tasClient = this.setupTASClient();
|
||||
}
|
||||
|
||||
const overrideDelay = this.configurationService.getValue<number>('tasTreatmentOverrideDelay') ?? 2000;
|
||||
this.overrideInitDelay = new Promise(resolve => setTimeout(resolve, overrideDelay));
|
||||
}
|
||||
|
||||
async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
|
||||
const override = this.configurationService.getValue('tasTreatmentOverrides.' + name);
|
||||
if (override !== undefined) {
|
||||
await this.overrideInitDelay;
|
||||
return override as T;
|
||||
}
|
||||
|
||||
const startSetup = Date.now();
|
||||
|
||||
if (!this.tasClient) {
|
||||
|
||||
@@ -357,7 +357,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost
|
||||
appUriScheme: this._productService.urlProtocol,
|
||||
appLanguage: platform.language,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsLocationURI: undefined, // never run extension tests in web worker extension host
|
||||
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||
globalStorageHome: this._environmentService.globalStorageHome,
|
||||
workspaceStorageHome: this._environmentService.workspaceStorageHome,
|
||||
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||
|
||||
@@ -32,6 +32,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
|
||||
import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
@@ -428,18 +429,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Use `this._registry` and `this._runningLocation` to better determine which extension host should launch the test runner
|
||||
let extensionHostManager: ExtensionHostManager | null = null;
|
||||
if (this._environmentService.extensionTestsLocationURI.scheme === Schemas.vscodeRemote) {
|
||||
extensionHostManager = this._getExtensionHostManager(ExtensionHostKind.Remote);
|
||||
}
|
||||
if (!extensionHostManager) {
|
||||
// When a debugger attaches to the extension host, it will surface all console.log messages from the extension host,
|
||||
// but not necessarily from the window. So it would be best if any errors get printed to the console of the extension host.
|
||||
// That is why here we use the local process extension host even for non-file URIs
|
||||
extensionHostManager = this._getExtensionHostManager(ExtensionHostKind.LocalProcess);
|
||||
}
|
||||
|
||||
const extensionHostManager = this.findTestExtensionHost(this._environmentService.extensionTestsLocationURI);
|
||||
if (!extensionHostManager) {
|
||||
const msg = nls.localize('extensionTestError', "No extension host found that can launch the test runner at {0}.", this._environmentService.extensionTestsLocationURI.toString());
|
||||
console.error(msg);
|
||||
@@ -447,6 +437,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let exitCode: number;
|
||||
try {
|
||||
exitCode = await extensionHostManager.extensionTestsExecute();
|
||||
@@ -459,6 +450,40 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
this._onExtensionHostExit(exitCode);
|
||||
}
|
||||
|
||||
private findTestExtensionHost(testLocation: URI): ExtensionHostManager | undefined | null {
|
||||
let extensionHostKind: ExtensionHostKind | undefined;
|
||||
|
||||
for (const extension of this._registry.getAllExtensionDescriptions()) {
|
||||
if (isEqualOrParent(testLocation, extension.extensionLocation)) {
|
||||
const runningLocation = this._runningLocation.get(ExtensionIdentifier.toKey(extension.identifier));
|
||||
if (runningLocation === ExtensionRunningLocation.LocalProcess) {
|
||||
extensionHostKind = ExtensionHostKind.LocalProcess;
|
||||
} else if (runningLocation === ExtensionRunningLocation.LocalWebWorker) {
|
||||
extensionHostKind = ExtensionHostKind.LocalWebWorker;
|
||||
} else if (runningLocation === ExtensionRunningLocation.Remote) {
|
||||
extensionHostKind = ExtensionHostKind.Remote;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (extensionHostKind === undefined) {
|
||||
// not sure if we should support that, but it was possible to have an test outside an extension
|
||||
|
||||
if (testLocation.scheme === Schemas.vscodeRemote) {
|
||||
extensionHostKind = ExtensionHostKind.Remote;
|
||||
} else {
|
||||
// When a debugger attaches to the extension host, it will surface all console.log messages from the extension host,
|
||||
// but not necessarily from the window. So it would be best if any errors get printed to the console of the extension host.
|
||||
// That is why here we use the local process extension host even for non-file URIs
|
||||
extensionHostKind = ExtensionHostKind.LocalProcess;
|
||||
}
|
||||
}
|
||||
if (extensionHostKind !== undefined) {
|
||||
return this._getExtensionHostManager(extensionHostKind);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _releaseBarrier(): void {
|
||||
this._installedExtensionsReady.open();
|
||||
this._onDidRegisterExtensions.fire(undefined);
|
||||
|
||||
@@ -14,7 +14,7 @@ const beginnerIcon = registerIcon('getting-started-beginner', Codicon.lightbulb,
|
||||
const codespacesIcon = registerIcon('getting-started-codespaces', Codicon.github, localize('getting-started-codespaces-icon', "Icon used for the codespaces category of getting started"));
|
||||
|
||||
|
||||
type GettingStartedItem = {
|
||||
export type BuiltinGettingStartedItem = {
|
||||
id: string
|
||||
title: string,
|
||||
description: string,
|
||||
@@ -26,30 +26,20 @@ type GettingStartedItem = {
|
||||
media: { type: 'image', path: string | { hc: string, light: string, dark: string }, altText: string },
|
||||
};
|
||||
|
||||
type GettingStartedCategory = {
|
||||
export type BuiltinGettingStartedCategory = {
|
||||
id: string
|
||||
title: string,
|
||||
description: string,
|
||||
icon: ThemeIcon,
|
||||
when?: string,
|
||||
content:
|
||||
| { type: 'items', items: GettingStartedItem[] }
|
||||
| { type: 'items', items: BuiltinGettingStartedItem[] }
|
||||
| { type: 'startEntry', command: string }
|
||||
};
|
||||
|
||||
type GettingStartedContent = GettingStartedCategory[];
|
||||
type GettingStartedContent = BuiltinGettingStartedCategory[];
|
||||
|
||||
export const content: GettingStartedContent = [
|
||||
// {
|
||||
// id: 'topLevelCommandPalette',
|
||||
// title: localize('gettingStarted.commandPalette.title', "Command Palette"),
|
||||
// description: localize('gettingStarted.commandPalette.description', "The one keybinding to show you everything VS Code can do."),
|
||||
// icon: Codicon.symbolColor,
|
||||
// content: {
|
||||
// type: 'startEntry',
|
||||
// command: 'workbench.action.showCommands',
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: 'topLevelNewFile',
|
||||
title: localize('gettingStarted.newFile.title', "New File"),
|
||||
@@ -104,13 +94,13 @@ export const content: GettingStartedContent = [
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'topLevelSeeExtensions',
|
||||
title: localize('gettingStarted.languageSupport.title', "Install Language Support"),
|
||||
description: localize('gettingStarted.languageSupport.description', "Want even more features? Install extensions to add support for languages like Python, C, or Java."),
|
||||
icon: Codicon.extensions,
|
||||
id: 'topLevelCommandPalette',
|
||||
title: localize('gettingStarted.topLevelCommandPalette.title', "Run a Command..."),
|
||||
description: localize('gettingStarted.topLevelCommandPalette.description', "Use the command palette to view and run all of vscode's commands"),
|
||||
icon: Codicon.symbolColor,
|
||||
content: {
|
||||
type: 'startEntry',
|
||||
command: 'workbench.extensions.action.showPopularExtensions',
|
||||
command: 'workbench.action.showCommands',
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,172 +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 { Emitter, Event } from 'vs/base/common/event';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { content } from 'vs/workbench/services/gettingStarted/common/gettingStartedContent';
|
||||
|
||||
export const enum GettingStartedCategory {
|
||||
Beginner = 'Beginner',
|
||||
Intermediate = 'Intermediate',
|
||||
Advanced = 'Advanced'
|
||||
}
|
||||
|
||||
export interface IGettingStartedTask {
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
category: GettingStartedCategory | string,
|
||||
when: ContextKeyExpression,
|
||||
order: number,
|
||||
button:
|
||||
| { title: string, command?: never, link: string }
|
||||
| { title: string, command: string, link?: never },
|
||||
doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never, }
|
||||
media: { type: 'image', path: { hc: URI, light: URI, dark: URI }, altText: string },
|
||||
}
|
||||
|
||||
export interface IGettingStartedCategoryDescriptor {
|
||||
id: GettingStartedCategory | string
|
||||
title: string
|
||||
description: string
|
||||
icon:
|
||||
| { type: 'icon', icon: ThemeIcon }
|
||||
| { type: 'image', path: string }
|
||||
when: ContextKeyExpression
|
||||
content:
|
||||
| { type: 'items' }
|
||||
| { type: 'startEntry', command: string }
|
||||
}
|
||||
|
||||
export interface IGettingStartedCategory {
|
||||
id: GettingStartedCategory | string
|
||||
title: string
|
||||
description: string
|
||||
icon:
|
||||
| { type: 'icon', icon: ThemeIcon }
|
||||
| { type: 'image', path: string }
|
||||
when: ContextKeyExpression
|
||||
content:
|
||||
| { type: 'items', items: IGettingStartedTask[] }
|
||||
| { type: 'startEntry', command: string }
|
||||
}
|
||||
|
||||
export interface IGettingStartedRegistry {
|
||||
onDidAddCategory: Event<IGettingStartedCategory>
|
||||
onDidAddTask: Event<IGettingStartedTask>
|
||||
|
||||
registerTask(task: IGettingStartedTask): IGettingStartedTask;
|
||||
getTask(id: string): IGettingStartedTask
|
||||
|
||||
registerCategory(categoryDescriptor: IGettingStartedCategoryDescriptor): void
|
||||
getCategory(id: GettingStartedCategory | string): Readonly<IGettingStartedCategory> | undefined
|
||||
|
||||
getCategories(): readonly Readonly<IGettingStartedCategory>[]
|
||||
}
|
||||
|
||||
export class GettingStartedRegistryImpl implements IGettingStartedRegistry {
|
||||
private readonly _onDidAddTask = new Emitter<IGettingStartedTask>();
|
||||
onDidAddTask: Event<IGettingStartedTask> = this._onDidAddTask.event;
|
||||
private readonly _onDidAddCategory = new Emitter<IGettingStartedCategory>();
|
||||
onDidAddCategory: Event<IGettingStartedCategory> = this._onDidAddCategory.event;
|
||||
|
||||
private readonly gettingStartedContributions = new Map<string, IGettingStartedCategory>();
|
||||
private readonly tasks = new Map<string, IGettingStartedTask>();
|
||||
|
||||
public registerTask(task: IGettingStartedTask): IGettingStartedTask {
|
||||
const category = this.gettingStartedContributions.get(task.category);
|
||||
if (!category) { throw Error('Registering getting started task to category that does not exist (' + task.category + ')'); }
|
||||
if (category.content.type !== 'items') { throw Error('Registering getting started task to category that is not of `items` type (' + task.category + ')'); }
|
||||
if (this.tasks.has(task.id)) { throw Error('Attempting to register task with id ' + task.id + ' twice. Second is dropped.'); }
|
||||
this.tasks.set(task.id, task);
|
||||
category.content.items.push(task);
|
||||
this._onDidAddTask.fire(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public registerCategory(categoryDescriptor: IGettingStartedCategoryDescriptor): void {
|
||||
const oldCategory = this.gettingStartedContributions.get(categoryDescriptor.id);
|
||||
if (oldCategory) {
|
||||
console.error(`Skipping attempt to overwrite getting started category. (${categoryDescriptor})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const category: IGettingStartedCategory = {
|
||||
...categoryDescriptor,
|
||||
content: categoryDescriptor.content.type === 'items'
|
||||
? { type: 'items', items: [] }
|
||||
: categoryDescriptor.content
|
||||
};
|
||||
|
||||
this.gettingStartedContributions.set(categoryDescriptor.id, category);
|
||||
this._onDidAddCategory.fire(category);
|
||||
}
|
||||
|
||||
public getCategory(id: GettingStartedCategory | string): Readonly<IGettingStartedCategory> | undefined {
|
||||
return this.gettingStartedContributions.get(id);
|
||||
}
|
||||
|
||||
public getTask(id: string): IGettingStartedTask {
|
||||
const task = this.tasks.get(id);
|
||||
if (!task) { throw Error('Attempting to access task which does not exist in registry ' + id); }
|
||||
return task;
|
||||
}
|
||||
|
||||
public getCategories(): readonly Readonly<IGettingStartedCategory>[] {
|
||||
return [...this.gettingStartedContributions.values()];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const GettingStartedRegistryID = 'GettingStartedRegistry';
|
||||
const registryImpl = new GettingStartedRegistryImpl();
|
||||
|
||||
content.forEach(category => {
|
||||
|
||||
registryImpl.registerCategory({
|
||||
...category,
|
||||
icon: { type: 'icon', icon: category.icon },
|
||||
when: ContextKeyExpr.deserialize(category.when) ?? ContextKeyExpr.true()
|
||||
});
|
||||
|
||||
if (category.content.type === 'items') {
|
||||
const convertPaths = (path: string | { hc: string, dark: string, light: string }): { hc: URI, dark: URI, light: URI } => {
|
||||
const convertPath = (path: string) => path.startsWith('https://')
|
||||
? URI.parse(path, true)
|
||||
: FileAccess.asBrowserUri('vs/workbench/services/gettingStarted/common/media/' + path, require);
|
||||
if (typeof path === 'string') {
|
||||
const converted = convertPath(path);
|
||||
return { hc: converted, dark: converted, light: converted };
|
||||
} else {
|
||||
return {
|
||||
hc: convertPath(path.hc),
|
||||
light: convertPath(path.light),
|
||||
dark: convertPath(path.dark)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
category.content.items.forEach((item, index) => {
|
||||
registryImpl.registerTask({
|
||||
...item,
|
||||
category: category.id,
|
||||
order: index,
|
||||
when: ContextKeyExpr.deserialize(item.when) ?? ContextKeyExpr.true(),
|
||||
media: {
|
||||
type: item.media.type,
|
||||
altText: item.media.altText,
|
||||
path: convertPaths(item.media.path)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Registry.add(GettingStartedRegistryID, registryImpl);
|
||||
export const GettingStartedRegistry: IGettingStartedRegistry = Registry.as(GettingStartedRegistryID);
|
||||
@@ -3,15 +3,14 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { createDecorator, optional, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IGettingStartedTask, GettingStartedRegistry, IGettingStartedCategory, } from 'vs/workbench/services/gettingStarted/common/gettingStartedRegistry';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { Memento } from 'vs/workbench/common/memento';
|
||||
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
@@ -21,9 +20,61 @@ import { joinPath } from 'vs/base/common/resources';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { DefaultIconPath } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { BuiltinGettingStartedCategory, BuiltinGettingStartedItem, content } from 'vs/workbench/services/gettingStarted/common/gettingStartedContent';
|
||||
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
|
||||
export const IGettingStartedService = createDecorator<IGettingStartedService>('gettingStartedService');
|
||||
|
||||
export const enum GettingStartedCategory {
|
||||
Beginner = 'Beginner',
|
||||
Intermediate = 'Intermediate',
|
||||
Advanced = 'Advanced'
|
||||
}
|
||||
|
||||
export interface IGettingStartedTask {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: GettingStartedCategory | string
|
||||
when: ContextKeyExpression
|
||||
order: number
|
||||
button:
|
||||
| { title: string, command?: never, link: string }
|
||||
| { title: string, command: string, link?: never },
|
||||
doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never }
|
||||
media: { type: 'image', path: { hc: URI, light: URI, dark: URI }, altText: string }
|
||||
}
|
||||
|
||||
export interface IGettingStartedCategoryDescriptor {
|
||||
id: GettingStartedCategory | string
|
||||
title: string
|
||||
description: string
|
||||
order: number
|
||||
icon:
|
||||
| { type: 'icon', icon: ThemeIcon }
|
||||
| { type: 'image', path: string }
|
||||
when: ContextKeyExpression
|
||||
content:
|
||||
| { type: 'items' }
|
||||
| { type: 'startEntry', command: string }
|
||||
}
|
||||
|
||||
export interface IGettingStartedCategory {
|
||||
id: GettingStartedCategory | string
|
||||
title: string
|
||||
description: string
|
||||
order: number
|
||||
icon:
|
||||
| { type: 'icon', icon: ThemeIcon }
|
||||
| { type: 'image', path: string }
|
||||
when: ContextKeyExpression
|
||||
content:
|
||||
| { type: 'items', items: IGettingStartedTask[] }
|
||||
| { type: 'startEntry', command: string }
|
||||
}
|
||||
|
||||
type TaskProgress = { done?: boolean; };
|
||||
export interface IGettingStartedTaskWithProgress extends IGettingStartedTask, Required<TaskProgress> { }
|
||||
|
||||
@@ -44,6 +95,8 @@ export interface IGettingStartedService {
|
||||
|
||||
readonly onDidAddTask: Event<IGettingStartedTaskWithProgress>
|
||||
readonly onDidAddCategory: Event<IGettingStartedCategoryWithProgress>
|
||||
readonly onDidChangeTask: Event<IGettingStartedTaskWithProgress>
|
||||
readonly onDidChangeCategory: Event<IGettingStartedCategoryWithProgress>
|
||||
|
||||
readonly onDidProgressTask: Event<IGettingStartedTaskWithProgress>
|
||||
|
||||
@@ -62,10 +115,15 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
private readonly _onDidAddCategory = new Emitter<IGettingStartedCategoryWithProgress>();
|
||||
onDidAddCategory: Event<IGettingStartedCategoryWithProgress> = this._onDidAddCategory.event;
|
||||
|
||||
private readonly _onDidChangeCategory = new Emitter<IGettingStartedCategoryWithProgress>();
|
||||
onDidChangeCategory: Event<IGettingStartedCategoryWithProgress> = this._onDidChangeCategory.event;
|
||||
|
||||
private readonly _onDidChangeTask = new Emitter<IGettingStartedTaskWithProgress>();
|
||||
onDidChangeTask: Event<IGettingStartedTaskWithProgress> = this._onDidChangeTask.event;
|
||||
|
||||
private readonly _onDidProgressTask = new Emitter<IGettingStartedTaskWithProgress>();
|
||||
onDidProgressTask: Event<IGettingStartedTaskWithProgress> = this._onDidProgressTask.event;
|
||||
|
||||
private registry = GettingStartedRegistry;
|
||||
private memento: Memento;
|
||||
private taskProgress: Record<string, TaskProgress>;
|
||||
|
||||
@@ -74,25 +132,27 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
|
||||
private trackedExtensions = new Set<string>();
|
||||
|
||||
private gettingStartedContributions = new Map<string, IGettingStartedCategory>();
|
||||
private tasks = new Map<string, IGettingStartedTask>();
|
||||
|
||||
private tasExperimentService?: ITASExperimentService;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IContextKeyService private readonly contextService: IContextKeyService,
|
||||
@IUserDataAutoSyncEnablementService readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@IProductService private readonly productService: IProductService
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@optional(ITASExperimentService) tasExperimentService: ITASExperimentService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tasExperimentService = tasExperimentService;
|
||||
|
||||
this.memento = new Memento('gettingStartedService', this.storageService);
|
||||
this.taskProgress = this.memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER);
|
||||
|
||||
this.registry.getCategories().forEach(category => {
|
||||
if (category.content.type === 'items') {
|
||||
category.content.items.forEach(task => this.registerDoneListeners(task));
|
||||
}
|
||||
});
|
||||
|
||||
this.extensionService.getExtensions().then(extensions => {
|
||||
extensions.forEach(extension => this.registerExtensionContributions(extension));
|
||||
});
|
||||
@@ -103,20 +163,100 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
});
|
||||
});
|
||||
|
||||
this._register(this.registry.onDidAddCategory(category =>
|
||||
this._onDidAddCategory.fire(this.getCategoryProgress(category))
|
||||
));
|
||||
|
||||
this._register(this.registry.onDidAddTask(task => {
|
||||
this.registerDoneListeners(task);
|
||||
this._onDidAddTask.fire(this.getTaskProgress(task));
|
||||
}));
|
||||
|
||||
this._register(this.commandService.onDidExecuteCommand(command => this.progressByCommand(command.commandId)));
|
||||
|
||||
this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => {
|
||||
if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('sync-enabled'); }
|
||||
}));
|
||||
|
||||
content.forEach(async (category, index) => {
|
||||
category = await this.getCategoryOverrides(category);
|
||||
this.registerCategory({
|
||||
...category,
|
||||
icon: { type: 'icon', icon: category.icon },
|
||||
order: index,
|
||||
when: ContextKeyExpr.deserialize(category.when) ?? ContextKeyExpr.true()
|
||||
});
|
||||
|
||||
if (category.content.type === 'items') {
|
||||
category.content.items.forEach(async (item, index) => {
|
||||
item = await this.getTaskOverrides(item, category.id);
|
||||
this.registerTask({
|
||||
...item,
|
||||
category: category.id,
|
||||
order: index,
|
||||
when: ContextKeyExpr.deserialize(item.when) ?? ContextKeyExpr.true(),
|
||||
media: {
|
||||
type: item.media.type,
|
||||
altText: item.media.altText,
|
||||
path: convertPaths(item.media.path)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getCategoryOverrides(category: BuiltinGettingStartedCategory): Promise<BuiltinGettingStartedCategory> {
|
||||
return new Promise(async (resolve) => {
|
||||
if (!this.tasExperimentService) { resolve(category); return; }
|
||||
let resolved = false;
|
||||
setTimeout(() => { resolve(category); resolved = true; }, 500);
|
||||
|
||||
const [title, description] = await Promise.all([
|
||||
this.tasExperimentService.getTreatment<string>(`gettingStarted.overrideCategory.${category.id}.title`),
|
||||
this.tasExperimentService.getTreatment<string>(`gettingStarted.overrideCategory.${category.id}.description`),
|
||||
]);
|
||||
|
||||
if (resolved) {
|
||||
const existing = assertIsDefined(this.gettingStartedContributions.get(category.id));
|
||||
existing.title = title ?? existing.title;
|
||||
existing.description = description ?? existing.description;
|
||||
console.log('hello', title, description, existing);
|
||||
this._onDidChangeCategory.fire(this.getCategoryProgress(existing));
|
||||
} else {
|
||||
resolve({
|
||||
...category,
|
||||
title: title ?? category.title,
|
||||
description: description ?? category.description,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getTaskOverrides(item: BuiltinGettingStartedItem, categoryId: string): Promise<BuiltinGettingStartedItem> {
|
||||
return new Promise(async (resolve) => {
|
||||
if (!this.tasExperimentService) { resolve(item); return; }
|
||||
let resolved = false;
|
||||
setTimeout(() => { resolve(item); resolved = true; }, 500);
|
||||
|
||||
const [title, description, media] = await Promise.all([
|
||||
this.tasExperimentService.getTreatment<string>(`gettingStarted.overrideTask.${item.id}.title`),
|
||||
this.tasExperimentService.getTreatment<string>(`gettingStarted.overrideTask.${item.id}.description`),
|
||||
this.tasExperimentService.getTreatment<string>(`gettingStarted.overrideTask.${item.id}.media`),
|
||||
]);
|
||||
|
||||
if (resolved) {
|
||||
const existingCategory = assertIsDefined(this.gettingStartedContributions.get(categoryId));
|
||||
if (existingCategory.content.type === 'startEntry') { throw Error('Unexpected content type'); }
|
||||
const existingItem = assertIsDefined(existingCategory.content.items.find(_item => _item.id === item.id));
|
||||
existingItem.title = title ?? existingItem.title;
|
||||
existingItem.description = description ?? existingItem.description;
|
||||
existingItem.media.path = media ? convertPaths(media) : existingItem.media.path;
|
||||
this._onDidChangeTask.fire(this.getTaskProgress(existingItem));
|
||||
} else {
|
||||
resolve({
|
||||
...item,
|
||||
title: title ?? item.title,
|
||||
description: description ?? item.description,
|
||||
media: {
|
||||
altText: item.media.altText,
|
||||
path: media ? media : item.media.path,
|
||||
type: item.media.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private registerExtensionContributions(extension: IExtensionDescription) {
|
||||
@@ -147,11 +287,12 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
|
||||
extension.contributes?.walkthroughs?.forEach(section => {
|
||||
const categoryID = extension.identifier.value + '#' + section.id;
|
||||
this.registry.registerCategory({
|
||||
this.registerCategory({
|
||||
content: { type: 'items' },
|
||||
description: section.description,
|
||||
title: section.title,
|
||||
id: categoryID,
|
||||
order: Math.min(),
|
||||
icon: {
|
||||
type: 'image',
|
||||
path: extension.icon
|
||||
@@ -161,9 +302,8 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
when: ContextKeyExpr.deserialize(section.when) ?? ContextKeyExpr.true(),
|
||||
});
|
||||
try {
|
||||
|
||||
section.tasks.forEach((task, index) =>
|
||||
this.registry.registerTask({
|
||||
this.registerTask({
|
||||
button: task.button,
|
||||
description: task.description,
|
||||
media: { type: 'image', altText: task.media.altText, path: convertPaths(task.media.path) },
|
||||
@@ -203,8 +343,9 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
}
|
||||
|
||||
getCategories(): IGettingStartedCategoryWithProgress[] {
|
||||
const registeredCategories = this.registry.getCategories();
|
||||
const registeredCategories = [...this.gettingStartedContributions.values()];
|
||||
const categoriesWithCompletion = registeredCategories
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.filter(category => this.contextService.contextMatchesRules(category.when))
|
||||
.map(category => {
|
||||
if (category.content.type === 'items') {
|
||||
@@ -256,7 +397,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
if (!oldProgress || oldProgress.done !== true) {
|
||||
this.taskProgress[id] = { done: true };
|
||||
this.memento.saveMemento();
|
||||
const task = this.registry.getTask(id);
|
||||
const task = this.getTask(id);
|
||||
this._onDidProgressTask.fire(this.getTaskProgress(task));
|
||||
}
|
||||
}
|
||||
@@ -264,7 +405,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
deprogressTask(id: string) {
|
||||
delete this.taskProgress[id];
|
||||
this.memento.saveMemento();
|
||||
const task = this.registry.getTask(id);
|
||||
const task = this.getTask(id);
|
||||
this._onDidProgressTask.fire(this.getTaskProgress(task));
|
||||
}
|
||||
|
||||
@@ -277,8 +418,61 @@ export class GettingStartedService extends Disposable implements IGettingStarted
|
||||
const listening = this.eventListeners.get(event) ?? [];
|
||||
listening.forEach(id => this.progressTask(id));
|
||||
}
|
||||
|
||||
public registerTask(task: IGettingStartedTask): IGettingStartedTask {
|
||||
const category = this.gettingStartedContributions.get(task.category);
|
||||
if (!category) { throw Error('Registering getting started task to category that does not exist (' + task.category + ')'); }
|
||||
if (category.content.type !== 'items') { throw Error('Registering getting started task to category that is not of `items` type (' + task.category + ')'); }
|
||||
if (this.tasks.has(task.id)) { throw Error('Attempting to register task with id ' + task.id + ' twice. Second is dropped.'); }
|
||||
this.tasks.set(task.id, task);
|
||||
const insertIndex = category.content.items.findIndex(item => item.order > task.order);
|
||||
category.content.items.splice(insertIndex, 0, task);
|
||||
this.registerDoneListeners(task);
|
||||
this._onDidAddTask.fire(this.getTaskProgress(task));
|
||||
return task;
|
||||
}
|
||||
|
||||
public registerCategory(categoryDescriptor: IGettingStartedCategoryDescriptor): void {
|
||||
const oldCategory = this.gettingStartedContributions.get(categoryDescriptor.id);
|
||||
if (oldCategory) {
|
||||
console.error(`Skipping attempt to overwrite getting started category. (${categoryDescriptor})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const category: IGettingStartedCategory = {
|
||||
...categoryDescriptor,
|
||||
content: categoryDescriptor.content.type === 'items'
|
||||
? { type: 'items', items: [] }
|
||||
: categoryDescriptor.content
|
||||
};
|
||||
|
||||
this.gettingStartedContributions.set(categoryDescriptor.id, category);
|
||||
this._onDidAddCategory.fire(this.getCategoryProgress(category));
|
||||
}
|
||||
|
||||
private getTask(id: string): IGettingStartedTask {
|
||||
const task = this.tasks.get(id);
|
||||
if (!task) { throw Error('Attempting to access task which does not exist in registry ' + id); }
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
const convertPaths = (path: string | { hc: string, dark: string, light: string }): { hc: URI, dark: URI, light: URI } => {
|
||||
const convertPath = (path: string) => path.startsWith('https://')
|
||||
? URI.parse(path, true)
|
||||
: FileAccess.asBrowserUri('vs/workbench/services/gettingStarted/common/media/' + path, require);
|
||||
if (typeof path === 'string') {
|
||||
const converted = convertPath(path);
|
||||
return { hc: converted, dark: converted, light: converted };
|
||||
} else {
|
||||
return {
|
||||
hc: convertPath(path.hc),
|
||||
light: convertPath(path.light),
|
||||
dark: convertPath(path.dark)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
|
||||
@@ -66,8 +66,6 @@ export interface IFileWorkingCopyModel {
|
||||
* Snapshots the model's current content for writing. This must include
|
||||
* any changes that were made to the model that are in memory.
|
||||
*
|
||||
* TODO@bpasero could this method return sync and not promise?
|
||||
*
|
||||
* @param token support for cancellation
|
||||
*/
|
||||
snapshot(token: CancellationToken): Promise<VSBufferReadableStream>;
|
||||
@@ -89,7 +87,7 @@ export interface IFileWorkingCopyModel {
|
||||
* case of undo-redo.
|
||||
*
|
||||
* TODO@bpasero should find a better name here maybe together
|
||||
* with the pushStackElement concept since this is around
|
||||
* with the `pushStackElement` concept since this is around
|
||||
* undo/redo?
|
||||
*/
|
||||
getAlternativeVersionId(): number;
|
||||
@@ -102,7 +100,9 @@ export interface IFileWorkingCopyModel {
|
||||
* save is triggered so that the user can always undo back
|
||||
* to the state before saving.
|
||||
*
|
||||
* TODO@bpasero rename to beforeSave()?
|
||||
* TODO@bpasero should find a better name here maybe together
|
||||
* with the `getAlternativeVersionId` concept since this is around
|
||||
* undo/redo?
|
||||
*/
|
||||
pushStackElement(): void;
|
||||
}
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ All unit tests are run inside a electron-browser environment which access to DOM
|
||||
|
||||
- use the `--debug` to see an electron window with dev tools which allows for debugging
|
||||
- to run only a subset of tests use the `--run` or `--glob` options
|
||||
- use `yarn watch` to automatically compile changes
|
||||
|
||||
For instance, `./scripts/test.sh --debug --glob **/extHost*.test.js` runs all tests from `extHost`-files and enables you to debug them.
|
||||
|
||||
@@ -24,7 +25,7 @@ Unit tests from layers `common` and `browser` are run inside `chromium`, `webkit
|
||||
|
||||
## Run (with node)
|
||||
|
||||
yarn run mocha --run src/vs/editor/test/browser/controller/cursor.test.ts
|
||||
yarn run mocha --ui tdd --run src/vs/editor/test/browser/controller/cursor.test.ts
|
||||
|
||||
|
||||
## Coverage
|
||||
|
||||
Reference in New Issue
Block a user