Vscode merge (#4582)

* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd

* fix issues with merges

* bump node version in azpipe

* replace license headers

* remove duplicate launch task

* fix build errors

* fix build errors

* fix tslint issues

* working through package and linux build issues

* more work

* wip

* fix packaged builds

* working through linux build errors

* wip

* wip

* wip

* fix mac and linux file limits

* iterate linux pipeline

* disable editor typing

* revert series to parallel

* remove optimize vscode from linux

* fix linting issues

* revert testing change

* add work round for new node

* readd packaging for extensions

* fix issue with angular not resolving decorator dependencies
This commit is contained in:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="#C5C5C5"><path d="M12.714 9.603c-.07.207-.15.407-.246.601l1.017 2.139c-.335.424-.718.807-1.142 1.143l-2.14-1.018c-.193.097-.394.176-.601.247l-.795 2.235c-.265.03-.534.05-.807.05-.272 0-.541-.02-.806-.05l-.795-2.235c-.207-.071-.408-.15-.602-.247l-2.14 1.017c-.424-.336-.807-.719-1.143-1.143l1.017-2.139c-.094-.193-.175-.393-.245-.6l-2.236-.796c-.03-.265-.05-.534-.05-.807s.02-.542.05-.807l2.236-.795c.07-.207.15-.407.246-.601l-1.016-2.139c.336-.423.719-.807 1.143-1.142l2.14 1.017c.193-.096.394-.176.602-.247l.793-2.236c.265-.03.534-.05.806-.05.273 0 .542.02.808.05l.795 2.236c.207.07.407.15.601.246l2.14-1.017c.424.335.807.719 1.142 1.142l-1.017 2.139c.096.194.176.394.246.601l2.236.795c.029.266.049.535.049.808s-.02.542-.05.807l-2.236.796zm-4.714-4.603c-1.657 0-3 1.343-3 3s1.343 3 3 3 3-1.343 3-3-1.343-3-3-3z"/><circle cx="8" cy="8" r="1.5"/></g></svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="#424242"><path d="M12.714 9.603c-.07.207-.15.407-.246.601l1.017 2.139c-.335.424-.718.807-1.142 1.143l-2.14-1.018c-.193.097-.394.176-.601.247l-.795 2.235c-.265.03-.534.05-.807.05-.272 0-.541-.02-.806-.05l-.795-2.235c-.207-.071-.408-.15-.602-.247l-2.14 1.017c-.424-.336-.807-.719-1.143-1.143l1.017-2.139c-.094-.193-.175-.393-.245-.6l-2.236-.796c-.03-.265-.05-.534-.05-.807s.02-.542.05-.807l2.236-.795c.07-.207.15-.407.246-.601l-1.016-2.139c.336-.423.719-.807 1.143-1.142l2.14 1.017c.193-.096.394-.176.602-.247l.793-2.236c.265-.03.534-.05.806-.05.273 0 .542.02.808.05l.795 2.236c.207.07.407.15.601.246l2.14-1.017c.424.335.807.719 1.142 1.142l-1.017 2.139c.096.194.176.394.246.601l2.236.795c.029.266.049.535.049.808s-.02.542-.05.807l-2.236.796zm-4.714-4.603c-1.657 0-3 1.343-3 3s1.343 3 3 3 3-1.343 3-3-1.343-3-3-3z"/><circle cx="8" cy="8" r="1.5"/></g></svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#C5C5C5;}
</style>
<path class="st0" d="M10,3V2c0-0.6-0.4-1-1-1H6C5.4,1,5,1.4,5,2v1H2v1h1v10c0,0.6,0.4,1,1,1h7c0.6,0,1-0.4,1-1V4h1V3H10z M6,12H5V6
h1V12z M6,2h3v1H6V2z M8,12H7V6h1V12z M10,12H9V6h1V12z"/>
</svg>

After

Width:  |  Height:  |  Size: 592 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#424242;}
</style>
<path class="st0" d="M10,3V2c0-0.6-0.4-1-1-1H6C5.4,1,5,1.4,5,2v1H2v1h1v10c0,0.6,0.4,1,1,1h7c0.6,0,1-0.4,1-1V4h1V3H10z M6,12H5V6
h1V12z M6,2h3v1H6V2z M8,12H7V6h1V12z M10,12H9V6h1V12z"/>
</svg>

After

Width:  |  Height:  |  Size: 592 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.714 2H6.286v12h3.428V2z" fill="#C5C5C5"/><path d="M14 6.286H2v3.428h12V6.286z" fill="#C5C5C5"/></svg>

After

Width:  |  Height:  |  Size: 208 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 1H6v14h4V1z" fill="#424242"/><path d="M15 6H1v4h14V6z" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 185 B

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-workbench .panel.integrated-terminal .xterm-viewport {
/* Use the hack presented in http://stackoverflow.com/a/38748186/1156119 to get opacity transitions working on the scrollbar */
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
transition: background-color 800ms linear;
}
.monaco-workbench .panel.integrated-terminal .xterm-viewport::-webkit-scrollbar {
width: 10px;
}
.monaco-workbench .panel.integrated-terminal .xterm-viewport::-webkit-scrollbar-track {
opacity: 0;
}
.monaco-workbench .panel.integrated-terminal .xterm-viewport::-webkit-scrollbar-thumb {
min-height: 20px;
background-color: inherit;
}
.monaco-workbench .panel.integrated-terminal .find-focused .xterm .xterm-viewport,
.monaco-workbench .panel.integrated-terminal .xterm.focus .xterm-viewport,
.monaco-workbench .panel.integrated-terminal .xterm:focus .xterm-viewport,
.monaco-workbench .panel.integrated-terminal .xterm:hover .xterm-viewport {
transition: opacity 100ms linear;
cursor: default;
}
.monaco-workbench .panel.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
transition: opacity 0ms linear;
}
.monaco-workbench .panel.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive {
background-color: inherit;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 15H0V1h16v14z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M1 2v12h14V2H1zm13 11H2v-3h12v3zm0-5H2V5h12v3z" id="iconBg"/><g id="iconFg" style="display: none;"><path class="icon-vs-fg" d="M14 8H2V5h12v3zm0 2H2v3h12v-3z"/></g></svg>

After

Width:  |  Height:  |  Size: 578 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 15H0V1h16v14z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M1 2v12h14V2H1zm13 11H2v-3h12v3zm0-5H2V5h12v3z" id="iconBg"/><g id="iconFg" style="display: none;"><path class="icon-vs-fg" d="M14 8H2V5h12v3zm0 2H2v3h12v-3z"/></g></svg>

After

Width:  |  Height:  |  Size: 578 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -1 16 16" enable-background="new 0 -1 16 16"><path fill="#C5C5C5" d="M1 1v12h14v-12h-14zm1 3h4.999v8h-4.999v-8zm12 8h-5.001v-8h5.001v8z"/></svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -1 16 16" enable-background="new 0 -1 16 16"><path fill="#424242" d="M1 1v12h14v-12h-14zm1 3h4.999v8h-4.999v-8zm12 8h-5.001v-8h5.001v8z"/></svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@@ -0,0 +1,172 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-workbench .panel.integrated-terminal {
align-content: flex-start;
align-items: baseline;
display: flex;
flex-direction: column;
background-color: transparent!important;
user-select: initial;
position: relative;
}
.monaco-workbench .panel.integrated-terminal .terminal-outer-container {
height: 100%;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.monaco-workbench .panel.integrated-terminal .terminal-tab {
height: 100%;
}
.monaco-workbench .panel.integrated-terminal .terminal-wrapper {
display: none;
margin: 0 10px;
}
.monaco-workbench .panel.integrated-terminal .terminal-wrapper.active {
display: block;
position: absolute;
bottom: 2px; /* Matches padding-bottom on .terminal-outer-container */
top: 0;
}
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.horizontal .split-view-view:first-child .terminal-wrapper {
margin-left: 20px;
}
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.horizontal .split-view-view:last-child .terminal-wrapper {
margin-right: 20px;
}
.monaco-workbench .panel.integrated-terminal .xterm a:not(.xterm-invalid-link) {
/* To support message box sizing */
position: relative;
}
.monaco-workbench .panel.integrated-terminal .terminal-wrapper > div {
height: 100%;
}
.monaco-workbench .panel.integrated-terminal .xterm-viewport {
margin-right: -10px;
}
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.horizontal .split-view-view:last-child .xterm-viewport {
margin-right: -20px;
}
.monaco-workbench .panel.integrated-terminal canvas {
/* Align the viewport and canvases to the bottom of the panel */
position: absolute;
right: -20px;
bottom: 0;
left: 0;
/* Disable upstream's style */
top: auto;
}
.monaco-workbench .panel.integrated-terminal {
font-variant-ligatures: none;
}
.monaco-workbench .panel.integrated-terminal .split-view-view {
/* Make relative as terminal absolute positioning needs it deeper in the tree */
position: relative;
box-sizing: border-box;
}
/* border-color is set by theme key terminal.border */
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:first-child) {
border-left-width: 1px;
border-left-style: solid;
}
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.vertical .split-view-view:not(:first-child) {
border-top-width: 1px;
border-top-style: solid;
}
.monaco-workbench .panel.integrated-terminal.enable-ligatures {
font-variant-ligatures: normal;
}
.monaco-workbench .panel.integrated-terminal.disable-bold .xterm-bold {
font-weight: normal !important;
}
/* Use the default cursor when alt is active to help with clicking to move cursor */
.monaco-workbench .panel.integrated-terminal .terminal-outer-container.alt-active .xterm {
cursor: default;
}
.monaco-workbench .panel.integrated-terminal .xterm {
position: absolute;
bottom: 0;
left: 0;
user-select: none;
}
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.vertical .split-view-view:not(:last-child) .xterm {
/* When vertical and NOT the bottom terminal, align to the top instead to prevent the output jumping around erratically */
top: 0;
bottom: auto;
}
.monaco-workbench .panel.integrated-terminal .xterm:focus {
/* Hide outline when focus jumps from xterm to the text area */
outline: none;
}
.hc-black .monaco-workbench .panel.integrated-terminal .xterm.focus::before,
.hc-black .monaco-workbench .panel.integrated-terminal .xterm:focus::before {
display: block;
content: "";
border: 1px solid;
position: absolute;
left: -5px;
top: 0;
right: -5px;
bottom: 0;
z-index: 10;
}
.hc-black .monaco-workbench .panel.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:only-child) .xterm.focus::before,
.hc-black .monaco-workbench .panel.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:only-child) .xterm:focus::before {
right: 0;
}
.monaco-workbench .panel.integrated-terminal .xterm .xterm-helpers {
position: absolute;
top: 0;
}
.monaco-workbench .panel.integrated-terminal .xterm .xterm-helper-textarea:focus {
/* Override the general vscode style applies `opacity:1!important` to textareas */
opacity: 0 !important;
}
/* Light theme */
.monaco-workbench .terminal-action.kill { background: url('kill.svg') center center no-repeat; }
.monaco-workbench .terminal-action.new { background: url('new.svg') center center no-repeat; }
.monaco-workbench .terminal-action.split { background: url('split.svg') center center no-repeat; }
.monaco-workbench .panel.right .terminal-action.split { background: url('split-horizontal.svg') center center no-repeat; }
/* Dark theme / HC theme */
.vs-dark .monaco-workbench .terminal-action.kill, .hc-black .monaco-workbench .terminal-action.kill { background: url('kill-inverse.svg') center center no-repeat; }
.vs-dark .monaco-workbench .terminal-action.new, .hc-black .monaco-workbench .terminal-action.new { background: url('new-inverse.svg') center center no-repeat; }
.vs-dark .monaco-workbench .terminal-action.split, .hc-black .monaco-workbench .terminal-action.split { background: url('split-inverse.svg') center center no-repeat; }
.vs-dark .monaco-workbench .panel.right .terminal-action.split, .hc-black .monaco-workbench .panel.right .terminal-action.split { background: url('split-horizontal-inverse.svg') center center no-repeat; }
.vs-dark .monaco-workbench.mac .panel.integrated-terminal .terminal-outer-container:not(.alt-active) .terminal:not(.enable-mouse-events),
.hc-black .monaco-workbench.mac .panel.integrated-terminal .terminal-outer-container:not(.alt-active) .terminal:not(.enable-mouse-events) {
cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text;
}
.monaco-workbench .quick-open-terminal-configure {
background-image: url('configure.svg');
}
.vs-dark .monaco-workbench .quick-open-terminal-configure,
.hc-black .monaco-workbench .quick-open-terminal-configure {
background-image: url('configure-inverse.svg');
}

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-workbench .terminal-widget-overlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.monaco-workbench .terminal-message-widget {
font-size: 12px;
line-height: 19px;
padding: 4px 5px;
animation: fadein 100ms linear;
white-space: nowrap;
/* Must be drawn on the top of the terminal's canvases */
z-index: 20;
}

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (Source EULA)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 10;
}
.xterm .xterm-helper-textarea {
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -10;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm:not(.enable-mouse-events) {
cursor: text;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 100;
color: transparent;
}
.xterm .xterm-accessibility-tree:focus [id^="xterm-active-item-"] {
outline: 1px solid #F80;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-cursor-pointer {
cursor: pointer !important;
}
.xterm.xterm-cursor-crosshair {
/* Column selection mode */
cursor: crosshair !important;
}

View File

@@ -0,0 +1,519 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import * as platform from 'vs/base/common/platform';
import 'vs/css!./media/scrollbar';
import 'vs/css!./media/terminal';
import 'vs/css!./media/widgets';
import 'vs/css!./media/xterm';
import * as nls from 'vs/nls';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as ActionBarExtensions, IActionBarRegistry, Scope } from 'vs/workbench/browser/actions';
import * as panel from 'vs/workbench/browser/panel';
import { getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen';
import { Extensions as QuickOpenExtensions, IQuickOpenRegistry, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen';
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { AllowWorkspaceShellTerminalCommand, ClearSelectionTerminalAction, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewInActiveWorkspaceTerminalAction, CreateNewTerminalAction, DeleteToLineStartTerminalAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, DisallowWorkspaceShellTerminalCommand, FindNext, FindPrevious, FocusActiveTerminalAction, FocusNextPaneTerminalAction, FocusNextTerminalAction, FocusPreviousPaneTerminalAction, FocusPreviousTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, KillTerminalAction, MoveToLineEndTerminalAction, MoveToLineStartTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, RenameTerminalAction, ResizePaneDownTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, RunActiveFileInTerminalAction, RunSelectedTextInTerminalAction, ScrollDownPageTerminalAction, ScrollDownTerminalAction, ScrollToBottomTerminalAction, ScrollToNextCommandAction, ScrollToPreviousCommandAction, ScrollToTopTerminalAction, ScrollUpPageTerminalAction, ScrollUpTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SelectToNextCommandAction, SelectToNextLineAction, SelectToPreviousCommandAction, SelectToPreviousLineAction, SendSequenceTerminalCommand, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, TERMINAL_PICKER_PREFIX, ToggleCaseSensitiveCommand, ToggleEscapeSequenceLoggingAction, ToggleRegexCommand, ToggleTerminalAction, ToggleWholeWordCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { TerminalPanel } from 'vs/workbench/contrib/terminal/browser/terminalPanel';
import { TerminalPickerHandler } from 'vs/workbench/contrib/terminal/browser/terminalQuickOpen';
import { KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle } from 'vs/workbench/contrib/terminal/common/terminal';
import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { setupTerminalCommands, TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminalCommands';
import { setupTerminalMenu } from 'vs/workbench/contrib/terminal/common/terminalMenu';
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { DEFAULT_COMMANDS_TO_SKIP_SHELL } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
const quickOpenRegistry = (Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen));
const inTerminalsPicker = 'inTerminalPicker';
quickOpenRegistry.registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
TerminalPickerHandler,
TerminalPickerHandler.ID,
TERMINAL_PICKER_PREFIX,
inTerminalsPicker,
nls.localize('quickOpen.terminal', "Show All Opened Terminals")
)
);
const quickOpenNavigateNextInTerminalPickerId = 'workbench.action.quickOpenNavigateNextInTerminalPicker';
CommandsRegistry.registerCommand(
{ id: quickOpenNavigateNextInTerminalPickerId, handler: getQuickNavigateHandler(quickOpenNavigateNextInTerminalPickerId, true) });
const quickOpenNavigatePreviousInTerminalPickerId = 'workbench.action.quickOpenNavigatePreviousInTerminalPicker';
CommandsRegistry.registerCommand(
{ id: quickOpenNavigatePreviousInTerminalPickerId, handler: getQuickNavigateHandler(quickOpenNavigatePreviousInTerminalPickerId, false) });
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration({
id: 'terminal',
order: 100,
title: nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
type: 'object',
properties: {
'terminal.integrated.shellArgs.linux': {
markdownDescription: nls.localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
type: 'array',
items: {
type: 'string'
},
default: []
},
'terminal.integrated.shellArgs.osx': {
markdownDescription: nls.localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
type: 'array',
items: {
type: 'string'
},
// Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This
// is the reason terminals on macOS typically run login shells by default which set up
// the environment. See http://unix.stackexchange.com/a/119675/115410
default: ['-l']
},
'terminal.integrated.shellArgs.windows': {
markdownDescription: nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
'anyOf': [
{
type: 'array',
items: {
type: 'string',
markdownDescription: nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")
},
},
{
type: 'string',
markdownDescription: nls.localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")
}
],
default: []
},
'terminal.integrated.macOptionIsMeta': {
description: nls.localize('terminal.integrated.macOptionIsMeta', "Controls whether to treat the option key as the meta key in the terminal on macOS."),
type: 'boolean',
default: false
},
'terminal.integrated.macOptionClickForcesSelection': {
description: nls.localize('terminal.integrated.macOptionClickForcesSelection', "Controls whether to force selection when using Option+click on macOS. This will force a regular (line) selection and disallow the use of column selection mode. This enables copying and pasting using the regular terminal selection, for example, when mouse mode is enabled in tmux."),
type: 'boolean',
default: false
},
'terminal.integrated.copyOnSelection': {
description: nls.localize('terminal.integrated.copyOnSelection', "Controls whether text selected in the terminal will be copied to the clipboard."),
type: 'boolean',
default: false
},
'terminal.integrated.drawBoldTextInBrightColors': {
description: nls.localize('terminal.integrated.drawBoldTextInBrightColors', "Controls whether bold text in the terminal will always use the \"bright\" ANSI color variant."),
type: 'boolean',
default: true
},
'terminal.integrated.fontFamily': {
markdownDescription: nls.localize('terminal.integrated.fontFamily', "Controls the font family of the terminal, this defaults to `#editor.fontFamily#`'s value."),
type: 'string'
},
// TODO: Support font ligatures
// 'terminal.integrated.fontLigatures': {
// 'description': nls.localize('terminal.integrated.fontLigatures', "Controls whether font ligatures are enabled in the terminal."),
// 'type': 'boolean',
// 'default': false
// },
'terminal.integrated.fontSize': {
description: nls.localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."),
type: 'number',
default: EDITOR_FONT_DEFAULTS.fontSize
},
'terminal.integrated.letterSpacing': {
description: nls.localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."),
type: 'number',
default: DEFAULT_LETTER_SPACING
},
'terminal.integrated.lineHeight': {
description: nls.localize('terminal.integrated.lineHeight', "Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels."),
type: 'number',
default: DEFAULT_LINE_HEIGHT
},
'terminal.integrated.fontWeight': {
type: 'string',
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
description: nls.localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."),
default: 'normal'
},
'terminal.integrated.fontWeightBold': {
type: 'string',
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
description: nls.localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."),
default: 'bold'
},
'terminal.integrated.cursorBlinking': {
description: nls.localize('terminal.integrated.cursorBlinking', "Controls whether the terminal cursor blinks."),
type: 'boolean',
default: false
},
'terminal.integrated.cursorStyle': {
description: nls.localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."),
enum: [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE],
default: TerminalCursorStyle.BLOCK
},
'terminal.integrated.scrollback': {
description: nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."),
type: 'number',
default: 1000
},
'terminal.integrated.setLocaleVariables': {
markdownDescription: nls.localize('terminal.integrated.setLocaleVariables', "Controls whether locale variables are set at startup of the terminal."),
type: 'boolean',
default: true
},
'terminal.integrated.rendererType': {
type: 'string',
enum: ['auto', 'canvas', 'dom'],
enumDescriptions: [
nls.localize('terminal.integrated.rendererType.auto', "Let VS Code guess which renderer to use."),
nls.localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer"),
nls.localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer.")
],
default: 'auto',
description: nls.localize('terminal.integrated.rendererType', "Controls how the terminal is rendered.")
},
'terminal.integrated.rightClickBehavior': {
type: 'string',
enum: ['default', 'copyPaste', 'selectWord'],
enumDescriptions: [
nls.localize('terminal.integrated.rightClickBehavior.default', "Show the context menu."),
nls.localize('terminal.integrated.rightClickBehavior.copyPaste', "Copy when there is a selection, otherwise paste."),
nls.localize('terminal.integrated.rightClickBehavior.selectWord', "Select the word under the cursor and show the context menu.")
],
default: platform.isMacintosh ? 'selectWord' : platform.isWindows ? 'copyPaste' : 'default',
description: nls.localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click.")
},
'terminal.integrated.cwd': {
description: nls.localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."),
type: 'string',
default: undefined
},
'terminal.integrated.confirmOnExit': {
description: nls.localize('terminal.integrated.confirmOnExit', "Controls whether to confirm on exit if there are active terminal sessions."),
type: 'boolean',
default: false
},
'terminal.integrated.enableBell': {
description: nls.localize('terminal.integrated.enableBell', "Controls whether the terminal bell is enabled."),
type: 'boolean',
default: false
},
'terminal.integrated.commandsToSkipShell': {
description: nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open.\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')),
type: 'array',
items: {
type: 'string'
},
default: []
},
'terminal.integrated.env.osx': {
markdownDescription: nls.localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the VS Code process to be used by the terminal on macOS. Set to `null` to delete the environment variable."),
type: 'object',
additionalProperties: {
type: ['string', 'null']
},
default: {}
},
'terminal.integrated.env.linux': {
markdownDescription: nls.localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux. Set to `null` to delete the environment variable."),
type: 'object',
additionalProperties: {
type: ['string', 'null']
},
default: {}
},
'terminal.integrated.env.windows': {
markdownDescription: nls.localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows. Set to `null` to delete the environment variable."),
type: 'object',
additionalProperties: {
type: ['string', 'null']
},
default: {}
},
'terminal.integrated.showExitAlert': {
description: nls.localize('terminal.integrated.showExitAlert', "Controls whether to show the alert \"The terminal process terminated with exit code\" when exit code is non-zero."),
type: 'boolean',
default: true
},
'terminal.integrated.splitCwd': {
description: nls.localize('terminal.integrated.splitCwd', "Controls the working directory a split terminal starts with."),
type: 'string',
enum: ['workspaceRoot', 'initial', 'inherited'],
enumDescriptions: [
nls.localize('terminal.integrated.splitCwd.workspaceRoot', "A new split terminal will use the workspace root as the working directory. In a multi-root workspace a choice for which root folder to use is offered."),
nls.localize('terminal.integrated.splitCwd.initial', "A new split terminal will use the working directory that the parent terminal started with."),
nls.localize('terminal.integrated.splitCwd.inherited', "On macOS and Linux, a new split terminal will use the working directory of the parent terminal. On Windows, this behaves the same as initial."),
],
default: 'inherited'
},
'terminal.integrated.windowsEnableConpty': {
description: nls.localize('terminal.integrated.windowsEnableConpty', "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false."),
type: 'boolean',
default: false
}
}
});
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenTermAction, QuickOpenTermAction.ID, QuickOpenTermAction.LABEL), 'Terminal: Switch Active Terminal', nls.localize('terminal', "Terminal"));
const actionBarRegistry = Registry.as<IActionBarRegistry>(ActionBarExtensions.Actionbar);
actionBarRegistry.registerActionBarContributor(Scope.VIEWER, QuickOpenActionTermContributor);
(<panel.PanelRegistry>Registry.as(panel.Extensions.Panels)).registerPanel(new panel.PanelDescriptor(
TerminalPanel,
TERMINAL_PANEL_ID,
nls.localize('terminal', "Terminal"),
'terminal',
40,
TERMINAL_COMMAND_ID.TOGGLE
));
// On mac cmd+` is reserved to cycle between windows, that's why the keybindings use WinCtrl
const category = nls.localize('terminalCategory', "Terminal");
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.LABEL), 'Terminal: Kill the Active Terminal Instance', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C }
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FOCUS)), 'Terminal: Copy Selection', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKTICK,
mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_BACKTICK }
}), 'Terminal: Create New Integrated Terminal', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearSelectionTerminalAction, ClearSelectionTerminalAction.ID, ClearSelectionTerminalAction.LABEL, {
primary: KeyCode.Escape,
linux: { primary: KeyCode.Escape }
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE)), 'Terminal: Escape selection', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewInActiveWorkspaceTerminalAction, CreateNewInActiveWorkspaceTerminalAction.ID, CreateNewInActiveWorkspaceTerminalAction.LABEL), 'Terminal: Create New Integrated Terminal (In Active Workspace)', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusActiveTerminalAction, FocusActiveTerminalAction.ID, FocusActiveTerminalAction.LABEL), 'Terminal: Focus Terminal', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextTerminalAction, FocusNextTerminalAction.ID, FocusNextTerminalAction.LABEL), 'Terminal: Focus Next Terminal', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousTerminalAction, FocusPreviousTerminalAction.ID, FocusPreviousTerminalAction.LABEL), 'Terminal: Focus Previous Terminal', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(TerminalPasteAction, TerminalPasteAction.ID, TerminalPasteAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V },
// Don't apply to Mac since cmd+v works
mac: { primary: 0 }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Paste into Active Terminal', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectAllTerminalAction, SelectAllTerminalAction.ID, SelectAllTerminalAction.LABEL, {
// Don't use ctrl+a by default as that would override the common go to start
// of prompt shell binding
primary: 0,
// Technically this doesn't need to be here as it will fall back to this
// behavior anyway when handed to xterm.js, having this handled by VS Code
// makes it easier for users to see how it works though.
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_A }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select All', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(RunSelectedTextInTerminalAction, RunSelectedTextInTerminalAction.ID, RunSelectedTextInTerminalAction.LABEL), 'Terminal: Run Selected Text In Active Terminal', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(RunActiveFileInTerminalAction, RunActiveFileInTerminalAction.ID, RunActiveFileInTerminalAction.LABEL), 'Terminal: Run Active File In Active Terminal', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleTerminalAction, ToggleTerminalAction.ID, ToggleTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.US_BACKTICK,
mac: { primary: KeyMod.WinCtrl | KeyCode.US_BACKTICK }
}), 'View: Toggle Integrated Terminal', nls.localize('viewCategory', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollDownTerminalAction, ScrollDownTerminalAction.ID, ScrollDownTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Down (Line)', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollDownPageTerminalAction, ScrollDownPageTerminalAction.ID, ScrollDownPageTerminalAction.LABEL, {
primary: KeyMod.Shift | KeyCode.PageDown,
mac: { primary: KeyCode.PageDown }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Down (Page)', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToBottomTerminalAction, ScrollToBottomTerminalAction.ID, ScrollToBottomTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.End,
linux: { primary: KeyMod.Shift | KeyCode.End }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll to Bottom', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollUpTerminalAction, ScrollUpTerminalAction.ID, ScrollUpTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow },
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Up (Line)', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollUpPageTerminalAction, ScrollUpPageTerminalAction.ID, ScrollUpPageTerminalAction.LABEL, {
primary: KeyMod.Shift | KeyCode.PageUp,
mac: { primary: KeyCode.PageUp }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll Up (Page)', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToTopTerminalAction, ScrollToTopTerminalAction.ID, ScrollToTopTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.Home,
linux: { primary: KeyMod.Shift | KeyCode.Home }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll to Top', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_K }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category);
if (platform.isWindows) {
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectDefaultShellWindowsTerminalAction, SelectDefaultShellWindowsTerminalAction.ID, SelectDefaultShellWindowsTerminalAction.LABEL), 'Terminal: Select Default Shell', category);
}
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(AllowWorkspaceShellTerminalCommand, AllowWorkspaceShellTerminalCommand.ID, AllowWorkspaceShellTerminalCommand.LABEL), 'Terminal: Allow Workspace Shell Configuration', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DisallowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand.ID, DisallowWorkspaceShellTerminalCommand.LABEL), 'Terminal: Disallow Workspace Shell Configuration', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(RenameTerminalAction, RenameTerminalAction.ID, RenameTerminalAction.LABEL), 'Terminal: Rename', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalFindWidgetAction, FocusTerminalFindWidgetAction.ID, FocusTerminalFindWidgetAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.KEY_F
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Find Widget', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalFindWidgetAction, FocusTerminalFindWidgetAction.ID, FocusTerminalFindWidgetAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.KEY_F
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Focus Find Widget', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(HideTerminalFindWidgetAction, HideTerminalFindWidgetAction.ID, HideTerminalFindWidgetAction.LABEL, {
primary: KeyCode.Escape,
secondary: [KeyMod.Shift | KeyCode.Escape]
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE)), 'Terminal: Hide Find Widget', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DeleteWordLeftTerminalAction, DeleteWordLeftTerminalAction.ID, DeleteWordLeftTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
mac: { primary: KeyMod.Alt | KeyCode.Backspace }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Delete Word Left', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DeleteWordRightTerminalAction, DeleteWordRightTerminalAction.ID, DeleteWordRightTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.Delete,
mac: { primary: KeyMod.Alt | KeyCode.Delete }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Delete Word Right', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DeleteToLineStartTerminalAction, DeleteToLineStartTerminalAction.ID, DeleteToLineStartTerminalAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Delete To Line Start', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(MoveToLineStartTerminalAction, MoveToLineStartTerminalAction.ID, MoveToLineStartTerminalAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.LeftArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Move To Line Start', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(MoveToLineEndTerminalAction, MoveToLineEndTerminalAction.ID, MoveToLineEndTerminalAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.RightArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Move To Line End', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_5],
mac: {
primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH,
secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_5]
}
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SplitInActiveWorkspaceTerminalAction, SplitInActiveWorkspaceTerminalAction.ID, SplitInActiveWorkspaceTerminalAction.LABEL), 'Terminal: Split Terminal (In Active Workspace)', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousPaneTerminalAction, FocusPreviousPaneTerminalAction.ID, FocusPreviousPaneTerminalAction.LABEL, {
primary: KeyMod.Alt | KeyCode.LeftArrow,
secondary: [KeyMod.Alt | KeyCode.UpArrow],
mac: {
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.LeftArrow,
secondary: [KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.UpArrow]
}
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Previous Pane', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextPaneTerminalAction, FocusNextPaneTerminalAction.ID, FocusNextPaneTerminalAction.LABEL, {
primary: KeyMod.Alt | KeyCode.RightArrow,
secondary: [KeyMod.Alt | KeyCode.DownArrow],
mac: {
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.RightArrow,
secondary: [KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.DownArrow]
}
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Next Pane', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneLeftTerminalAction, ResizePaneLeftTerminalAction.ID, ResizePaneLeftTerminalAction.LABEL, {
primary: 0,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow },
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Left', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneRightTerminalAction, ResizePaneRightTerminalAction.ID, ResizePaneRightTerminalAction.LABEL, {
primary: 0,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.RightArrow },
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Right', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneUpTerminalAction, ResizePaneUpTerminalAction.ID, ResizePaneUpTerminalAction.LABEL, {
primary: 0,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow },
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.UpArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Up', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneDownTerminalAction, ResizePaneDownTerminalAction.ID, ResizePaneDownTerminalAction.LABEL, {
primary: 0,
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow },
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.DownArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Down', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToPreviousCommandAction, ScrollToPreviousCommandAction.ID, ScrollToPreviousCommandAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll To Previous Command', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToNextCommandAction, ScrollToNextCommandAction.ID, ScrollToNextCommandAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll To Next Command', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousCommandAction, SelectToPreviousCommandAction.ID, SelectToPreviousCommandAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Previous Command', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextCommandAction, SelectToNextCommandAction.ID, SelectToNextCommandAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Next Command', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousLineAction, SelectToPreviousLineAction.ID, SelectToPreviousLineAction.LABEL), 'Terminal: Select To Previous Line', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextLineAction, SelectToNextLineAction.ID, SelectToNextLineAction.LABEL), 'Terminal: Select To Next Line', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEscapeSequenceLoggingAction, ToggleEscapeSequenceLoggingAction.ID, ToggleEscapeSequenceLoggingAction.LABEL), 'Terminal: Toggle Escape Sequence Logging', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRegexCommand, ToggleRegexCommand.ID, ToggleRegexCommand.LABEL, {
primary: KeyMod.Alt | KeyCode.KEY_R,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Toggle find by regex');
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRegexCommand, ToggleRegexCommand.ID_TERMINAL_FOCUS, ToggleRegexCommand.LABEL, {
primary: KeyMod.Alt | KeyCode.KEY_R,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Toggle find by regex', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleWholeWordCommand, ToggleWholeWordCommand.ID, ToggleWholeWordCommand.LABEL, {
primary: KeyMod.Alt | KeyCode.KEY_W,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W }
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Toggle find whole word');
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleWholeWordCommand, ToggleWholeWordCommand.ID_TERMINAL_FOCUS, ToggleWholeWordCommand.LABEL, {
primary: KeyMod.Alt | KeyCode.KEY_W,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Toggle find whole word', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleCaseSensitiveCommand, ToggleCaseSensitiveCommand.ID, ToggleCaseSensitiveCommand.LABEL, {
primary: KeyMod.Alt | KeyCode.KEY_C,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Toggle find match case');
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleCaseSensitiveCommand, ToggleCaseSensitiveCommand.ID_TERMINAL_FOCUS, ToggleCaseSensitiveCommand.LABEL, {
primary: KeyMod.Alt | KeyCode.KEY_C,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Toggle find match case', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindNext, FindNext.ID_TERMINAL_FOCUS, FindNext.LABEL, {
primary: KeyCode.F3,
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find next', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindNext, FindNext.ID, FindNext.LABEL, {
primary: KeyCode.F3,
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find next');
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, FindPrevious.ID_TERMINAL_FOCUS, FindPrevious.LABEL, {
primary: KeyMod.Shift | KeyCode.F3,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] },
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find previous', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, FindPrevious.ID, FindPrevious.LABEL, {
primary: KeyMod.Shift | KeyCode.F3,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] },
}, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find previous');
const sendSequenceTerminalCommand = new SendSequenceTerminalCommand({
id: SendSequenceTerminalCommand.ID,
precondition: null,
description: {
description: `Send Custom Sequence To Terminal`,
args: [{
name: 'args',
schema: {
'type': 'object',
'required': ['text'],
'properties': {
'text': {
'type': 'string'
}
},
}
}]
}
});
sendSequenceTerminalCommand.register();
setupTerminalCommands();
setupTerminalMenu();
registerColors();

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal as XTermTerminal } from 'vscode-xterm';
import { ITerminalInstance, IWindowsShellHelper, ITerminalProcessManager, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProcessEnvironment } from 'vs/base/common/platform';
export const ITerminalInstanceService = createDecorator<ITerminalInstanceService>('terminalInstanceService');
export interface ITerminalInstanceService {
_serviceBrand: any;
getXtermConstructor(): Promise<typeof XTermTerminal>;
createWindowsShellHelper(shellProcessId: number, instance: ITerminalInstance, xterm: XTermTerminal): IWindowsShellHelper;
createTerminalProcessManager(id: number, configHelper: ITerminalConfigHelper): ITerminalProcessManager;
createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess;
}
export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper {
panelContainer: HTMLElement;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,279 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal, IMarker } from 'vscode-xterm';
import { ITerminalCommandTracker } from 'vs/workbench/contrib/terminal/common/terminal';
import { IDisposable } from 'vs/base/common/lifecycle';
/**
* The minimum size of the prompt in which to assume the line is a command.
*/
const MINIMUM_PROMPT_LENGTH = 2;
enum Boundary {
Top,
Bottom
}
export const enum ScrollPosition {
Top,
Middle
}
export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposable {
private _currentMarker: IMarker | Boundary = Boundary.Bottom;
private _selectionStart: IMarker | Boundary | null = null;
private _isDisposable: boolean = false;
constructor(
private _xterm: Terminal
) {
this._xterm.on('key', key => this._onKey(key));
}
public dispose(): void {
}
private _onKey(key: string): void {
if (key === '\x0d') {
this._onEnter();
}
// Clear the current marker so successive focus/selection actions are performed from the
// bottom of the buffer
this._currentMarker = Boundary.Bottom;
this._selectionStart = null;
}
private _onEnter(): void {
if (this._xterm._core.buffer.x >= MINIMUM_PROMPT_LENGTH) {
this._xterm.addMarker(0);
}
}
public scrollToPreviousCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!retainSelection) {
this._selectionStart = null;
}
let markerIndex;
if (this._currentMarker === Boundary.Bottom) {
markerIndex = this._xterm.markers.length - 1;
} else if (this._currentMarker === Boundary.Top) {
markerIndex = -1;
} else if (this._isDisposable) {
markerIndex = this._findPreviousCommand();
this._currentMarker.dispose();
this._isDisposable = false;
} else {
markerIndex = this._xterm.markers.indexOf(this._currentMarker) - 1;
}
if (markerIndex < 0) {
this._currentMarker = Boundary.Top;
this._xterm.scrollToTop();
return;
}
this._currentMarker = this._xterm.markers[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
}
public scrollToNextCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!retainSelection) {
this._selectionStart = null;
}
let markerIndex;
if (this._currentMarker === Boundary.Bottom) {
markerIndex = this._xterm.markers.length;
} else if (this._currentMarker === Boundary.Top) {
markerIndex = 0;
} else if (this._isDisposable) {
markerIndex = this._findNextCommand();
this._currentMarker.dispose();
this._isDisposable = false;
} else {
markerIndex = this._xterm.markers.indexOf(this._currentMarker) + 1;
}
if (markerIndex >= this._xterm.markers.length) {
this._currentMarker = Boundary.Bottom;
this._xterm.scrollToBottom();
return;
}
this._currentMarker = this._xterm.markers[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
}
private _scrollToMarker(marker: IMarker, position: ScrollPosition): void {
let line = marker.line;
if (position === ScrollPosition.Middle) {
line = Math.max(line - this._xterm.rows / 2, 0);
}
this._xterm.scrollToLine(line);
}
public selectToPreviousCommand(): void {
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToPreviousCommand(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
}
public selectToNextCommand(): void {
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToNextCommand(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
}
public selectToPreviousLine(): void {
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToPreviousLine(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
}
public selectToNextLine(): void {
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToNextLine(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
}
private _selectLines(start: IMarker | Boundary, end: IMarker | Boundary | null): void {
if (end === null) {
end = Boundary.Bottom;
}
let startLine = this._getLine(start);
let endLine = this._getLine(end);
if (startLine > endLine) {
const temp = startLine;
startLine = endLine;
endLine = temp;
}
// Subtract a line as the marker is on the line the command run, we do not want the next
// command in the selection for the current command
endLine -= 1;
this._xterm.selectLines(startLine, endLine);
}
private _getLine(marker: IMarker | Boundary): number {
// Use the _second last_ row as the last row is likely the prompt
if (marker === Boundary.Bottom) {
return this._xterm._core.buffer.ybase + this._xterm.rows - 1;
}
if (marker === Boundary.Top) {
return 0;
}
return marker.line;
}
public scrollToPreviousLine(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!retainSelection) {
this._selectionStart = null;
}
if (this._currentMarker === Boundary.Top) {
this._xterm.scrollToTop();
return;
}
if (this._currentMarker === Boundary.Bottom) {
this._currentMarker = this._xterm.addMarker(this._getOffset() - 1);
} else {
const offset = this._getOffset();
if (this._isDisposable) {
this._currentMarker.dispose();
}
this._currentMarker = this._xterm.addMarker(offset - 1);
}
this._isDisposable = true;
this._scrollToMarker(this._currentMarker, scrollPosition);
}
public scrollToNextLine(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!retainSelection) {
this._selectionStart = null;
}
if (this._currentMarker === Boundary.Bottom) {
this._xterm.scrollToBottom();
return;
}
if (this._currentMarker === Boundary.Top) {
this._currentMarker = this._xterm.addMarker(this._getOffset() + 1);
} else {
const offset = this._getOffset();
if (this._isDisposable) {
this._currentMarker.dispose();
}
this._currentMarker = this._xterm.addMarker(offset + 1);
}
this._isDisposable = true;
this._scrollToMarker(this._currentMarker, scrollPosition);
}
private _getOffset(): number {
if (this._currentMarker === Boundary.Bottom) {
return 0;
} else if (this._currentMarker === Boundary.Top) {
return 0 - (this._xterm._core.buffer.ybase + this._xterm._core.buffer.y);
} else {
let offset = this._getLine(this._currentMarker);
offset -= this._xterm._core.buffer.ybase + this._xterm._core.buffer.y;
return offset;
}
}
private _findPreviousCommand(): number {
if (this._currentMarker === Boundary.Top) {
return 0;
} else if (this._currentMarker === Boundary.Bottom) {
return this._xterm.markers.length - 1;
}
let i;
for (i = this._xterm.markers.length - 1; i >= 0; i--) {
if (this._xterm.markers[i].line < this._currentMarker.line) {
return i;
}
}
return -1;
}
private _findNextCommand(): number {
if (this._currentMarker === Boundary.Top) {
return 0;
} else if (this._currentMarker === Boundary.Bottom) {
return this._xterm.markers.length - 1;
}
let i;
for (i = 0; i < this._xterm.markers.length; i++) {
if (this._xterm.markers[i].line > this._currentMarker.line) {
return i;
}
}
return this._xterm.markers.length;
}
}

View File

@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ITerminalConfiguration, ITerminalFont, IShellLaunchConfig, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MINIMUM_LETTER_SPACING, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal';
import Severity from 'vs/base/common/severity';
import { Terminal as XTermTerminal } from 'vscode-xterm';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IBrowserTerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminal';
const MINIMUM_FONT_SIZE = 6;
const MAXIMUM_FONT_SIZE = 25;
/**
* Encapsulates terminal configuration logic, the primary purpose of this file is so that platform
* specific test cases can be written.
*/
export class TerminalConfigHelper implements IBrowserTerminalConfigHelper {
public panelContainer: HTMLElement;
private _charMeasureElement: HTMLElement;
private _lastFontMeasurement: ITerminalFont;
public config: ITerminalConfiguration;
public constructor(
private readonly _linuxDistro: LinuxDistro,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IConfigurationService private readonly _workspaceConfigurationService: IConfigurationService,
@INotificationService private readonly _notificationService: INotificationService,
@IStorageService private readonly _storageService: IStorageService
) {
this._updateConfig();
this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(TERMINAL_CONFIG_SECTION)) {
this._updateConfig();
}
});
}
private _updateConfig(): void {
this.config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
}
public configFontIsMonospace(): boolean {
this._createCharMeasureElementIfNecessary();
const fontSize = 15;
const fontFamily = this.config.fontFamily || this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
const i_rect = this._getBoundingRectFor('i', fontFamily, fontSize);
const w_rect = this._getBoundingRectFor('w', fontFamily, fontSize);
const invalidBounds = !i_rect.width || !w_rect.width;
if (invalidBounds) {
// There is no reason to believe the font is not Monospace.
return true;
}
return i_rect.width === w_rect.width;
}
private _createCharMeasureElementIfNecessary() {
// Create charMeasureElement if it hasn't been created or if it was orphaned by its parent
if (!this._charMeasureElement || !this._charMeasureElement.parentElement) {
this._charMeasureElement = document.createElement('div');
this.panelContainer.appendChild(this._charMeasureElement);
}
}
private _getBoundingRectFor(char: string, fontFamily: string, fontSize: number): ClientRect | DOMRect {
const style = this._charMeasureElement.style;
style.display = 'inline-block';
style.fontFamily = fontFamily;
style.fontSize = fontSize + 'px';
style.lineHeight = 'normal';
this._charMeasureElement.innerText = char;
const rect = this._charMeasureElement.getBoundingClientRect();
style.display = 'none';
return rect;
}
private _measureFont(fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number): ITerminalFont {
this._createCharMeasureElementIfNecessary();
const rect = this._getBoundingRectFor('X', fontFamily, fontSize);
// Bounding client rect was invalid, use last font measurement if available.
if (this._lastFontMeasurement && !rect.width && !rect.height) {
return this._lastFontMeasurement;
}
this._lastFontMeasurement = {
fontFamily,
fontSize,
letterSpacing,
lineHeight,
charWidth: rect.width,
charHeight: Math.ceil(rect.height)
};
return this._lastFontMeasurement;
}
/**
* Gets the font information based on the terminal.integrated.fontFamily
* terminal.integrated.fontSize, terminal.integrated.lineHeight configuration properties
*/
public getFont(xterm?: XTermTerminal, excludeDimensions?: boolean): ITerminalFont {
const editorConfig = this._configurationService.getValue<IEditorOptions>('editor');
let fontFamily = this.config.fontFamily || editorConfig.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
let fontSize = this._toInteger(this.config.fontSize, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize);
// Work around bad font on Fedora/Ubuntu
if (!this.config.fontFamily) {
if (this._linuxDistro === LinuxDistro.Fedora) {
fontFamily = '\'DejaVu Sans Mono\', monospace';
}
if (this._linuxDistro === LinuxDistro.Ubuntu) {
fontFamily = '\'Ubuntu Mono\', monospace';
// Ubuntu mono is somehow smaller, so set fontSize a bit larger to get the same perceived size.
fontSize = this._toInteger(fontSize + 2, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize);
}
}
const letterSpacing = this.config.letterSpacing ? Math.max(Math.floor(this.config.letterSpacing), MINIMUM_LETTER_SPACING) : DEFAULT_LETTER_SPACING;
const lineHeight = this.config.lineHeight ? Math.max(this.config.lineHeight, 1) : DEFAULT_LINE_HEIGHT;
if (excludeDimensions) {
return {
fontFamily,
fontSize,
letterSpacing,
lineHeight
};
}
// Get the character dimensions from xterm if it's available
if (xterm) {
if (xterm._core.charMeasure && xterm._core.charMeasure.width && xterm._core.charMeasure.height) {
return {
fontFamily,
fontSize,
letterSpacing,
lineHeight,
charHeight: xterm._core.charMeasure.height,
charWidth: xterm._core.charMeasure.width
};
}
}
// Fall back to measuring the font ourselves
return this._measureFont(fontFamily, fontSize, letterSpacing, lineHeight);
}
public setWorkspaceShellAllowed(isAllowed: boolean): void {
this._storageService.store(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, isAllowed, StorageScope.WORKSPACE);
}
public isWorkspaceShellAllowed(defaultValue: boolean | undefined = undefined): boolean | undefined {
return this._storageService.getBoolean(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, StorageScope.WORKSPACE, defaultValue);
}
public checkWorkspaceShellPermissions(platformOverride: platform.Platform = platform.platform): boolean {
// Check whether there is a workspace setting
const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
const shellConfigValue = this._workspaceConfigurationService.inspect<string>(`terminal.integrated.shell.${platformKey}`);
const shellArgsConfigValue = this._workspaceConfigurationService.inspect<string[]>(`terminal.integrated.shellArgs.${platformKey}`);
const envConfigValue = this._workspaceConfigurationService.inspect<string[]>(`terminal.integrated.env.${platformKey}`);
// Check if workspace setting exists and whether it's whitelisted
let isWorkspaceShellAllowed: boolean | undefined = false;
if (shellConfigValue.workspace !== undefined || shellArgsConfigValue.workspace !== undefined || envConfigValue.workspace !== undefined) {
isWorkspaceShellAllowed = this.isWorkspaceShellAllowed(undefined);
}
// Always allow [] args as it would lead to an odd error message and should not be dangerous
if (shellConfigValue.workspace === undefined && envConfigValue.workspace === undefined &&
shellArgsConfigValue.workspace && shellArgsConfigValue.workspace.length === 0) {
isWorkspaceShellAllowed = true;
}
// Check if the value is neither blacklisted (false) or whitelisted (true) and ask for
// permission
if (isWorkspaceShellAllowed === undefined) {
let shellString: string | undefined;
if (shellConfigValue.workspace) {
shellString = `shell: "${shellConfigValue.workspace}"`;
}
let argsString: string | undefined;
if (shellArgsConfigValue.workspace) {
argsString = `shellArgs: [${shellArgsConfigValue.workspace.map(v => '"' + v + '"').join(', ')}]`;
}
let envString: string | undefined;
if (envConfigValue.workspace) {
envString = `env: {${Object.keys(envConfigValue.workspace).map(k => `${k}:${envConfigValue.workspace![k]}`).join(', ')}}`;
}
// Should not be localized as it's json-like syntax referencing settings keys
const workspaceConfigStrings: string[] = [];
if (shellString) {
workspaceConfigStrings.push(shellString);
}
if (argsString) {
workspaceConfigStrings.push(argsString);
}
if (envString) {
workspaceConfigStrings.push(envString);
}
const workspaceConfigString = workspaceConfigStrings.join(', ');
this._notificationService.prompt(Severity.Info, nls.localize('terminal.integrated.allowWorkspaceShell', "Do you allow this workspace to modify your terminal shell? {0}", workspaceConfigString),
[{
label: nls.localize('allow', "Allow"),
run: () => this.setWorkspaceShellAllowed(true)
},
{
label: nls.localize('disallow', "Disallow"),
run: () => this.setWorkspaceShellAllowed(false)
}]
);
}
return !!isWorkspaceShellAllowed;
}
public mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, platformOverride: platform.Platform = platform.platform): void {
const isWorkspaceShellAllowed = this.checkWorkspaceShellPermissions(platformOverride);
const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
const shellConfigValue = this._workspaceConfigurationService.inspect<string>(`terminal.integrated.shell.${platformKey}`);
const shellArgsConfigValue = this._workspaceConfigurationService.inspect<string[]>(`terminal.integrated.shellArgs.${platformKey}`);
shell.executable = (isWorkspaceShellAllowed ? shellConfigValue.value : shellConfigValue.user) || shellConfigValue.default;
shell.args = (isWorkspaceShellAllowed ? shellArgsConfigValue.value : shellArgsConfigValue.user) || shellArgsConfigValue.default;
// Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's
// safe to assume that this was used by accident as Sysnative does not
// exist and will break the terminal in non-WoW64 environments.
if ((platformOverride === platform.Platform.Windows) && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432') && process.env.windir) {
const sysnativePath = path.join(process.env.windir, 'Sysnative').toLowerCase();
if (shell.executable.toLowerCase().indexOf(sysnativePath) === 0) {
shell.executable = path.join(process.env.windir, 'System32', shell.executable.substr(sysnativePath.length));
}
}
// Convert / to \ on Windows for convenience
if (platformOverride === platform.Platform.Windows) {
shell.executable = shell.executable.replace(/\//g, '\\');
}
}
private _toInteger(source: any, minimum: number, maximum: number, fallback: number): number {
let r = parseInt(source, 10);
if (isNaN(r)) {
return fallback;
}
if (typeof minimum === 'number') {
r = Math.max(minimum, r);
}
if (typeof maximum === 'number') {
r = Math.min(maximum, r);
}
return r;
}
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/terminal/common/terminal';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
export class TerminalFindWidget extends SimpleFindWidget {
protected _findInputFocused: IContextKey<boolean>;
protected _findWidgetFocused: IContextKey<boolean>;
constructor(
findState: FindReplaceState,
@IContextViewService _contextViewService: IContextViewService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@ITerminalService private readonly _terminalService: ITerminalService
) {
super(_contextViewService, _contextKeyService, findState, true);
this._register(findState.onFindReplaceStateChange(() => {
this.show();
}));
this._findInputFocused = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED.bindTo(this._contextKeyService);
this._findWidgetFocused = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED.bindTo(this._contextKeyService);
}
public find(previous: boolean) {
const instance = this._terminalService.getActiveInstance();
if (instance !== null) {
if (previous) {
instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() });
} else {
instance.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() });
}
}
}
public hide() {
super.hide();
const instance = this._terminalService.getActiveInstance();
if (instance) {
instance.focus();
}
}
protected onInputChanged() {
// Ignore input changes for now
const instance = this._terminalService.getActiveInstance();
if (instance !== null) {
instance.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: true });
}
}
protected onFocusTrackerFocus() {
const instance = this._terminalService.getActiveInstance();
if (instance) {
instance.notifyFindWidgetFocusChanged(true);
}
this._findWidgetFocused.set(true);
}
protected onFocusTrackerBlur() {
const instance = this._terminalService.getActiveInstance();
if (instance) {
instance.notifyFindWidgetFocusChanged(false);
}
this._findWidgetFocused.reset();
}
protected onFindInputFocusTrackerFocus() {
this._findInputFocused.set(true);
}
protected onFindInputFocusTrackerBlur() {
this._findInputFocused.reset();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,374 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as platform from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITerminalService, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IFileService } from 'vs/platform/files/common/files';
import { ILinkMatcherOptions } from 'vscode-xterm';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { posix, win32 } from 'vs/base/common/path';
const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
// '":; are allowed in paths but they are often separators so ignore them
// Also disallow \\ to prevent a catastropic backtracking case #24798
const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]+\'":;\\\\]';
/** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */
const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)';
const winDrivePrefix = '[a-zA-Z]:';
const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)';
const winPathSeparatorClause = '(\\\\|\\/)';
const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]+\'":;]';
/** A regex that matches paths in the form c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)';
/** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160,
replacing space with nonBreakningSpace or space ASCII code - 32. */
const lineAndColumnClause = [
'((\\S*)", line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468]
'((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13
'((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13
'(([^\\s\\(\\)]*)(\\s?[\\(\\[](\\d+)(,\\s?(\\d+))?)[\\)\\]])', // (file path)(45), (file path) (45), (file path)(45,18), (file path) (45,18), (file path)(45, 18), (file path) (45, 18), also with []
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9
].join('|').replace(/ /g, `[${'\u00A0'} ]`);
// Changing any regex may effect this value, hence changes this as well if required.
const winLineAndColumnMatchIndex = 12;
const unixLineAndColumnMatchIndex = 11;
// Each line and column clause have 6 groups (ie no. of expressions in round brackets)
const lineAndColumnClauseGroupCount = 6;
/** Higher than local link, lower than hypertext */
const CUSTOM_LINK_PRIORITY = -1;
/** Lowest */
const LOCAL_LINK_PRIORITY = -2;
export type XtermLinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
interface IPath {
join(...paths: string[]): string;
normalize(path: string): string;
}
export class TerminalLinkHandler {
private _hoverDisposables: IDisposable[] = [];
private _mouseMoveDisposable: IDisposable;
private _widgetManager: TerminalWidgetManager;
private _processCwd: string;
private _gitDiffPreImagePattern: RegExp;
private _gitDiffPostImagePattern: RegExp;
private readonly _tooltipCallback: (event: MouseEvent, uri: string) => boolean | void;
constructor(
private _xterm: any,
private _platform: platform.Platform,
private readonly _processManager: ITerminalProcessManager,
@IOpenerService private readonly _openerService: IOpenerService,
@IEditorService private readonly _editorService: IEditorService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@IFileService private readonly _fileService: IFileService
) {
// Matches '--- a/src/file1', capturing 'src/file1' in group 1
this._gitDiffPreImagePattern = /^--- a\/(\S*)/;
// Matches '+++ b/src/file1', capturing 'src/file1' in group 1
this._gitDiffPostImagePattern = /^\+\+\+ b\/(\S*)/;
this._tooltipCallback = (e: MouseEvent) => {
if (this._terminalService && this._terminalService.configHelper.config.rendererType === 'dom') {
const target = (e.target as HTMLElement);
this._widgetManager.showMessage(target.offsetLeft, target.offsetTop, this._getLinkHoverString());
} else {
this._widgetManager.showMessage(e.offsetX, e.offsetY, this._getLinkHoverString());
}
};
this.registerWebLinkHandler();
this.registerLocalLinkHandler();
this.registerGitDiffLinkHandlers();
}
public setWidgetManager(widgetManager: TerminalWidgetManager): void {
this._widgetManager = widgetManager;
}
public set processCwd(processCwd: string) {
this._processCwd = processCwd;
}
public registerCustomLinkHandler(regex: RegExp, handler: (uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number {
const options: ILinkMatcherOptions = {
matchIndex,
tooltipCallback: this._tooltipCallback,
leaveCallback: () => this._widgetManager.closeMessage(),
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: CUSTOM_LINK_PRIORITY
};
if (validationCallback) {
options.validationCallback = (uri: string, callback: (isValid: boolean) => void) => validationCallback(uri, callback);
}
return this._xterm.registerLinkMatcher(regex, this._wrapLinkHandler(handler), options);
}
public registerWebLinkHandler(): void {
const wrappedHandler = this._wrapLinkHandler(uri => {
this._handleHypertextLink(uri);
});
this._xterm.webLinksInit(wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(uri, callback),
tooltipCallback: this._tooltipCallback,
leaveCallback: () => this._widgetManager.closeMessage(),
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e)
});
}
public registerLocalLinkHandler(): void {
const wrappedHandler = this._wrapLinkHandler(url => {
this._handleLocalLink(url);
});
this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
tooltipCallback: this._tooltipCallback,
leaveCallback: () => this._widgetManager.closeMessage(),
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: LOCAL_LINK_PRIORITY
});
}
public registerGitDiffLinkHandlers(): void {
const wrappedHandler = this._wrapLinkHandler(url => {
this._handleLocalLink(url);
});
const options = {
matchIndex: 1,
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
tooltipCallback: this._tooltipCallback,
leaveCallback: () => this._widgetManager.closeMessage(),
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: LOCAL_LINK_PRIORITY
};
this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options);
this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options);
}
public dispose(): void {
this._xterm = null;
this._hoverDisposables = dispose(this._hoverDisposables);
this._mouseMoveDisposable = dispose(this._mouseMoveDisposable);
}
private _wrapLinkHandler(handler: (uri: string) => boolean | void): XtermLinkMatcherHandler {
return (event: MouseEvent, uri: string) => {
// Prevent default electron link handling so Alt+Click mode works normally
event.preventDefault();
// Require correct modifier on click
if (!this._isLinkActivationModifierDown(event)) {
// If the modifier is not pressed, the terminal should be
// focused if it's not already
this._terminalService.getActiveInstance()!.focus(true);
return false;
}
return handler(uri);
};
}
protected get _localLinkRegex(): RegExp {
const baseLocalLinkClause = this._processManager.os === platform.OperatingSystem.Windows ? winLocalLinkClause : unixLocalLinkClause;
// Append line and column number regex
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
}
protected get _gitDiffPreImageRegex(): RegExp {
return this._gitDiffPreImagePattern;
}
protected get _gitDiffPostImageRegex(): RegExp {
return this._gitDiffPostImagePattern;
}
private _handleLocalLink(link: string): PromiseLike<any> {
return this._resolvePath(link).then(resolvedLink => {
if (!resolvedLink) {
return Promise.resolve(null);
}
const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link);
const selection: ITextEditorSelection = {
startLineNumber: lineColumnInfo.lineNumber,
startColumn: lineColumnInfo.columnNumber
};
return this._editorService.openEditor({ resource: resolvedLink, options: { pinned: true, selection } });
});
}
private _validateLocalLink(link: string, callback: (isValid: boolean) => void): void {
this._resolvePath(link).then(resolvedLink => callback(!!resolvedLink));
}
private _validateWebLink(link: string, callback: (isValid: boolean) => void): void {
callback(true);
}
private _handleHypertextLink(url: string): void {
const uri = URI.parse(url);
this._openerService.open(uri);
}
private _isLinkActivationModifierDown(event: MouseEvent): boolean {
const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
if (editorConf.multiCursorModifier === 'ctrlCmd') {
return !!event.altKey;
}
return platform.isMacintosh ? event.metaKey : event.ctrlKey;
}
private _getLinkHoverString(): string {
const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
if (editorConf.multiCursorModifier === 'ctrlCmd') {
return nls.localize('terminalLinkHandler.followLinkAlt', 'Alt + click to follow link');
}
if (platform.isMacintosh) {
return nls.localize('terminalLinkHandler.followLinkCmd', 'Cmd + click to follow link');
}
return nls.localize('terminalLinkHandler.followLinkCtrl', 'Ctrl + click to follow link');
}
private get osPath(): IPath {
if (this._processManager.os === platform.OperatingSystem.Windows) {
return win32;
}
return posix;
}
protected _preprocessPath(link: string): string | null {
if (link.charAt(0) === '~') {
// Resolve ~ -> userHome
if (!this._processManager.userHome) {
return null;
}
link = this.osPath.join(this._processManager.userHome, link.substring(1));
} else if (link.charAt(0) !== '/' && link.charAt(0) !== '~') {
// Resolve workspace path . | .. | <relative_path> -> <path>/. | <path>/.. | <path>/<relative_path>
if (this._processManager.os === platform.OperatingSystem.Windows) {
if (!link.match('^' + winDrivePrefix)) {
if (!this._processCwd) {
// Abort if no workspace is open
return null;
}
link = this.osPath.join(this._processCwd, link);
}
} else {
if (!this._processCwd) {
// Abort if no workspace is open
return null;
}
link = this.osPath.join(this._processCwd, link);
}
}
link = this.osPath.normalize(link);
return link;
}
private _resolvePath(link: string): PromiseLike<URI | null> {
const preprocessedLink = this._preprocessPath(link);
if (!preprocessedLink) {
return Promise.resolve(null);
}
const linkUrl = this.extractLinkUrl(preprocessedLink);
if (!linkUrl) {
return Promise.resolve(null);
}
try {
let uri: URI;
if (this._processManager.remoteAuthority) {
uri = URI.from({
scheme: REMOTE_HOST_SCHEME,
authority: this._processManager.remoteAuthority,
path: linkUrl
});
} else {
uri = URI.file(linkUrl);
}
return this._fileService.resolveFile(uri).then(stat => {
if (stat.isDirectory) {
return null;
}
return uri;
}).catch(() => {
// Does not exist
return null;
});
} catch {
// Errors in parsing the path
return Promise.resolve(null);
}
}
/**
* Returns line and column number of URl if that is present.
*
* @param link Url link which may contain line and column number.
*/
public extractLineColumnInfo(link: string): LineColumnInfo {
const matches: string[] | null = this._localLinkRegex.exec(link);
const lineColumnInfo: LineColumnInfo = {
lineNumber: 1,
columnNumber: 1
};
if (!matches) {
return lineColumnInfo;
}
const lineAndColumnMatchIndex = this._platform === platform.Platform.Windows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex;
for (let i = 0; i < lineAndColumnClause.length; i++) {
const lineMatchIndex = lineAndColumnMatchIndex + (lineAndColumnClauseGroupCount * i);
const rowNumber = matches[lineMatchIndex];
if (rowNumber) {
lineColumnInfo['lineNumber'] = parseInt(rowNumber, 10);
// Check if column number exists
const columnNumber = matches[lineMatchIndex + 2];
if (columnNumber) {
lineColumnInfo['columnNumber'] = parseInt(columnNumber, 10);
}
break;
}
}
return lineColumnInfo;
}
/**
* Returns url from link as link may contain line and column information.
*
* @param link url link which may contain line and column number.
*/
public extractLinkUrl(link: string): string | null {
const matches: string[] | null = this._localLinkRegex.exec(link);
if (!matches) {
return null;
}
return matches[1];
}
}
export interface LineColumnInfo {
lineNumber: number;
columnNumber: number;
}

View File

@@ -0,0 +1,353 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import * as nls from 'vs/nls';
import * as platform from 'vs/base/common/platform';
import { Action, IAction } from 'vs/base/common/actions';
import { IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITerminalService, TERMINAL_PANEL_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { IThemeService, ITheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget';
import { editorHoverBackground, editorHoverBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { KillTerminalAction, SwitchTerminalAction, SwitchTerminalActionItem, CopyTerminalSelectionAction, TerminalPasteAction, ClearTerminalAction, SelectAllTerminalAction, CreateNewTerminalAction, SplitTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { Panel } from 'vs/workbench/browser/panel';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { URI } from 'vs/base/common/uri';
import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { DataTransfers } from 'vs/base/browser/dnd';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { IStorageService } from 'vs/platform/storage/common/storage';
const FIND_FOCUS_CLASS = 'find-focused';
export class TerminalPanel extends Panel {
private _actions: IAction[];
private _copyContextMenuAction: IAction;
private _contextMenuActions: IAction[];
private _cancelContextMenu: boolean = false;
private _fontStyleElement: HTMLElement;
private _parentDomElement: HTMLElement;
private _terminalContainer: HTMLElement;
private _findWidget: TerminalFindWidget;
constructor(
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@IThemeService protected readonly _themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
@INotificationService private readonly _notificationService: INotificationService,
@IStorageService storageService: IStorageService
) {
super(TERMINAL_PANEL_ID, telemetryService, _themeService, storageService);
}
public create(parent: HTMLElement): void {
super.create(parent);
this._parentDomElement = parent;
dom.addClass(this._parentDomElement, 'integrated-terminal');
this._fontStyleElement = document.createElement('style');
this._terminalContainer = document.createElement('div');
dom.addClass(this._terminalContainer, 'terminal-outer-container');
this._findWidget = this._instantiationService.createInstance(TerminalFindWidget, this._terminalService.getFindState());
this._findWidget.focusTracker.onDidFocus(() => this._terminalContainer.classList.add(FIND_FOCUS_CLASS));
this._findWidget.focusTracker.onDidBlur(() => this._terminalContainer.classList.remove(FIND_FOCUS_CLASS));
this._parentDomElement.appendChild(this._fontStyleElement);
this._parentDomElement.appendChild(this._terminalContainer);
this._parentDomElement.appendChild(this._findWidget.getDomNode());
this._attachEventListeners();
this._terminalService.setContainers(this.getContainer(), this._terminalContainer);
this._register(this.themeService.onThemeChange(theme => this._updateTheme(theme)));
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('terminal.integrated') || e.affectsConfiguration('editor.fontFamily')) {
this._updateFont();
}
if (e.affectsConfiguration('terminal.integrated.fontFamily') || e.affectsConfiguration('editor.fontFamily')) {
const configHelper = this._terminalService.configHelper;
if (!configHelper.configFontIsMonospace()) {
const choices: IPromptChoice[] = [{
label: nls.localize('terminal.useMonospace', "Use 'monospace'"),
run: () => this._configurationService.updateValue('terminal.integrated.fontFamily', 'monospace'),
}];
this._notificationService.prompt(Severity.Warning, nls.localize('terminal.monospaceOnly', "The terminal only supports monospace fonts."), choices);
}
}
}));
this._updateFont();
this._updateTheme();
this._register(this.onDidChangeVisibility(visible => {
if (visible) {
if (this._terminalService.terminalInstances.length > 0) {
this._updateFont();
this._updateTheme();
} else {
// Check if instances were already restored as part of workbench restore
if (this._terminalService.terminalInstances.length === 0) {
this._terminalService.createTerminal();
}
if (this._terminalService.terminalInstances.length > 0) {
this._updateFont();
this._updateTheme();
}
}
}
}));
// Force another layout (first is setContainers) since config has changed
this.layout(new dom.Dimension(this._terminalContainer.offsetWidth, this._terminalContainer.offsetHeight));
}
public layout(dimension?: dom.Dimension): void {
if (!dimension) {
return;
}
this._terminalService.terminalTabs.forEach(t => t.layout(dimension.width, dimension.height));
}
public getActions(): IAction[] {
if (!this._actions) {
this._actions = [
this._instantiationService.createInstance(SwitchTerminalAction, SwitchTerminalAction.ID, SwitchTerminalAction.LABEL),
this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL),
this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL),
this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL)
];
this._actions.forEach(a => {
this._register(a);
});
}
return this._actions;
}
private _getContextMenuActions(): IAction[] {
if (!this._contextMenuActions) {
this._copyContextMenuAction = this._instantiationService.createInstance(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.SHORT_LABEL);
this._contextMenuActions = [
this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL),
this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.SHORT_LABEL),
new Separator(),
this._copyContextMenuAction,
this._instantiationService.createInstance(TerminalPasteAction, TerminalPasteAction.ID, TerminalPasteAction.SHORT_LABEL),
this._instantiationService.createInstance(SelectAllTerminalAction, SelectAllTerminalAction.ID, SelectAllTerminalAction.LABEL),
new Separator(),
this._instantiationService.createInstance(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL),
new Separator(),
this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL)
];
this._contextMenuActions.forEach(a => {
this._register(a);
});
}
const activeInstance = this._terminalService.getActiveInstance();
this._copyContextMenuAction.enabled = !!activeInstance && activeInstance.hasSelection();
return this._contextMenuActions;
}
public getActionItem(action: Action): IActionItem | null {
if (action.id === SwitchTerminalAction.ID) {
return this._instantiationService.createInstance(SwitchTerminalActionItem, action);
}
return super.getActionItem(action);
}
public focus(): void {
const activeInstance = this._terminalService.getActiveInstance();
if (activeInstance) {
activeInstance.focusWhenReady(true);
}
}
public focusFindWidget() {
const activeInstance = this._terminalService.getActiveInstance();
if (activeInstance && activeInstance.hasSelection() && activeInstance.selection!.indexOf('\n') === -1) {
this._findWidget.reveal(activeInstance.selection);
} else {
this._findWidget.reveal();
}
}
public hideFindWidget() {
this._findWidget.hide();
}
public showFindWidget() {
const activeInstance = this._terminalService.getActiveInstance();
if (activeInstance && activeInstance.hasSelection() && activeInstance.selection!.indexOf('\n') === -1) {
this._findWidget.show(activeInstance.selection);
} else {
this._findWidget.show();
}
}
public getFindWidget(): TerminalFindWidget {
return this._findWidget;
}
private _attachEventListeners(): void {
this._register(dom.addDisposableListener(this._parentDomElement, 'mousedown', (event: MouseEvent) => {
if (this._terminalService.terminalInstances.length === 0) {
return;
}
if (event.which === 2 && platform.isLinux) {
// Drop selection and focus terminal on Linux to enable middle button paste when click
// occurs on the selection itself.
const terminal = this._terminalService.getActiveInstance();
if (terminal) {
terminal.focus();
}
} else if (event.which === 3) {
if (this._terminalService.configHelper.config.rightClickBehavior === 'copyPaste') {
const terminal = this._terminalService.getActiveInstance();
if (!terminal) {
return;
}
if (terminal.hasSelection()) {
terminal.copySelection();
terminal.clearSelection();
} else {
terminal.paste();
}
// Clear selection after all click event bubbling is finished on Mac to prevent
// right-click selecting a word which is seemed cannot be disabled. There is a
// flicker when pasting but this appears to give the best experience if the
// setting is enabled.
if (platform.isMacintosh) {
setTimeout(() => {
terminal.clearSelection();
}, 0);
}
this._cancelContextMenu = true;
}
}
}));
this._register(dom.addDisposableListener(this._parentDomElement, 'mouseup', (event: MouseEvent) => {
if (this._configurationService.getValue('terminal.integrated.copyOnSelection')) {
if (this._terminalService.terminalInstances.length === 0) {
return;
}
if (event.which === 1) {
const terminal = this._terminalService.getActiveInstance();
if (terminal && terminal.hasSelection()) {
terminal.copySelection();
}
}
}
}));
this._register(dom.addDisposableListener(this._parentDomElement, 'contextmenu', (event: MouseEvent) => {
if (!this._cancelContextMenu) {
const standardEvent = new StandardMouseEvent(event);
const anchor: { x: number, y: number } = { x: standardEvent.posx, y: standardEvent.posy };
this._contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => this._getContextMenuActions(),
getActionsContext: () => this._parentDomElement
});
} else {
event.stopImmediatePropagation();
}
this._cancelContextMenu = false;
}));
this._register(dom.addDisposableListener(document, 'keydown', (event: KeyboardEvent) => {
this._terminalContainer.classList.toggle('alt-active', !!event.altKey);
}));
this._register(dom.addDisposableListener(document, 'keyup', (event: KeyboardEvent) => {
this._terminalContainer.classList.toggle('alt-active', !!event.altKey);
}));
this._register(dom.addDisposableListener(this._parentDomElement, 'keyup', (event: KeyboardEvent) => {
if (event.keyCode === 27) {
// Keep terminal open on escape
event.stopPropagation();
}
}));
this._register(dom.addDisposableListener(this._parentDomElement, dom.EventType.DROP, async (e: DragEvent) => {
if (e.target === this._parentDomElement || dom.isAncestor(e.target as HTMLElement, this._parentDomElement)) {
if (!e.dataTransfer) {
return;
}
// Check if files were dragged from the tree explorer
let path: string | undefined;
const resources = e.dataTransfer.getData(DataTransfers.RESOURCES);
if (resources) {
path = URI.parse(JSON.parse(resources)[0]).fsPath;
} else if (e.dataTransfer.files.length > 0) {
// Check if the file was dragged from the filesystem
path = URI.file(e.dataTransfer.files[0].path).fsPath;
}
if (!path) {
return;
}
const terminal = this._terminalService.getActiveInstance();
if (terminal) {
return this._terminalService.preparePathForTerminalAsync(path, terminal.shellLaunchConfig.executable, terminal.title).then(preparedPath => {
terminal.sendText(preparedPath, false);
});
}
}
}));
}
private _updateTheme(theme?: ITheme): void {
if (!theme) {
theme = this.themeService.getTheme();
}
this._findWidget.updateTheme(theme);
}
private _updateFont(): void {
if (this._terminalService.terminalInstances.length === 0) {
return;
}
// TODO: Can we support ligatures?
// dom.toggleClass(this._parentDomElement, 'enable-ligatures', this._terminalService.configHelper.config.fontLigatures);
this.layout(new dom.Dimension(this._parentDomElement.offsetWidth, this._parentDomElement.offsetHeight));
}
}
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR);
collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-outer-container { background-color: ${backgroundColor ? backgroundColor.toString() : ''}; }`);
const borderColor = theme.getColor(TERMINAL_BORDER_COLOR);
if (borderColor) {
collector.addRule(`.monaco-workbench .panel.integrated-terminal .split-view-view:not(:first-child) { border-color: ${borderColor.toString()}; }`);
}
// Borrow the editor's hover background for now
const hoverBackground = theme.getColor(editorHoverBackground);
if (hoverBackground) {
collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { background-color: ${hoverBackground}; }`);
}
const hoverBorder = theme.getColor(editorHoverBorder);
if (hoverBorder) {
collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { border: 1px solid ${hoverBorder}; }`);
}
const hoverForeground = theme.getColor(editorForeground);
if (hoverForeground) {
collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { color: ${hoverForeground}; }`);
}
});

View File

@@ -0,0 +1,261 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as platform from 'vs/base/common/platform';
import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess } from 'vs/workbench/contrib/terminal/common/terminal';
import { ILogService } from 'vs/platform/log/common/log';
import { Emitter, Event } from 'vs/base/common/event';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminalProcessExtHostProxy';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { Schemas } from 'vs/base/common/network';
import { REMOTE_HOST_SCHEME, getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
import { sanitizeProcessEnvironment } from 'vs/base/common/processes';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IProductService } from 'vs/platform/product/common/product';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IRemoteEnvironmentService } from 'vs/workbench/services/remote/common/remoteEnvironmentService';
/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;
/**
* Holds all state related to the creation and management of terminal processes.
*
* Internal definitions:
* - Process: The process launched with the terminalProcess.ts file, or the pty as a whole
* - Pty Process: The pseudoterminal master process (or the winpty agent process)
* - Shell Process: The pseudoterminal slave process (ie. the shell)
*/
export class TerminalProcessManager implements ITerminalProcessManager {
public processState: ProcessState = ProcessState.UNINITIALIZED;
public ptyProcessReady: Promise<void>;
public shellProcessId: number;
public remoteAuthority: string | undefined;
public os: platform.OperatingSystem | undefined;
public userHome: string | undefined;
private _process: ITerminalChildProcess | null = null;
private _preLaunchInputQueue: string[] = [];
private _disposables: IDisposable[] = [];
private readonly _onProcessReady = new Emitter<void>();
public get onProcessReady(): Event<void> { return this._onProcessReady.event; }
private readonly _onProcessData = new Emitter<string>();
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessTitle = new Emitter<string>();
public get onProcessTitle(): Event<string> { return this._onProcessTitle.event; }
private readonly _onProcessExit = new Emitter<number>();
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
constructor(
private readonly _terminalId: number,
private readonly _configHelper: ITerminalConfigHelper,
@IHistoryService private readonly _historyService: IHistoryService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ILogService private readonly _logService: ILogService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService,
@IWindowService private readonly _windowService: IWindowService,
@IConfigurationService private readonly _workspaceConfigurationService: IConfigurationService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@IProductService private readonly _productService: IProductService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IRemoteEnvironmentService private readonly _remoteEnvironmentService: IRemoteEnvironmentService
) {
this.ptyProcessReady = new Promise<void>(c => {
this.onProcessReady(() => {
this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`);
c(undefined);
});
});
}
public dispose(immediate: boolean = false): void {
if (this._process) {
// If the process was still connected this dispose came from
// within VS Code, not the process, so mark the process as
// killed by the user.
this.processState = ProcessState.KILLED_BY_USER;
this._process.shutdown(immediate);
this._process = null;
}
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
public addDisposable(disposable: IDisposable) {
this._disposables.push(disposable);
}
public createProcess(
shellLaunchConfig: IShellLaunchConfig,
cols: number,
rows: number
): void {
const forceExtHostProcess = (this._configHelper.config as any).extHostProcess;
if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') {
this.remoteAuthority = getRemoteAuthority(shellLaunchConfig.cwd);
} else {
this.remoteAuthority = this._windowService.getConfiguration().remoteAuthority;
}
const hasRemoteAuthority = !!this.remoteAuthority;
let launchRemotely = hasRemoteAuthority || forceExtHostProcess;
this.userHome = this._environmentService.userHome;
this.os = platform.OS;
if (launchRemotely) {
if (hasRemoteAuthority) {
this._remoteEnvironmentService.remoteEnvironment.then(env => {
if (!env) {
return;
}
this.userHome = env.userHome.path;
this.os = env.os;
});
}
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(hasRemoteAuthority ? REMOTE_HOST_SCHEME : undefined);
this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows);
} else {
if (!shellLaunchConfig.executable) {
this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig);
}
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file);
const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, this._environmentService.userHome, activeWorkspaceRootUri, this._configHelper.config.cwd);
// Compel type system as process.env should not have any undefined entries
let env: platform.IProcessEnvironment = {};
if (shellLaunchConfig.strictEnv) {
// Only base the terminal process environment on this environment and add the
// various mixins when strictEnv is false
env = { ...shellLaunchConfig.env } as any;
} else {
// Merge process env with the env from config and from shellLaunchConfig
env = { ...process.env } as any;
// Resolve env vars from config and shell
const lastActiveWorkspaceRoot = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : null;
const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions();
const envFromConfigValue = this._workspaceConfigurationService.inspect<{ [key: string]: string }>(`terminal.integrated.env.${platformKey}`);
const allowedEnvFromConfig = (isWorkspaceShellAllowed ? envFromConfigValue.value : envFromConfigValue.user);
const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...allowedEnvFromConfig }, lastActiveWorkspaceRoot);
const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot);
shellLaunchConfig.env = envFromShell;
terminalEnvironment.mergeEnvironments(env, envFromConfig);
terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env);
// Sanitize the environment, removing any undesirable VS Code and Electron environment
// variables
sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI');
// Adding other env keys necessary to create the process
terminalEnvironment.addTerminalEnvironmentKeys(env, this._productService.version, platform.locale, this._configHelper.config.setLocaleVariables);
}
this._logService.debug(`Terminal process launching`, shellLaunchConfig, initialCwd, cols, rows, env);
this._process = this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, this._configHelper.config.windowsEnableConpty);
}
this.processState = ProcessState.LAUNCHING;
// The process is non-null, but TS isn't clever enough to know
const p = this._process!;
p.onProcessData(data => {
this._onProcessData.fire(data);
});
p.onProcessIdReady(pid => {
this.shellProcessId = pid;
this._onProcessReady.fire();
// Send any queued data that's waiting
if (this._preLaunchInputQueue.length > 0) {
p.input(this._preLaunchInputQueue.join(''));
this._preLaunchInputQueue.length = 0;
}
});
p.onProcessTitleChanged(title => this._onProcessTitle.fire(title));
p.onProcessExit(exitCode => this._onExit(exitCode));
setTimeout(() => {
if (this.processState === ProcessState.LAUNCHING) {
this.processState = ProcessState.RUNNING;
}
}, LAUNCHING_DURATION);
}
public setDimensions(cols: number, rows: number): void {
if (!this._process) {
return;
}
// The child process could already be terminated
try {
this._process.resize(cols, rows);
} catch (error) {
// We tried to write to a closed pipe / channel.
if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') {
throw (error);
}
}
}
public write(data: string): void {
if (this.shellProcessId) {
if (this._process) {
// Send data if the pty is ready
this._process.input(data);
}
} else {
// If the pty is not ready, queue the data received to send later
this._preLaunchInputQueue.push(data);
}
}
public getInitialCwd(): Promise<string> {
if (!this._process) {
return Promise.resolve('');
}
return this._process.getInitialCwd();
}
public getCwd(): Promise<string> {
if (!this._process) {
return Promise.resolve('');
}
return this._process.getCwd();
}
private _onExit(exitCode: number): void {
this._process = null;
// If the process is marked as launching then mark the process as killed
// during launch. This typically means that there is a problem with the
// shell and args.
if (this.processState === ProcessState.LAUNCHING) {
this.processState = ProcessState.KILLED_DURING_LAUNCH;
}
// If TerminalInstance did not know about the process exit then it was
// triggered by the process, not on VS Code's side.
if (this.processState === ProcessState.RUNNING) {
this.processState = ProcessState.KILLED_BY_PROCESS;
}
this._onProcessExit.fire(exitCode);
}
}

View File

@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen';
import { QuickOpenModel, QuickOpenEntry } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { QuickOpenHandler } from 'vs/workbench/browser/quickopen';
import { ITerminalService, ITerminalInstance } from 'vs/workbench/contrib/terminal/common/terminal';
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
import { stripWildcards } from 'vs/base/common/strings';
import { matchesFuzzy } from 'vs/base/common/filters';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CancellationToken } from 'vs/base/common/cancellation';
export class TerminalEntry extends QuickOpenEntry {
constructor(
public instance: ITerminalInstance,
private label: string,
private terminalService: ITerminalService
) {
super();
}
public getLabel(): string {
return this.label;
}
public getAriaLabel(): string {
return nls.localize('termEntryAriaLabel', "{0}, terminal picker", this.getLabel());
}
public run(mode: Mode, context: IEntryRunContext): boolean {
if (mode === Mode.OPEN) {
setTimeout(() => {
this.terminalService.setActiveInstance(this.instance);
this.terminalService.showPanel(true);
}, 0);
return true;
}
return super.run(mode, context);
}
}
export class CreateTerminal extends QuickOpenEntry {
constructor(
private label: string,
private commandService: ICommandService
) {
super();
}
public getLabel(): string {
return this.label;
}
public getAriaLabel(): string {
return nls.localize('termCreateEntryAriaLabel', "{0}, create new terminal", this.getLabel());
}
public run(mode: Mode, context: IEntryRunContext): boolean {
if (mode === Mode.OPEN) {
setTimeout(() => this.commandService.executeCommand('workbench.action.terminal.new'), 0);
return true;
}
return super.run(mode, context);
}
}
export class TerminalPickerHandler extends QuickOpenHandler {
public static readonly ID = 'workbench.picker.terminals';
constructor(
@ITerminalService private readonly terminalService: ITerminalService,
@ICommandService private readonly commandService: ICommandService,
) {
super();
}
public getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel> {
searchValue = searchValue.trim();
const normalizedSearchValueLowercase = stripWildcards(searchValue).toLowerCase();
const terminalEntries: QuickOpenEntry[] = this.getTerminals();
terminalEntries.push(new CreateTerminal(nls.localize("workbench.action.terminal.newplus", "$(plus) Create New Integrated Terminal"), this.commandService));
const entries = terminalEntries.filter(e => {
if (!searchValue) {
return true;
}
const label = e.getLabel();
if (!label) {
return false;
}
const highlights = matchesFuzzy(normalizedSearchValueLowercase, label, true);
if (!highlights) {
return false;
}
e.setHighlights(highlights);
return true;
});
return Promise.resolve(new QuickOpenModel(entries, new ContributableActionProvider()));
}
private getTerminals(): TerminalEntry[] {
return this.terminalService.terminalTabs.reduce((terminals, tab, tabIndex) => {
const terminalsInTab = tab.terminalInstances.map((terminal, terminalIndex) => {
const label = `${tabIndex + 1}.${terminalIndex + 1}: ${terminal.title}`;
return new TerminalEntry(terminal, label, this.terminalService);
});
return [...terminals, ...terminalsInTab];
}, []);
}
public getAutoFocus(searchValue: string, context: { model: IModel<QuickOpenEntry>, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus {
return {
autoFocusFirstEntry: !!searchValue || !!context.quickNavigateConfiguration
};
}
public getEmptyLabel(searchString: string): string {
if (searchString.length > 0) {
return nls.localize('noTerminalsMatching', "No terminals matching");
}
return nls.localize('noTerminalsFound', "No terminals open");
}
}

View File

@@ -0,0 +1,182 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as platform from 'vs/base/common/platform';
import { ITerminalService, TERMINAL_PANEL_ID, ITerminalInstance, IShellLaunchConfig, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalService as CommonTerminalService } from 'vs/workbench/contrib/terminal/common/terminalService';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { TerminalPanel } from 'vs/workbench/contrib/terminal/browser/terminalPanel';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
import { IBrowserTerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminal';
export abstract class TerminalService extends CommonTerminalService implements ITerminalService {
protected _configHelper: IBrowserTerminalConfigHelper;
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IPanelService panelService: IPanelService,
@IWorkbenchLayoutService private _layoutService: IWorkbenchLayoutService,
@ILifecycleService lifecycleService: ILifecycleService,
@IStorageService storageService: IStorageService,
@INotificationService notificationService: INotificationService,
@IDialogService dialogService: IDialogService,
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
@IWindowService private _windowService: IWindowService,
@IExtensionService extensionService: IExtensionService,
@IFileService fileService: IFileService,
) {
super(contextKeyService, panelService, lifecycleService, storageService, notificationService, dialogService, extensionService, fileService);
}
protected abstract _getDefaultShell(p: platform.Platform): string;
public createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance {
const instance = this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig);
this._onInstanceCreated.fire(instance);
return instance;
}
public createTerminal(shell: IShellLaunchConfig = {}, wasNewTerminalAction?: boolean): ITerminalInstance {
const terminalTab = this._instantiationService.createInstance(TerminalTab,
this._terminalFocusContextKey,
this.configHelper,
this._terminalContainer,
shell);
this._terminalTabs.push(terminalTab);
const instance = terminalTab.terminalInstances[0];
terminalTab.addDisposable(terminalTab.onDisposed(this._onTabDisposed.fire, this._onTabDisposed));
terminalTab.addDisposable(terminalTab.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged));
this._initInstanceListeners(instance);
if (this.terminalInstances.length === 1) {
// It's the first instance so it should be made active automatically
this.setActiveInstanceByIndex(0);
}
this._onInstancesChanged.fire();
this._suggestShellChange(wasNewTerminalAction);
return instance;
}
private _suggestShellChange(wasNewTerminalAction?: boolean): void {
// Only suggest on Windows since $SHELL works great for macOS/Linux
if (!platform.isWindows) {
return;
}
if (this._windowService.getConfiguration().remoteAuthority) {
// Don't suggest if the opened workspace is remote
return;
}
// Only suggest when the terminal instance is being created by an explicit user action to
// launch a terminal, as opposed to something like tasks, debug, panel restore, etc.
if (!wasNewTerminalAction) {
return;
}
if (this._windowService.getConfiguration().remoteAuthority) {
// Don't suggest if the opened workspace is remote
return;
}
// Don't suggest if the user has explicitly opted out
const neverSuggest = this._storageService.getBoolean(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, StorageScope.GLOBAL, false);
if (neverSuggest) {
return;
}
// Never suggest if the setting is non-default already (ie. they set the setting manually)
if (this.configHelper.config.shell.windows !== this._getDefaultShell(platform.Platform.Windows)) {
this._storageService.store(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, true, StorageScope.GLOBAL);
return;
}
this._notificationService.prompt(
Severity.Info,
nls.localize('terminal.integrated.chooseWindowsShellInfo', "You can change the default terminal shell by selecting the customize button."),
[{
label: nls.localize('customize', "Customize"),
run: () => {
this.selectDefaultWindowsShell().then(shell => {
if (!shell) {
return Promise.resolve(null);
}
// Launch a new instance with the newly selected shell
const instance = this.createTerminal({
executable: shell,
args: this.configHelper.config.shellArgs.windows
});
if (instance) {
this.setActiveInstance(instance);
}
return Promise.resolve(null);
});
}
},
{
label: nls.localize('never again', "Don't Show Again"),
isSecondary: true,
run: () => this._storageService.store(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, true, StorageScope.GLOBAL)
}]
);
}
public focusFindWidget(): Promise<void> {
return this.showPanel(false).then(() => {
const panel = this._panelService.getActivePanel() as TerminalPanel;
panel.focusFindWidget();
this._findWidgetVisible.set(true);
});
}
public hideFindWidget(): void {
const panel = this._panelService.getActivePanel() as TerminalPanel;
if (panel && panel.getId() === TERMINAL_PANEL_ID) {
panel.hideFindWidget();
this._findWidgetVisible.reset();
panel.focus();
}
}
public findNext(): void {
const panel = this._panelService.getActivePanel() as TerminalPanel;
if (panel && panel.getId() === TERMINAL_PANEL_ID) {
panel.showFindWidget();
panel.getFindWidget().find(false);
}
}
public findPrevious(): void {
const panel = this._panelService.getActivePanel() as TerminalPanel;
if (panel && panel.getId() === TERMINAL_PANEL_ID) {
panel.showFindWidget();
panel.getFindWidget().find(true);
}
}
public setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void {
this._configHelper.panelContainer = panelContainer;
this._terminalContainer = terminalContainer;
this._terminalTabs.forEach(tab => tab.attachToElement(this._terminalContainer));
}
public hidePanel(): void {
const panel = this._panelService.getActivePanel();
if (panel && panel.getId() === TERMINAL_PANEL_ID) {
this._layoutService.setPanelHidden(true);
}
}
}

View File

@@ -0,0 +1,437 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as aria from 'vs/base/browser/ui/aria/aria';
import * as nls from 'vs/nls';
import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction, ITerminalService, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { SplitView, Orientation, IView, Sizing } from 'vs/base/browser/ui/splitview/splitview';
import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService';
const SPLIT_PANE_MIN_SIZE = 120;
const TERMINAL_MIN_USEFUL_SIZE = 250;
class SplitPaneContainer {
private _height: number;
private _width: number;
private _splitView: SplitView;
private _splitViewDisposables: IDisposable[];
private _children: SplitPane[] = [];
private _onDidChange: Event<number | undefined> = Event.None;
public get onDidChange(): Event<number | undefined> { return this._onDidChange; }
constructor(
private _container: HTMLElement,
public orientation: Orientation
) {
this._width = this._container.offsetWidth;
this._height = this._container.offsetHeight;
this._createSplitView();
this._splitView.layout(this.orientation === Orientation.HORIZONTAL ? this._width : this._height);
}
private _createSplitView(): void {
this._splitView = new SplitView(this._container, { orientation: this.orientation });
this._splitViewDisposables = [];
this._splitViewDisposables.push(this._splitView.onDidSashReset(() => this._splitView.distributeViewSizes()));
}
public split(instance: ITerminalInstance, index: number = this._children.length): void {
this._addChild(instance, index);
}
public resizePane(index: number, direction: Direction, amount: number): void {
// TODO: Should resize pane up/down resize the panel?
// Only resize the correct dimension
const isHorizontal = direction === Direction.Left || direction === Direction.Right;
if (isHorizontal && this.orientation !== Orientation.HORIZONTAL ||
!isHorizontal && this.orientation !== Orientation.VERTICAL) {
return;
}
// Only resize when there is mor ethan one pane
if (this._children.length <= 1) {
return;
}
// Get sizes
const sizes: number[] = [];
for (let i = 0; i < this._splitView.length; i++) {
sizes.push(this._splitView.getViewSize(i));
}
// Remove size from right pane, unless index is the last pane in which case use left pane
const isSizingEndPane = index !== this._children.length - 1;
const indexToChange = isSizingEndPane ? index + 1 : index - 1;
if (isSizingEndPane && direction === Direction.Left) {
amount *= -1;
} else if (!isSizingEndPane && direction === Direction.Right) {
amount *= -1;
} else if (isSizingEndPane && direction === Direction.Up) {
amount *= -1;
} else if (!isSizingEndPane && direction === Direction.Down) {
amount *= -1;
}
// Ensure the size is not reduced beyond the minimum, otherwise weird things can happen
if (sizes[index] + amount < SPLIT_PANE_MIN_SIZE) {
amount = SPLIT_PANE_MIN_SIZE - sizes[index];
} else if (sizes[indexToChange] - amount < SPLIT_PANE_MIN_SIZE) {
amount = sizes[indexToChange] - SPLIT_PANE_MIN_SIZE;
}
// Apply the size change
sizes[index] += amount;
sizes[indexToChange] -= amount;
for (let i = 0; i < this._splitView.length - 1; i++) {
this._splitView.resizeView(i, sizes[i]);
}
}
private _addChild(instance: ITerminalInstance, index: number): void {
const child = new SplitPane(instance, this.orientation === Orientation.HORIZONTAL ? this._height : this._width);
child.orientation = this.orientation;
if (typeof index === 'number') {
this._children.splice(index, 0, child);
} else {
this._children.push(child);
}
this._withDisabledLayout(() => this._splitView.addView(child, Sizing.Distribute, index));
this._onDidChange = Event.any(...this._children.map(c => c.onDidChange));
}
public remove(instance: ITerminalInstance): void {
let index: number | null = null;
for (let i = 0; i < this._children.length; i++) {
if (this._children[i].instance === instance) {
index = i;
}
}
if (index !== null) {
this._children.splice(index, 1);
this._splitView.removeView(index, Sizing.Distribute);
}
}
public layout(width: number, height: number): void {
this._width = width;
this._height = height;
if (this.orientation === Orientation.HORIZONTAL) {
this._children.forEach(c => c.orthogonalLayout(height));
this._splitView.layout(width);
} else {
this._children.forEach(c => c.orthogonalLayout(width));
this._splitView.layout(height);
}
}
public setOrientation(orientation: Orientation): void {
if (this.orientation === orientation) {
return;
}
this.orientation = orientation;
// Remove old split view
while (this._container.children.length > 0) {
this._container.removeChild(this._container.children[0]);
}
this._splitViewDisposables.forEach(d => d.dispose());
this._splitViewDisposables = [];
this._splitView.dispose();
// Create new split view with updated orientation
this._createSplitView();
this._withDisabledLayout(() => {
this._children.forEach(child => {
child.orientation = orientation;
this._splitView.addView(child, 1);
});
});
}
private _withDisabledLayout(innerFunction: () => void): void {
// Whenever manipulating views that are going to be changed immediately, disabling
// layout/resize events in the terminal prevent bad dimensions going to the pty.
this._children.forEach(c => c.instance.disableLayout = true);
innerFunction();
this._children.forEach(c => c.instance.disableLayout = false);
}
}
class SplitPane implements IView {
public minimumSize: number = SPLIT_PANE_MIN_SIZE;
public maximumSize: number = Number.MAX_VALUE;
public orientation: Orientation | undefined;
protected _size: number;
private _onDidChange: Event<number | undefined> = Event.None;
public get onDidChange(): Event<number | undefined> { return this._onDidChange; }
readonly element: HTMLElement;
constructor(
readonly instance: ITerminalInstance,
public orthogonalSize: number
) {
this.element = document.createElement('div');
this.element.className = 'terminal-split-pane';
this.instance.attachToElement(this.element);
}
public layout(size: number): void {
// Only layout when both sizes are known
this._size = size;
if (!this._size || !this.orthogonalSize) {
return;
}
if (this.orientation === Orientation.VERTICAL) {
this.instance.layout({ width: this.orthogonalSize, height: this._size });
} else {
this.instance.layout({ width: this._size, height: this.orthogonalSize });
}
}
public orthogonalLayout(size: number): void {
this.orthogonalSize = size;
}
}
export class TerminalTab extends Disposable implements ITerminalTab {
private _terminalInstances: ITerminalInstance[] = [];
private _splitPaneContainer: SplitPaneContainer | undefined;
private _tabElement: HTMLElement | null;
private _panelPosition: Position = Position.BOTTOM;
private _activeInstanceIndex: number;
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
private readonly _onDisposed: Emitter<ITerminalTab>;
public get onDisposed(): Event<ITerminalTab> { return this._onDisposed.event; }
private readonly _onInstancesChanged: Emitter<void>;
public get onInstancesChanged(): Event<void> { return this._onInstancesChanged.event; }
constructor(
terminalFocusContextKey: IContextKey<boolean>,
configHelper: ITerminalConfigHelper,
private _container: HTMLElement,
shellLaunchConfig: IShellLaunchConfig,
@ITerminalService private readonly _terminalService: ITerminalService,
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService
) {
super();
this._onDisposed = new Emitter<ITerminalTab>();
this._onInstancesChanged = new Emitter<void>();
const instance = this._terminalService.createInstance(
terminalFocusContextKey,
configHelper,
undefined,
shellLaunchConfig,
true);
this._terminalInstances.push(instance);
this._initInstanceListeners(instance);
this._activeInstanceIndex = 0;
if (this._container) {
this.attachToElement(this._container);
}
}
public dispose(): void {
super.dispose();
if (this._tabElement) {
this._container.removeChild(this._tabElement);
this._tabElement = null;
}
this._terminalInstances = [];
this._onInstancesChanged.fire();
}
public get activeInstance(): ITerminalInstance | null {
if (this._terminalInstances.length === 0) {
return null;
}
return this._terminalInstances[this._activeInstanceIndex];
}
private _initInstanceListeners(instance: ITerminalInstance): void {
instance.addDisposable(instance.onDisposed(instance => this._onInstanceDisposed(instance)));
instance.addDisposable(instance.onFocused(instance => {
aria.alert(nls.localize('terminalFocus', "Terminal {0}", this._terminalService.activeTabIndex + 1));
this._setActiveInstance(instance);
}));
}
private _onInstanceDisposed(instance: ITerminalInstance): void {
// Get the index of the instance and remove it from the list
const index = this._terminalInstances.indexOf(instance);
const wasActiveInstance = instance === this.activeInstance;
if (index !== -1) {
this._terminalInstances.splice(index, 1);
}
// Adjust focus if the instance was active
if (wasActiveInstance && this._terminalInstances.length > 0) {
const newIndex = index < this._terminalInstances.length ? index : this._terminalInstances.length - 1;
this.setActiveInstanceByIndex(newIndex);
// TODO: Only focus the new instance if the tab had focus?
if (this.activeInstance) {
this.activeInstance.focus(true);
}
}
// Remove the instance from the split pane if it has been created
if (this._splitPaneContainer) {
this._splitPaneContainer.remove(instance);
}
// Fire events and dispose tab if it was the last instance
this._onInstancesChanged.fire();
if (this._terminalInstances.length === 0) {
this._onDisposed.fire(this);
this.dispose();
}
}
private _setActiveInstance(instance: ITerminalInstance): void {
this.setActiveInstanceByIndex(this._getIndexFromId(instance.id));
}
private _getIndexFromId(terminalId: number): number {
let terminalIndex = -1;
this.terminalInstances.forEach((terminalInstance, i) => {
if (terminalInstance.id === terminalId) {
terminalIndex = i;
}
});
if (terminalIndex === -1) {
throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`);
}
return terminalIndex;
}
public setActiveInstanceByIndex(index: number): void {
// Check for invalid value
if (index < 0 || index >= this._terminalInstances.length) {
return;
}
const didInstanceChange = this._activeInstanceIndex !== index;
this._activeInstanceIndex = index;
if (didInstanceChange) {
this._onInstancesChanged.fire();
}
}
public attachToElement(element: HTMLElement): void {
this._container = element;
this._tabElement = document.createElement('div');
this._tabElement.classList.add('terminal-tab');
this._container.appendChild(this._tabElement);
if (!this._splitPaneContainer) {
this._panelPosition = this._layoutService.getPanelPosition();
const orientation = this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL;
const newLocal = new SplitPaneContainer(this._tabElement, orientation);
this._splitPaneContainer = newLocal;
this.terminalInstances.forEach(instance => this._splitPaneContainer!.split(instance));
}
}
public get title(): string {
let title = this.terminalInstances[0].title;
for (let i = 1; i < this.terminalInstances.length; i++) {
if (this.terminalInstances[i].title) {
title += `, ${this.terminalInstances[i].title}`;
}
}
return title;
}
public setVisible(visible: boolean): void {
if (this._tabElement) {
this._tabElement.style.display = visible ? '' : 'none';
}
this.terminalInstances.forEach(i => i.setVisible(visible));
}
public split(
terminalFocusContextKey: IContextKey<boolean>,
configHelper: ITerminalConfigHelper,
shellLaunchConfig: IShellLaunchConfig
): ITerminalInstance | undefined {
const newTerminalSize = ((this._panelPosition === Position.BOTTOM ? this._container.clientWidth : this._container.clientHeight) / (this._terminalInstances.length + 1));
if (newTerminalSize < TERMINAL_MIN_USEFUL_SIZE) {
return undefined;
}
const instance = this._terminalService.createInstance(
terminalFocusContextKey,
configHelper,
undefined,
shellLaunchConfig,
true);
this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance);
this._initInstanceListeners(instance);
this._setActiveInstance(instance);
if (this._splitPaneContainer) {
this._splitPaneContainer.split(instance, this._activeInstanceIndex);
}
return instance;
}
public addDisposable(disposable: IDisposable): void {
this._register(disposable);
}
public layout(width: number, height: number): void {
if (this._splitPaneContainer) {
// Check if the panel position changed and rotate panes if so
const newPanelPosition = this._layoutService.getPanelPosition();
const panelPositionChanged = newPanelPosition !== this._panelPosition;
if (panelPositionChanged) {
const newOrientation = newPanelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL;
this._splitPaneContainer.setOrientation(newOrientation);
this._panelPosition = newPanelPosition;
}
this._splitPaneContainer.layout(width, height);
}
}
public focusPreviousPane(): void {
const newIndex = this._activeInstanceIndex === 0 ? this._terminalInstances.length - 1 : this._activeInstanceIndex - 1;
this.setActiveInstanceByIndex(newIndex);
}
public focusNextPane(): void {
const newIndex = this._activeInstanceIndex === this._terminalInstances.length - 1 ? 0 : this._activeInstanceIndex + 1;
this.setActiveInstanceByIndex(newIndex);
}
public resizePane(direction: Direction): void {
if (!this._splitPaneContainer) {
return;
}
const isHorizontal = (direction === Direction.Left || direction === Direction.Right);
const font = this._terminalService.configHelper.getFont();
// TODO: Support letter spacing and line height
const amount = isHorizontal ? font.charWidth : font.charHeight;
if (amount) {
this._splitPaneContainer.resizePane(this._activeInstanceIndex, direction, amount);
}
}
}

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
const WIDGET_HEIGHT = 29;
export class TerminalWidgetManager implements IDisposable {
private _container: HTMLElement | null;
private _xtermViewport: HTMLElement | null;
private _messageWidget: MessageWidget;
private _messageListeners: IDisposable[] = [];
constructor(
terminalWrapper: HTMLElement
) {
this._container = document.createElement('div');
this._container.classList.add('terminal-widget-overlay');
terminalWrapper.appendChild(this._container);
this._initTerminalHeightWatcher(terminalWrapper);
}
public dispose(): void {
if (this._container && this._container.parentElement) {
this._container.parentElement.removeChild(this._container);
this._container = null;
}
this._xtermViewport = null;
}
private _initTerminalHeightWatcher(terminalWrapper: HTMLElement) {
// Watch the xterm.js viewport for style changes and do a layout if it changes
this._xtermViewport = <HTMLElement>terminalWrapper.querySelector('.xterm-viewport');
if (!this._xtermViewport) {
return;
}
const mutationObserver = new MutationObserver(() => this._refreshHeight());
mutationObserver.observe(this._xtermViewport, { attributes: true, attributeFilter: ['style'] });
}
public showMessage(left: number, top: number, text: string): void {
if (!this._container) {
return;
}
dispose(this._messageWidget);
this._messageListeners = dispose(this._messageListeners);
this._messageWidget = new MessageWidget(this._container, left, top, text);
}
public closeMessage(): void {
this._messageListeners = dispose(this._messageListeners);
if (this._messageWidget) {
this._messageListeners.push(MessageWidget.fadeOut(this._messageWidget));
}
}
private _refreshHeight(): void {
if (!this._container || !this._xtermViewport) {
return;
}
this._container.style.height = this._xtermViewport.style.height;
}
}
class MessageWidget {
private _domNode: HTMLDivElement;
public get left(): number { return this._left; }
public get top(): number { return this._top; }
public get text(): string { return this._text; }
public get domNode(): HTMLElement { return this._domNode; }
public static fadeOut(messageWidget: MessageWidget): IDisposable {
let handle: any;
const dispose = () => {
messageWidget.dispose();
clearTimeout(handle);
messageWidget.domNode.removeEventListener('animationend', dispose);
};
handle = setTimeout(dispose, 110);
messageWidget.domNode.addEventListener('animationend', dispose);
messageWidget.domNode.classList.add('fadeOut');
return { dispose };
}
constructor(
private _container: HTMLElement,
private _left: number,
private _top: number,
private _text: string
) {
this._domNode = document.createElement('div');
this._domNode.style.position = 'absolute';
this._domNode.style.left = `${_left}px`;
this._domNode.style.bottom = `${_container.offsetHeight - Math.max(_top, WIDGET_HEIGHT)}px`;
this._domNode.classList.add('terminal-message-widget', 'fadeIn');
this._domNode.textContent = _text;
this._container.appendChild(this._domNode);
}
public dispose(): void {
if (this.domNode.parentElement === this._container) {
this._container.removeChild(this.domNode);
}
}
}

View File

@@ -0,0 +1,736 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import { RawContextKey, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
export const TERMINAL_PANEL_ID = 'workbench.panel.terminal';
/** A context key that is set when there is at least one opened integrated terminal. */
export const KEYBINDING_CONTEXT_TERMINAL_IS_OPEN = new RawContextKey<boolean>('terminalIsOpen', false);
/** A context key that is set when the integrated terminal has focus. */
export const KEYBINDING_CONTEXT_TERMINAL_FOCUS = new RawContextKey<boolean>('terminalFocus', false);
/** A context key that is set when the integrated terminal does not have focus. */
export const KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FOCUS.toNegated();
/** A keybinding context key that is set when the integrated terminal has text selected. */
export const KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED = new RawContextKey<boolean>('terminalTextSelected', false);
/** A keybinding context key that is set when the integrated terminal does not have text selected. */
export const KEYBINDING_CONTEXT_TERMINAL_TEXT_NOT_SELECTED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED.toNegated();
/** A context key that is set when the find widget in integrated terminal is visible. */
export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE = new RawContextKey<boolean>('terminalFindWidgetVisible', false);
/** A context key that is set when the find widget in integrated terminal is not visible. */
export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE.toNegated();
/** A context key that is set when the find widget find input in integrated terminal is focused. */
export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED = new RawContextKey<boolean>('terminalFindWidgetInputFocused', false);
/** A context key that is set when the find widget in integrated terminal is focused. */
export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED = new RawContextKey<boolean>('terminalFindWidgetFocused', false);
/** A context key that is set when the find widget find input in integrated terminal is not focused. */
export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED.toNegated();
export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWorkspaceShellAllowed';
export const NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY = 'terminal.integrated.neverSuggestSelectWindowsShell';
export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime';
// The creation of extension host terminals is delayed by this value (milliseconds). The purpose of
// this delay is to allow the terminal instance to initialize correctly and have its ID set before
// trying to create the corressponding object on the ext host.
export const EXT_HOST_CREATION_DELAY = 100;
export const ITerminalService = createDecorator<ITerminalService>('terminalService');
export const TerminalCursorStyle = {
BLOCK: 'block',
LINE: 'line',
UNDERLINE: 'underline'
};
export const TERMINAL_CONFIG_SECTION = 'terminal.integrated';
export const DEFAULT_LETTER_SPACING = 0;
export const MINIMUM_LETTER_SPACING = -5;
export const DEFAULT_LINE_HEIGHT = 1;
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
export interface ITerminalConfiguration {
shell: {
linux: string;
osx: string;
windows: string;
};
shellArgs: {
linux: string[];
osx: string[];
windows: string[];
};
macOptionIsMeta: boolean;
macOptionClickForcesSelection: boolean;
rendererType: 'auto' | 'canvas' | 'dom';
rightClickBehavior: 'default' | 'copyPaste' | 'selectWord';
cursorBlinking: boolean;
cursorStyle: string;
drawBoldTextInBrightColors: boolean;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
// fontLigatures: boolean;
fontSize: number;
letterSpacing: number;
lineHeight: number;
setLocaleVariables: boolean;
scrollback: number;
commandsToSkipShell: string[];
cwd: string;
confirmOnExit: boolean;
enableBell: boolean;
env: {
linux: { [key: string]: string };
osx: { [key: string]: string };
windows: { [key: string]: string };
};
showExitAlert: boolean;
experimentalBufferImpl: 'JsArray' | 'TypedArray';
splitCwd: 'workspaceRoot' | 'initial' | 'inherited';
windowsEnableConpty: boolean;
}
export interface ITerminalConfigHelper {
config: ITerminalConfiguration;
configFontIsMonospace(): boolean;
getFont(): ITerminalFont;
/**
* Merges the default shell path and args into the provided launch configuration
*/
mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, platformOverride?: platform.Platform): void;
/** Sets whether a workspace shell configuration is allowed or not */
setWorkspaceShellAllowed(isAllowed: boolean): void;
checkWorkspaceShellPermissions(platformOverride?: platform.Platform): boolean;
}
export interface ITerminalFont {
fontFamily: string;
fontSize: number;
letterSpacing: number;
lineHeight: number;
charWidth?: number;
charHeight?: number;
}
export interface ITerminalEnvironment {
[key: string]: string | null;
}
export interface IShellLaunchConfig {
/**
* The name of the terminal, if this is not set the name of the process will be used.
*/
name?: string;
/**
* The shell executable (bash, cmd, etc.).
*/
executable?: string;
/**
* The CLI arguments to use with executable, a string[] is in argv format and will be escaped,
* a string is in "CommandLine" pre-escaped format and will be used as is. The string option is
* only supported on Windows and will throw an exception if used on macOS or Linux.
*/
args?: string[] | string;
/**
* The current working directory of the terminal, this overrides the `terminal.integrated.cwd`
* settings key.
*/
cwd?: string | URI;
/**
* A custom environment for the terminal, if this is not set the environment will be inherited
* from the VS Code process.
*/
env?: ITerminalEnvironment;
/**
* Whether to ignore a custom cwd from the `terminal.integrated.cwd` settings key (eg. if the
* shell is being launched by an extension).
*/
ignoreConfigurationCwd?: boolean;
/** Whether to wait for a key press before closing the terminal. */
waitOnExit?: boolean | string;
/**
* A string including ANSI escape sequences that will be written to the terminal emulator
* _before_ the terminal process has launched, a trailing \n is added at the end of the string.
* This allows for example the terminal instance to display a styled message as the first line
* of the terminal. Use \x1b over \033 or \e for the escape control character.
*/
initialText?: string;
/**
* When true the terminal will be created with no process. This is primarily used to give
* extensions full control over the terminal.
*/
isRendererOnly?: boolean;
/**
* Whether the terminal process environment should be exactly as provided in
* `TerminalOptions.env`. When this is false (default), the environment will be based on the
* window's environment and also apply configured platform settings like
* `terminal.integrated.windows.env` on top. When this is true, the complete environment must be
* provided as nothing will be inherited from the process or any configuration.
*/
strictEnv?: boolean;
}
export interface ITerminalService {
_serviceBrand: any;
activeTabIndex: number;
configHelper: ITerminalConfigHelper;
onActiveTabChanged: Event<void>;
onTabDisposed: Event<ITerminalTab>;
onInstanceCreated: Event<ITerminalInstance>;
onInstanceDisposed: Event<ITerminalInstance>;
onInstanceProcessIdReady: Event<ITerminalInstance>;
onInstanceDimensionsChanged: Event<ITerminalInstance>;
onInstanceRequestExtHostProcess: Event<ITerminalProcessExtHostRequest>;
onInstancesChanged: Event<void>;
onInstanceTitleChanged: Event<ITerminalInstance>;
onActiveInstanceChanged: Event<ITerminalInstance | undefined>;
terminalInstances: ITerminalInstance[];
terminalTabs: ITerminalTab[];
/**
* Creates a terminal.
* @param shell The shell launch configuration to use.
* @param wasNewTerminalAction Whether this was triggered by a new terminal action, if so a
* default shell selection dialog may display.
*/
createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
/**
* Creates a terminal renderer.
* @param name The name of the terminal.
*/
createTerminalRenderer(name: string): ITerminalInstance;
/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
*/
createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance;
getInstanceFromId(terminalId: number): ITerminalInstance;
getInstanceFromIndex(terminalIndex: number): ITerminalInstance;
getTabLabels(): string[];
getActiveInstance(): ITerminalInstance | null;
setActiveInstance(terminalInstance: ITerminalInstance): void;
setActiveInstanceByIndex(terminalIndex: number): void;
getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance;
splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig): ITerminalInstance | null;
getActiveTab(): ITerminalTab | null;
setActiveTabToNext(): void;
setActiveTabToPrevious(): void;
setActiveTabByIndex(tabIndex: number): void;
showPanel(focus?: boolean): Promise<void>;
hidePanel(): void;
focusFindWidget(): Promise<void>;
hideFindWidget(): void;
getFindState(): FindReplaceState;
findNext(): void;
findPrevious(): void;
setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
selectDefaultWindowsShell(): Promise<string | undefined>;
setWorkspaceShellAllowed(isAllowed: boolean): void;
/**
* Takes a path and returns the properly escaped path to send to the terminal.
* On Windows, this included trying to prepare the path for WSL if needed.
*
* @param executable The executable off the shellLaunchConfig
* @param title The terminal's title
* @param path The path to be escaped and formatted.
* @returns An escaped version of the path to be execuded in the terminal.
*/
preparePathForTerminalAsync(path: string, executable: string | undefined, title: string): Promise<string>;
requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI, cols: number, rows: number): void;
}
export const enum Direction {
Left = 0,
Right = 1,
Up = 2,
Down = 3
}
export interface ITerminalTab {
activeInstance: ITerminalInstance | null;
terminalInstances: ITerminalInstance[];
title: string;
onDisposed: Event<ITerminalTab>;
onInstancesChanged: Event<void>;
focusPreviousPane(): void;
focusNextPane(): void;
resizePane(direction: Direction): void;
setActiveInstanceByIndex(index: number): void;
attachToElement(element: HTMLElement): void;
setVisible(visible: boolean): void;
layout(width: number, height: number): void;
addDisposable(disposable: IDisposable): void;
split(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance | undefined;
}
export interface ITerminalDimensions {
/**
* The columns of the terminal.
*/
readonly cols: number;
/**
* The rows of the terminal.
*/
readonly rows: number;
}
interface ISearchOptions {
/**
* Whether the find should be done as a regex.
*/
regex?: boolean;
/**
* Whether only whole words should match.
*/
wholeWord?: boolean;
/**
* Whether find should pay attention to case.
*/
caseSensitive?: boolean;
/**
* Whether the search should start at the current search position (not the next row)
*/
incremental?: boolean;
}
export interface ITerminalInstance {
/**
* The ID of the terminal instance, this is an arbitrary number only used to identify the
* terminal instance.
*/
readonly id: number;
readonly cols: number;
readonly rows: number;
/**
* The process ID of the shell process, this is undefined when there is no process associated
* with this terminal.
*/
processId: number | undefined;
/**
* An event that fires when the terminal instance's title changes.
*/
onTitleChanged: Event<ITerminalInstance>;
/**
* An event that fires when the terminal instance is disposed.
*/
onDisposed: Event<ITerminalInstance>;
onFocused: Event<ITerminalInstance>;
onProcessIdReady: Event<ITerminalInstance>;
onRequestExtHostProcess: Event<ITerminalInstance>;
onDimensionsChanged: Event<void>;
onFocus: Event<ITerminalInstance>;
/**
* Attach a listener to the raw data stream coming from the pty, including ANSI escape
* sequences.
*/
onData: Event<string>;
/**
* Attach a listener to the "renderer" input event, this event fires for terminal renderers on
* keystrokes and when the Terminal.sendText extension API is used.
* @param listener The listener function.
*/
onRendererInput: Event<string>;
/**
* Attach a listener to listen for new lines added to this terminal instance.
*
* @param listener The listener function which takes new line strings added to the terminal,
* excluding ANSI escape sequences. The line event will fire when an LF character is added to
* the terminal (ie. the line is not wrapped). Note that this means that the line data will
* not fire for the last line, until either the line is ended with a LF character of the process
* is exited. The lineData string will contain the fully wrapped line, not containing any LF/CR
* characters.
*/
onLineData: Event<string>;
/**
* Attach a listener that fires when the terminal's pty process exits. The number in the event
* is the processes' exit code, an exit code of null means the process was killed as a result of
* the ITerminalInstance being disposed.
*/
onExit: Event<number>;
processReady: Promise<void>;
/**
* The title of the terminal. This is either title or the process currently running or an
* explicit name given to the terminal instance through the extension API.
*/
readonly title: string;
/**
* The focus state of the terminal before exiting.
*/
readonly hadFocusOnExit: boolean;
/**
* False when the title is set by an API or the user. We check this to make sure we
* do not override the title when the process title changes in the terminal.
*/
isTitleSetByProcess: boolean;
/**
* The shell launch config used to launch the shell.
*/
readonly shellLaunchConfig: IShellLaunchConfig;
/**
* Whether to disable layout for the terminal. This is useful when the size of the terminal is
* being manipulating (eg. adding a split pane) and we want the terminal to ignore particular
* resize events.
*/
disableLayout: boolean;
/**
* An object that tracks when commands are run and enables navigating and selecting between
* them.
*/
readonly commandTracker: ITerminalCommandTracker;
/**
* Dispose the terminal instance, removing it from the panel/service and freeing up resources.
*
* @param immediate Whether the kill should be immediate or not. Immediate should only be used
* when VS Code is shutting down or in cases where the terminal dispose was user initiated.
* The immediate===false exists to cover an edge case where the final output of the terminal can
* get cut off. If immediate kill any terminal processes immediately.
*/
dispose(immediate?: boolean): void;
/**
* Indicates that a consumer of a renderer only terminal is finished with it.
*
* @param exitCode The exit code of the terminal. Zero indicates success, non-zero indicates
* failure.
*/
rendererExit(exitCode: number): void;
/**
* Forces the terminal to redraw its viewport.
*/
forceRedraw(): void;
/**
* Registers a link matcher, allowing custom link patterns to be matched and handled.
* @param regex The regular expression the search for, specifically this searches the
* textContent of the rows. You will want to use \s to match a space ' ' character for example.
* @param handler The callback when the link is called.
* @param matchIndex The index of the link from the regex.match(html) call. This defaults to 0
* (for regular expressions without capture groups).
* @param validationCallback A callback which can be used to validate the link after it has been
* added to the DOM.
* @return The ID of the new matcher, this can be used to deregister.
*/
registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void): number;
/**
* Deregisters a link matcher if it has been registered.
* @param matcherId The link matcher's ID (returned after register)
* @return Whether a link matcher was found and deregistered.
*/
deregisterLinkMatcher(matcherId: number): void;
/**
* Check if anything is selected in terminal.
*/
hasSelection(): boolean;
/**
* Copies the terminal selection to the clipboard.
*/
copySelection(): void;
/**
* Current selection in the terminal.
*/
readonly selection: string | undefined;
/**
* Clear current selection.
*/
clearSelection(): void;
/**
* Select all text in the terminal.
*/
selectAll(): void;
/**
* Find the next instance of the term
*/
findNext(term: string, searchOptions: ISearchOptions): boolean;
/**
* Find the previous instance of the term
*/
findPrevious(term: string, searchOptions: ISearchOptions): boolean;
/**
* Notifies the terminal that the find widget's focus state has been changed.
*/
notifyFindWidgetFocusChanged(isFocused: boolean): void;
/**
* Focuses the terminal instance if it's able to (xterm.js instance exists).
*
* @param focus Force focus even if there is a selection.
*/
focus(force?: boolean): void;
/**
* Focuses the terminal instance when it's ready (the xterm.js instance is created). Use this
* when the terminal is being shown.
*
* @param focus Force focus even if there is a selection.
*/
focusWhenReady(force?: boolean): Promise<void>;
/**
* Focuses and pastes the contents of the clipboard into the terminal instance.
*/
paste(): void;
/**
* Send text to the terminal instance. The text is written to the stdin of the underlying pty
* process (shell) of the terminal instance.
*
* @param text The text to send.
* @param addNewLine Whether to add a new line to the text being sent, this is normally
* required to run a command in the terminal. The character(s) added are \n or \r\n
* depending on the platform. This defaults to `true`.
*/
sendText(text: string, addNewLine: boolean): void;
/**
* Write text directly to the terminal, skipping the process if it exists.
* @param text The text to write.
*/
write(text: string): void;
/** Scroll the terminal buffer down 1 line. */
scrollDownLine(): void;
/** Scroll the terminal buffer down 1 page. */
scrollDownPage(): void;
/** Scroll the terminal buffer to the bottom. */
scrollToBottom(): void;
/** Scroll the terminal buffer up 1 line. */
scrollUpLine(): void;
/** Scroll the terminal buffer up 1 page. */
scrollUpPage(): void;
/** Scroll the terminal buffer to the top. */
scrollToTop(): void;
/**
* Clears the terminal buffer, leaving only the prompt line.
*/
clear(): void;
/**
* Attaches the terminal instance to an element on the DOM, before this is called the terminal
* instance process may run in the background but cannot be displayed on the UI.
*
* @param container The element to attach the terminal instance to.
*/
attachToElement(container: HTMLElement): void;
/**
* Updates the configuration of the terminal instance.
*/
updateConfig(): void;
/**
* Updates the accessibility support state of the terminal instance.
* @param isEnabled Whether it's enabled.
*/
updateAccessibilitySupport(isEnabled: boolean): void;
/**
* Configure the dimensions of the terminal instance.
*
* @param dimension The dimensions of the container.
*/
layout(dimension: { width: number, height: number }): void;
/**
* Sets whether the terminal instance's element is visible in the DOM.
*
* @param visible Whether the element is visible.
*/
setVisible(visible: boolean): void;
/**
* Immediately kills the terminal's current pty process and launches a new one to replace it.
*
* @param shell The new launch configuration.
*/
reuseTerminal(shell: IShellLaunchConfig): void;
/**
* Sets the title of the terminal instance.
*/
setTitle(title: string, eventFromProcess: boolean): void;
waitForTitle(): Promise<string>;
setDimensions(dimensions: ITerminalDimensions): void;
addDisposable(disposable: IDisposable): void;
toggleEscapeSequenceLogging(): void;
getInitialCwd(): Promise<string>;
getCwd(): Promise<string>;
}
export interface ITerminalCommandTracker {
scrollToPreviousCommand(): void;
scrollToNextCommand(): void;
selectToPreviousCommand(): void;
selectToNextCommand(): void;
selectToPreviousLine(): void;
selectToNextLine(): void;
}
export interface ITerminalProcessManager extends IDisposable {
readonly processState: ProcessState;
readonly ptyProcessReady: Promise<void>;
readonly shellProcessId: number;
readonly remoteAuthority: string | undefined;
readonly os: platform.OperatingSystem | undefined;
readonly userHome: string | undefined;
readonly onProcessReady: Event<void>;
readonly onProcessData: Event<string>;
readonly onProcessTitle: Event<string>;
readonly onProcessExit: Event<number>;
addDisposable(disposable: IDisposable);
dispose(immediate?: boolean);
createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number);
write(data: string): void;
setDimensions(cols: number, rows: number): void;
getInitialCwd(): Promise<string>;
getCwd(): Promise<string>;
}
export const enum ProcessState {
// The process has not been initialized yet.
UNINITIALIZED,
// The process is currently launching, the process is marked as launching
// for a short duration after being created and is helpful to indicate
// whether the process died as a result of bad shell and args.
LAUNCHING,
// The process is running normally.
RUNNING,
// The process was killed during launch, likely as a result of bad shell and
// args.
KILLED_DURING_LAUNCH,
// The process was killed by the user (the event originated from VS Code).
KILLED_BY_USER,
// The process was killed by itself, for example the shell crashed or `exit`
// was run.
KILLED_BY_PROCESS
}
export interface ITerminalProcessExtHostProxy extends IDisposable {
readonly terminalId: number;
emitData(data: string): void;
emitTitle(title: string): void;
emitPid(pid: number): void;
emitExit(exitCode: number): void;
emitInitialCwd(initialCwd: string): void;
emitCwd(cwd: string): void;
onInput: Event<string>;
onResize: Event<{ cols: number, rows: number }>;
onShutdown: Event<boolean>;
onRequestInitialCwd: Event<void>;
onRequestCwd: Event<void>;
}
export interface ITerminalProcessExtHostRequest {
proxy: ITerminalProcessExtHostProxy;
shellLaunchConfig: IShellLaunchConfig;
activeWorkspaceRootUri: URI;
cols: number;
rows: number;
}
export enum LinuxDistro {
Fedora,
Ubuntu,
Unknown
}
export interface IWindowsShellHelper extends IDisposable {
getShellName(): Promise<string>;
}
/**
* An interface representing a raw terminal child process, this contains a subset of the
* child_process.ChildProcess node.js interface.
*/
export interface ITerminalChildProcess {
onProcessData: Event<string>;
onProcessExit: Event<number>;
onProcessIdReady: Event<number>;
onProcessTitleChanged: Event<string>;
/**
* Shutdown the terminal process.
*
* @param immediate When true the process will be killed immediately, otherwise the process will
* be given some time to make sure no additional data comes through.
*/
shutdown(immediate: boolean): void;
input(data: string): void;
resize(cols: number, rows: number): void;
getInitialCwd(): Promise<string>;
getCwd(): Promise<string>;
}

View File

@@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { registerColor, ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { PANEL_BORDER, PANEL_BACKGROUND } from 'vs/workbench/common/theme';
/**
* The color identifiers for the terminal's ansi colors. The index in the array corresponds to the index
* of the color in the terminal color table.
*/
export const ansiColorIdentifiers: ColorIdentifier[] = [];
export const TERMINAL_BACKGROUND_COLOR = registerColor('terminal.background', {
dark: PANEL_BACKGROUND,
light: PANEL_BACKGROUND,
hc: PANEL_BACKGROUND
}, nls.localize('terminal.background', 'The background color of the terminal, this allows coloring the terminal differently to the panel.'));
export const TERMINAL_FOREGROUND_COLOR = registerColor('terminal.foreground', {
light: '#333333',
dark: '#CCCCCC',
hc: '#FFFFFF'
}, nls.localize('terminal.foreground', 'The foreground color of the terminal.'));
export const TERMINAL_CURSOR_FOREGROUND_COLOR = registerColor('terminalCursor.foreground', null, nls.localize('terminalCursor.foreground', 'The foreground color of the terminal cursor.'));
export const TERMINAL_CURSOR_BACKGROUND_COLOR = registerColor('terminalCursor.background', null, nls.localize('terminalCursor.background', 'The background color of the terminal cursor. Allows customizing the color of a character overlapped by a block cursor.'));
export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', {
light: '#00000040',
dark: '#FFFFFF40',
hc: '#FFFFFF80'
}, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.'));
export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', {
dark: PANEL_BORDER,
light: PANEL_BORDER,
hc: PANEL_BORDER
}, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.'));
const ansiColorMap = {
'terminal.ansiBlack': {
index: 0,
defaults: {
light: '#000000',
dark: '#000000',
hc: '#000000'
}
},
'terminal.ansiRed': {
index: 1,
defaults: {
light: '#cd3131',
dark: '#cd3131',
hc: '#cd0000'
}
},
'terminal.ansiGreen': {
index: 2,
defaults: {
light: '#00BC00',
dark: '#0DBC79',
hc: '#00cd00'
}
},
'terminal.ansiYellow': {
index: 3,
defaults: {
light: '#949800',
dark: '#e5e510',
hc: '#cdcd00'
}
},
'terminal.ansiBlue': {
index: 4,
defaults: {
light: '#0451a5',
dark: '#2472c8',
hc: '#0000ee'
}
},
'terminal.ansiMagenta': {
index: 5,
defaults: {
light: '#bc05bc',
dark: '#bc3fbc',
hc: '#cd00cd'
}
},
'terminal.ansiCyan': {
index: 6,
defaults: {
light: '#0598bc',
dark: '#11a8cd',
hc: '#00cdcd'
}
},
'terminal.ansiWhite': {
index: 7,
defaults: {
light: '#555555',
dark: '#e5e5e5',
hc: '#e5e5e5'
}
},
'terminal.ansiBrightBlack': {
index: 8,
defaults: {
light: '#666666',
dark: '#666666',
hc: '#7f7f7f'
}
},
'terminal.ansiBrightRed': {
index: 9,
defaults: {
light: '#cd3131',
dark: '#f14c4c',
hc: '#ff0000'
}
},
'terminal.ansiBrightGreen': {
index: 10,
defaults: {
light: '#14CE14',
dark: '#23d18b',
hc: '#00ff00'
}
},
'terminal.ansiBrightYellow': {
index: 11,
defaults: {
light: '#b5ba00',
dark: '#f5f543',
hc: '#ffff00'
}
},
'terminal.ansiBrightBlue': {
index: 12,
defaults: {
light: '#0451a5',
dark: '#3b8eea',
hc: '#5c5cff'
}
},
'terminal.ansiBrightMagenta': {
index: 13,
defaults: {
light: '#bc05bc',
dark: '#d670d6',
hc: '#ff00ff'
}
},
'terminal.ansiBrightCyan': {
index: 14,
defaults: {
light: '#0598bc',
dark: '#29b8db',
hc: '#00ffff'
}
},
'terminal.ansiBrightWhite': {
index: 15,
defaults: {
light: '#a5a5a5',
dark: '#e5e5e5',
hc: '#ffffff'
}
}
};
export function registerColors(): void {
for (const id in ansiColorMap) {
const entry = ansiColorMap[id];
const colorName = id.substring(13);
ansiColorIdentifiers[entry.index] = registerColor(id, entry.defaults, nls.localize('terminal.ansiColor', '\'{0}\' ANSI color in the terminal.', colorName));
}
}

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ITerminalService } from 'vs/workbench/contrib/terminal/common/terminal';
export const enum TERMINAL_COMMAND_ID {
FIND_NEXT = 'workbench.action.terminal.findNext',
FIND_NEXT_TERMINAL_FOCUS = 'workbench.action.terminal.findNextTerminalFocus',
FIND_PREVIOUS = 'workbench.action.terminal.findPrevious',
FIND_PREVIOUS_TERMINAL_FOCUS = 'workbench.action.terminal.findPreviousTerminalFocus',
TOGGLE = 'workbench.action.terminal.toggleTerminal',
KILL = 'workbench.action.terminal.kill',
QUICK_KILL = 'workbench.action.terminal.quickKill',
COPY_SELECTION = 'workbench.action.terminal.copySelection',
SELECT_ALL = 'workbench.action.terminal.selectAll',
DELETE_WORD_LEFT = 'workbench.action.terminal.deleteWordLeft',
DELETE_WORD_RIGHT = 'workbench.action.terminal.deleteWordRight',
DELETE_TO_LINE_START = 'workbench.action.terminal.deleteToLineStart',
MOVE_TO_LINE_START = 'workbench.action.terminal.moveToLineStart',
MOVE_TO_LINE_END = 'workbench.action.terminal.moveToLineEnd',
NEW = 'workbench.action.terminal.new',
NEW_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.newInActiveWorkspace',
SPLIT = 'workbench.action.terminal.split',
SPLIT_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.splitInActiveWorkspace',
FOCUS_PREVIOUS_PANE = 'workbench.action.terminal.focusPreviousPane',
FOCUS_NEXT_PANE = 'workbench.action.terminal.focusNextPane',
RESIZE_PANE_LEFT = 'workbench.action.terminal.resizePaneLeft',
RESIZE_PANE_RIGHT = 'workbench.action.terminal.resizePaneRight',
RESIZE_PANE_UP = 'workbench.action.terminal.resizePaneUp',
RESIZE_PANE_DOWN = 'workbench.action.terminal.resizePaneDown',
FOCUS = 'workbench.action.terminal.focus',
FOCUS_NEXT = 'workbench.action.terminal.focusNext',
FOCUS_PREVIOUS = 'workbench.action.terminal.focusPrevious',
PASTE = 'workbench.action.terminal.paste',
SELECT_DEFAULT_SHELL = 'workbench.action.terminal.selectDefaultShell',
RUN_SELECTED_TEXT = 'workbench.action.terminal.runSelectedText',
RUN_ACTIVE_FILE = 'workbench.action.terminal.runActiveFile',
SWITCH_TERMINAL = 'workbench.action.terminal.switchTerminal',
SCROLL_DOWN_LINE = 'workbench.action.terminal.scrollDown',
SCROLL_DOWN_PAGE = 'workbench.action.terminal.scrollDownPage',
SCROLL_TO_BOTTOM = 'workbench.action.terminal.scrollToBottom',
SCROLL_UP_LINE = 'workbench.action.terminal.scrollUp',
SCROLL_UP_PAGE = 'workbench.action.terminal.scrollUpPage',
SCROLL_TO_TOP = 'workbench.action.terminal.scrollToTop',
CLEAR = 'workbench.action.terminal.clear',
CLEAR_SELECTION = 'workbench.action.terminal.clearSelection',
WORKSPACE_SHELL_ALLOW = 'workbench.action.terminal.allowWorkspaceShell',
WORKSPACE_SHELL_DISALLOW = 'workbench.action.terminal.disallowWorkspaceShell',
RENAME = 'workbench.action.terminal.rename',
FIND_WIDGET_FOCUS = 'workbench.action.terminal.focusFindWidget',
FIND_WIDGET_HIDE = 'workbench.action.terminal.hideFindWidget',
QUICK_OPEN_TERM = 'workbench.action.quickOpenTerm',
SCROLL_TO_PREVIOUS_COMMAND = 'workbench.action.terminal.scrollToPreviousCommand',
SCROLL_TO_NEXT_COMMAND = 'workbench.action.terminal.scrollToNextCommand',
SELECT_TO_PREVIOUS_COMMAND = 'workbench.action.terminal.selectToPreviousCommand',
SELECT_TO_NEXT_COMMAND = 'workbench.action.terminal.selectToNextCommand',
SELECT_TO_PREVIOUS_LINE = 'workbench.action.terminal.selectToPreviousLine',
SELECT_TO_NEXT_LINE = 'workbench.action.terminal.selectToNextLine',
TOGGLE_ESCAPE_SEQUENCE_LOGGING = 'toggleEscapeSequenceLogging',
SEND_SEQUENCE = 'workbench.action.terminal.sendSequence',
TOGGLE_FIND_REGEX = 'workbench.action.terminal.toggleFindRegex',
TOGGLE_FIND_WHOLE_WORD = 'workbench.action.terminal.toggleFindWholeWord',
TOGGLE_FIND_CASE_SENSITIVE = 'workbench.action.terminal.toggleFindCaseSensitive',
TOGGLE_FIND_REGEX_TERMINAL_FOCUS = 'workbench.action.terminal.toggleFindRegexTerminalFocus',
TOGGLE_FIND_WHOLE_WORD_TERMINAL_FOCUS = 'workbench.action.terminal.toggleFindWholeWordTerminalFocus',
TOGGLE_FIND_CASE_SENSITIVE_TERMINAL_FOCUS = 'workbench.action.terminal.toggleFindCaseSensitiveTerminalFocus',
}
export function setupTerminalCommands(): void {
registerOpenTerminalAtIndexCommands();
}
function registerOpenTerminalAtIndexCommands(): void {
for (let i = 0; i < 9; i++) {
const terminalIndex = i;
const visibleIndex = i + 1;
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: `workbench.action.terminal.focusAtIndex${visibleIndex}`,
weight: KeybindingWeight.WorkbenchContrib,
when: undefined,
primary: 0,
handler: accessor => {
const terminalService = accessor.get(ITerminalService);
terminalService.setActiveInstanceByIndex(terminalIndex);
return terminalService.showPanel(true);
}
});
}
}

View File

@@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { URI as Uri } from 'vs/base/common/uri';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/workbench/contrib/terminal/common/terminal';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
/**
* This module contains utility functions related to the environment, cwd and paths.
*/
export function mergeEnvironments(parent: platform.IProcessEnvironment, other?: ITerminalEnvironment): void {
if (!other) {
return;
}
// On Windows apply the new values ignoring case, while still retaining
// the case of the original key.
if (platform.isWindows) {
for (const configKey in other) {
let actualKey = configKey;
for (const envKey in parent) {
if (configKey.toLowerCase() === envKey.toLowerCase()) {
actualKey = envKey;
break;
}
}
const value = other[configKey];
_mergeEnvironmentValue(parent, actualKey, value);
}
} else {
Object.keys(other).forEach((key) => {
const value = other[key];
_mergeEnvironmentValue(parent, key, value);
});
}
}
function _mergeEnvironmentValue(env: ITerminalEnvironment, key: string, value: string | null): void {
if (typeof value === 'string') {
env[key] = value;
} else {
delete env[key];
}
}
export function addTerminalEnvironmentKeys(env: ITerminalEnvironment, version: string | undefined, locale: string | undefined, setLocaleVariables: boolean): void {
env['TERM_PROGRAM'] = 'vscode';
env['TERM_PROGRAM_VERSION'] = version ? version : null;
if (setLocaleVariables) {
env['LANG'] = _getLangEnvVariable(locale);
}
}
export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: ITerminalEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder | null): ITerminalEnvironment {
Object.keys(env).forEach((key) => {
const value = env[key];
if (typeof value === 'string' && lastActiveWorkspaceRoot !== null) {
env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, value);
}
});
return env;
}
function _getLangEnvVariable(locale?: string) {
const parts = locale ? locale.split('-') : [];
const n = parts.length;
if (n === 0) {
// Fallback to en_US to prevent possible encoding issues.
return 'en_US.UTF-8';
}
if (n === 1) {
// app.getLocale can return just a language without a variant, fill in the variant for
// supported languages as many shells expect a 2-part locale.
const languageVariants = {
de: 'DE',
en: 'US',
es: 'ES',
fi: 'FI',
fr: 'FR',
it: 'IT',
ja: 'JP',
ko: 'KR',
pl: 'PL',
ru: 'RU',
zh: 'CN'
};
if (parts[0] in languageVariants) {
parts.push(languageVariants[parts[0]]);
}
} else {
// Ensure the variant is uppercase
parts[1] = parts[1].toUpperCase();
}
return parts.join('_') + '.UTF-8';
}
export function getCwd(shell: IShellLaunchConfig, userHome: string, root?: Uri, customCwd?: string): string {
if (shell.cwd) {
return (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd;
}
let cwd: string | undefined;
// TODO: Handle non-existent customCwd
if (!shell.ignoreConfigurationCwd && customCwd) {
if (path.isAbsolute(customCwd)) {
cwd = customCwd;
} else if (root) {
cwd = path.join(root.fsPath, customCwd);
}
}
// If there was no custom cwd or it was relative with no workspace
if (!cwd) {
cwd = root ? root.fsPath : userHome;
}
return _sanitizeCwd(cwd);
}
function _sanitizeCwd(cwd: string): string {
// Make the drive letter uppercase on Windows (see #9448)
if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') {
return cwd[0].toUpperCase() + cwd.substr(1);
}
return cwd;
}
export function escapeNonWindowsPath(path: string): string {
let newPath = path;
if (newPath.indexOf('\\') !== 0) {
newPath = newPath.replace(/\\/g, '\\\\');
}
if (!newPath && (newPath.indexOf('"') !== -1)) {
newPath = '\'' + newPath + '\'';
} else if (newPath.indexOf(' ') !== -1) {
newPath = newPath.replace(/ /g, '\\ ');
}
return newPath;
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminalCommands';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
export function setupTerminalMenu() {
// View menu
MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
group: '4_panels',
command: {
id: TERMINAL_COMMAND_ID.TOGGLE,
title: nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")
},
order: 3
});
// Manage
const createGroup = '1_create';
MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, {
group: createGroup,
command: {
id: TERMINAL_COMMAND_ID.NEW,
title: nls.localize({ key: 'miNewTerminal', comment: ['&& denotes a mnemonic'] }, "&&New Terminal")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, {
group: createGroup,
command: {
id: TERMINAL_COMMAND_ID.SPLIT,
title: nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal"),
precondition: ContextKeyExpr.has('terminalIsOpen')
},
order: 2
});
// Run
const runGroup = '2_run';
MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, {
group: runGroup,
command: {
id: TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE,
title: nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File")
},
order: 3
});
MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, {
group: runGroup,
command: {
id: TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT,
title: nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text")
},
order: 4
});
}

View File

@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { ITerminalService, ITerminalProcessExtHostProxy, IShellLaunchConfig, ITerminalChildProcess } from 'vs/workbench/contrib/terminal/common/terminal';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export class TerminalProcessExtHostProxy implements ITerminalChildProcess, ITerminalProcessExtHostProxy {
private _disposables: IDisposable[] = [];
private readonly _onProcessData = new Emitter<string>();
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessExit = new Emitter<number>();
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
private readonly _onProcessIdReady = new Emitter<number>();
public get onProcessIdReady(): Event<number> { return this._onProcessIdReady.event; }
private readonly _onProcessTitleChanged = new Emitter<string>();
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
private readonly _onInput = new Emitter<string>();
public get onInput(): Event<string> { return this._onInput.event; }
private readonly _onResize: Emitter<{ cols: number, rows: number }> = new Emitter<{ cols: number, rows: number }>();
public get onResize(): Event<{ cols: number, rows: number }> { return this._onResize.event; }
private readonly _onShutdown = new Emitter<boolean>();
public get onShutdown(): Event<boolean> { return this._onShutdown.event; }
private readonly _onRequestInitialCwd = new Emitter<void>();
public get onRequestInitialCwd(): Event<void> { return this._onRequestInitialCwd.event; }
private readonly _onRequestCwd = new Emitter<void>();
public get onRequestCwd(): Event<void> { return this._onRequestCwd.event; }
private _pendingInitialCwdRequests: ((value?: string | Thenable<string>) => void)[] = [];
private _pendingCwdRequests: ((value?: string | Thenable<string>) => void)[] = [];
constructor(
public terminalId: number,
shellLaunchConfig: IShellLaunchConfig,
activeWorkspaceRootUri: URI,
cols: number,
rows: number,
@ITerminalService private readonly _terminalService: ITerminalService,
@IExtensionService private readonly _extensionService: IExtensionService
) {
this._extensionService.whenInstalledExtensionsRegistered().then(() => {
// TODO: MainThreadTerminalService is not ready at this point, fix this
setTimeout(() => {
this._terminalService.requestExtHostProcess(this, shellLaunchConfig, activeWorkspaceRootUri, cols, rows);
}, 0);
});
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
public emitData(data: string): void {
this._onProcessData.fire(data);
}
public emitTitle(title: string): void {
this._onProcessTitleChanged.fire(title);
}
public emitPid(pid: number): void {
this._onProcessIdReady.fire(pid);
}
public emitExit(exitCode: number): void {
this._onProcessExit.fire(exitCode);
this.dispose();
}
public emitInitialCwd(initialCwd: string): void {
while (this._pendingInitialCwdRequests.length > 0) {
this._pendingInitialCwdRequests.pop()!(initialCwd);
}
}
public emitCwd(cwd: string): void {
while (this._pendingCwdRequests.length > 0) {
this._pendingCwdRequests.pop()!(cwd);
}
}
public shutdown(immediate: boolean): void {
this._onShutdown.fire(immediate);
}
public input(data: string): void {
this._onInput.fire(data);
}
public resize(cols: number, rows: number): void {
this._onResize.fire({ cols, rows });
}
public getInitialCwd(): Promise<string> {
return new Promise<string>(resolve => {
this._onRequestInitialCwd.fire();
this._pendingInitialCwdRequests.push(resolve);
});
}
public getCwd(): Promise<string> {
return new Promise<string>(resolve => {
this._onRequestCwd.fire();
this._pendingCwdRequests.push(resolve);
});
}
}

View File

@@ -0,0 +1,464 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN } from 'vs/workbench/contrib/terminal/common/terminal';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { isWindows } from 'vs/base/common/platform';
import { basename } from 'vs/base/common/path';
export abstract class TerminalService implements ITerminalService {
public _serviceBrand: any;
protected _isShuttingDown: boolean;
protected _terminalFocusContextKey: IContextKey<boolean>;
protected _findWidgetVisible: IContextKey<boolean>;
protected _terminalContainer: HTMLElement;
protected _terminalTabs: ITerminalTab[] = [];
protected get _terminalInstances(): ITerminalInstance[] {
return this._terminalTabs.reduce((p, c) => p.concat(c.terminalInstances), <ITerminalInstance[]>[]);
}
private _findState: FindReplaceState;
private _activeTabIndex: number;
public get activeTabIndex(): number { return this._activeTabIndex; }
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; }
private readonly _onActiveTabChanged = new Emitter<void>();
public get onActiveTabChanged(): Event<void> { return this._onActiveTabChanged.event; }
protected readonly _onInstanceCreated = new Emitter<ITerminalInstance>();
public get onInstanceCreated(): Event<ITerminalInstance> { return this._onInstanceCreated.event; }
protected readonly _onInstanceDisposed = new Emitter<ITerminalInstance>();
public get onInstanceDisposed(): Event<ITerminalInstance> { return this._onInstanceDisposed.event; }
protected readonly _onInstanceProcessIdReady = new Emitter<ITerminalInstance>();
public get onInstanceProcessIdReady(): Event<ITerminalInstance> { return this._onInstanceProcessIdReady.event; }
protected readonly _onInstanceRequestExtHostProcess = new Emitter<ITerminalProcessExtHostRequest>();
public get onInstanceRequestExtHostProcess(): Event<ITerminalProcessExtHostRequest> { return this._onInstanceRequestExtHostProcess.event; }
protected readonly _onInstanceDimensionsChanged = new Emitter<ITerminalInstance>();
public get onInstanceDimensionsChanged(): Event<ITerminalInstance> { return this._onInstanceDimensionsChanged.event; }
protected readonly _onInstancesChanged = new Emitter<void>();
public get onInstancesChanged(): Event<void> { return this._onInstancesChanged.event; }
protected readonly _onInstanceTitleChanged = new Emitter<ITerminalInstance>();
public get onInstanceTitleChanged(): Event<ITerminalInstance> { return this._onInstanceTitleChanged.event; }
protected readonly _onActiveInstanceChanged = new Emitter<ITerminalInstance | undefined>();
public get onActiveInstanceChanged(): Event<ITerminalInstance | undefined> { return this._onActiveInstanceChanged.event; }
protected readonly _onTabDisposed = new Emitter<ITerminalTab>();
public get onTabDisposed(): Event<ITerminalTab> { return this._onTabDisposed.event; }
public abstract get configHelper(): ITerminalConfigHelper;
constructor(
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IPanelService protected readonly _panelService: IPanelService,
@ILifecycleService lifecycleService: ILifecycleService,
@IStorageService protected readonly _storageService: IStorageService,
@INotificationService protected readonly _notificationService: INotificationService,
@IDialogService private readonly _dialogService: IDialogService,
@IExtensionService private readonly _extensionService: IExtensionService,
@IFileService private readonly _fileService: IFileService
) {
this._activeTabIndex = 0;
this._isShuttingDown = false;
this._findState = new FindReplaceState();
lifecycleService.onBeforeShutdown(event => event.veto(this._onBeforeShutdown()));
lifecycleService.onShutdown(() => this._onShutdown());
this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService);
this._findWidgetVisible = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService);
this.onTabDisposed(tab => this._removeTab(tab));
this.onActiveTabChanged(() => {
const instance = this.getActiveInstance();
this._onActiveInstanceChanged.fire(instance ? instance : undefined);
});
this._handleContextKeys();
}
private _handleContextKeys(): void {
const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService);
const updateTerminalContextKeys = () => {
terminalIsOpenContext.set(this.terminalInstances.length > 0);
};
this.onInstancesChanged(() => updateTerminalContextKeys());
}
protected abstract _getWslPath(path: string): Promise<string>;
protected abstract _getWindowsBuildNumber(): number;
public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
public abstract createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance;
public abstract selectDefaultWindowsShell(): Promise<string | undefined>;
public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
public createTerminalRenderer(name: string): ITerminalInstance {
return this.createTerminal({ name, isRendererOnly: true });
}
public getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance {
const activeInstance = this.getActiveInstance();
return activeInstance ? activeInstance : this.createTerminal(undefined, wasNewTerminalAction);
}
public requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI, cols: number, rows: number): void {
// Ensure extension host is ready before requesting a process
this._extensionService.whenInstalledExtensionsRegistered().then(() => {
// TODO: MainThreadTerminalService is not ready at this point, fix this
setTimeout(() => {
this._onInstanceRequestExtHostProcess.fire({ proxy, shellLaunchConfig, activeWorkspaceRootUri, cols, rows });
}, 500);
});
}
private _onBeforeShutdown(): boolean | Promise<boolean> {
if (this.terminalInstances.length === 0) {
// No terminal instances, don't veto
return false;
}
if (this.configHelper.config.confirmOnExit) {
// veto if configured to show confirmation and the user choosed not to exit
return this._showTerminalCloseConfirmation().then(veto => {
if (!veto) {
this._isShuttingDown = true;
}
return veto;
});
}
this._isShuttingDown = true;
return false;
}
private _onShutdown(): void {
// Dispose of all instances
this.terminalInstances.forEach(instance => instance.dispose(true));
}
public getTabLabels(): string[] {
return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => `${index + 1}: ${tab.title ? tab.title : ''}`);
}
public getFindState(): FindReplaceState {
return this._findState;
}
private _removeTab(tab: ITerminalTab): void {
// Get the index of the tab and remove it from the list
const index = this._terminalTabs.indexOf(tab);
const wasActiveTab = tab === this.getActiveTab();
if (index !== -1) {
this._terminalTabs.splice(index, 1);
}
// Adjust focus if the tab was active
if (wasActiveTab && this._terminalTabs.length > 0) {
// TODO: Only focus the new tab if the removed tab had focus?
// const hasFocusOnExit = tab.activeInstance.hadFocusOnExit;
const newIndex = index < this._terminalTabs.length ? index : this._terminalTabs.length - 1;
this.setActiveTabByIndex(newIndex);
const activeInstance = this.getActiveInstance();
if (activeInstance) {
activeInstance.focus(true);
}
}
// Hide the panel if there are no more instances, provided that VS Code is not shutting
// down. When shutting down the panel is locked in place so that it is restored upon next
// launch.
if (this._terminalTabs.length === 0 && !this._isShuttingDown) {
this.hidePanel();
this._onActiveInstanceChanged.fire(undefined);
}
// Fire events
this._onInstancesChanged.fire();
if (wasActiveTab) {
this._onActiveTabChanged.fire();
}
}
public getActiveTab(): ITerminalTab | null {
if (this._activeTabIndex < 0 || this._activeTabIndex >= this._terminalTabs.length) {
return null;
}
return this._terminalTabs[this._activeTabIndex];
}
public getActiveInstance(): ITerminalInstance | null {
const tab = this.getActiveTab();
if (!tab) {
return null;
}
return tab.activeInstance;
}
public getInstanceFromId(terminalId: number): ITerminalInstance {
return this.terminalInstances[this._getIndexFromId(terminalId)];
}
public getInstanceFromIndex(terminalIndex: number): ITerminalInstance {
return this.terminalInstances[terminalIndex];
}
public setActiveInstance(terminalInstance: ITerminalInstance): void {
this.setActiveInstanceByIndex(this._getIndexFromId(terminalInstance.id));
}
public setActiveTabByIndex(tabIndex: number): void {
if (tabIndex >= this._terminalTabs.length) {
return;
}
const didTabChange = this._activeTabIndex !== tabIndex;
this._activeTabIndex = tabIndex;
this._terminalTabs.forEach((t, i) => t.setVisible(i === this._activeTabIndex));
if (didTabChange) {
this._onActiveTabChanged.fire();
}
}
private _getInstanceFromGlobalInstanceIndex(index: number): { tab: ITerminalTab, tabIndex: number, instance: ITerminalInstance, localInstanceIndex: number } | null {
let currentTabIndex = 0;
while (index >= 0 && currentTabIndex < this._terminalTabs.length) {
const tab = this._terminalTabs[currentTabIndex];
const count = tab.terminalInstances.length;
if (index < count) {
return {
tab,
tabIndex: currentTabIndex,
instance: tab.terminalInstances[index],
localInstanceIndex: index
};
}
index -= count;
currentTabIndex++;
}
return null;
}
public setActiveInstanceByIndex(terminalIndex: number): void {
const query = this._getInstanceFromGlobalInstanceIndex(terminalIndex);
if (!query) {
return;
}
query.tab.setActiveInstanceByIndex(query.localInstanceIndex);
const didTabChange = this._activeTabIndex !== query.tabIndex;
this._activeTabIndex = query.tabIndex;
this._terminalTabs.forEach((t, i) => t.setVisible(i === query.tabIndex));
// Only fire the event if there was a change
if (didTabChange) {
this._onActiveTabChanged.fire();
}
}
public setActiveTabToNext(): void {
if (this._terminalTabs.length <= 1) {
return;
}
let newIndex = this._activeTabIndex + 1;
if (newIndex >= this._terminalTabs.length) {
newIndex = 0;
}
this.setActiveTabByIndex(newIndex);
}
public setActiveTabToPrevious(): void {
if (this._terminalTabs.length <= 1) {
return;
}
let newIndex = this._activeTabIndex - 1;
if (newIndex < 0) {
newIndex = this._terminalTabs.length - 1;
}
this.setActiveTabByIndex(newIndex);
}
public splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig: IShellLaunchConfig = {}): ITerminalInstance | null {
const tab = this._getTabForInstance(instanceToSplit);
if (!tab) {
return null;
}
const instance = tab.split(this._terminalFocusContextKey, this.configHelper, shellLaunchConfig);
if (!instance) {
this._showNotEnoughSpaceToast();
return null;
}
this._initInstanceListeners(instance);
this._onInstancesChanged.fire();
this._terminalTabs.forEach((t, i) => t.setVisible(i === this._activeTabIndex));
return instance;
}
protected _initInstanceListeners(instance: ITerminalInstance): void {
instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed));
instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged));
instance.addDisposable(instance.onProcessIdReady(this._onInstanceProcessIdReady.fire, this._onInstanceProcessIdReady));
instance.addDisposable(instance.onDimensionsChanged(() => this._onInstanceDimensionsChanged.fire(instance)));
instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged));
}
private _getTabForInstance(instance: ITerminalInstance): ITerminalTab | null {
for (const tab of this._terminalTabs) {
if (tab.terminalInstances.indexOf(instance) !== -1) {
return tab;
}
}
return null;
}
public showPanel(focus?: boolean): Promise<void> {
return new Promise<void>((complete) => {
const panel = this._panelService.getActivePanel();
if (!panel || panel.getId() !== TERMINAL_PANEL_ID) {
this._panelService.openPanel(TERMINAL_PANEL_ID, focus);
if (focus) {
// Do the focus call asynchronously as going through the
// command palette will force editor focus
setTimeout(() => {
const instance = this.getActiveInstance();
if (instance) {
instance.focusWhenReady(true).then(() => complete(undefined));
} else {
complete(undefined);
}
}, 0);
} else {
complete(undefined);
}
} else {
if (focus) {
// Do the focus call asynchronously as going through the
// command palette will force editor focus
setTimeout(() => {
const instance = this.getActiveInstance();
if (instance) {
instance.focusWhenReady(true).then(() => complete(undefined));
} else {
complete(undefined);
}
}, 0);
} else {
complete(undefined);
}
}
return undefined;
});
}
public abstract hidePanel(): void;
public abstract focusFindWidget(): Promise<void>;
public abstract hideFindWidget(): void;
public abstract findNext(): void;
public abstract findPrevious(): void;
private _getIndexFromId(terminalId: number): number {
let terminalIndex = -1;
this.terminalInstances.forEach((terminalInstance, i) => {
if (terminalInstance.id === terminalId) {
terminalIndex = i;
}
});
if (terminalIndex === -1) {
throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`);
}
return terminalIndex;
}
public setWorkspaceShellAllowed(isAllowed: boolean): void {
this.configHelper.setWorkspaceShellAllowed(isAllowed);
}
protected _showTerminalCloseConfirmation(): Promise<boolean> {
let message;
if (this.terminalInstances.length === 1) {
message = nls.localize('terminalService.terminalCloseConfirmationSingular', "There is an active terminal session, do you want to kill it?");
} else {
message = nls.localize('terminalService.terminalCloseConfirmationPlural', "There are {0} active terminal sessions, do you want to kill them?", this.terminalInstances.length);
}
return this._dialogService.confirm({
message,
type: 'warning',
}).then(res => !res.confirmed);
}
protected _showNotEnoughSpaceToast(): void {
this._notificationService.info(nls.localize('terminal.minWidth', "Not enough space to split terminal."));
}
protected _validateShellPaths(label: string, potentialPaths: string[]): Promise<[string, string] | null> {
if (potentialPaths.length === 0) {
return Promise.resolve(null);
}
const current = potentialPaths.shift();
return this._fileService.existsFile(URI.file(current!)).then(exists => {
if (!exists) {
return this._validateShellPaths(label, potentialPaths);
}
return [label, current] as [string, string];
});
}
public preparePathForTerminalAsync(originalPath: string, executable: string, title: string): Promise<string> {
return new Promise<string>(c => {
const exe = executable;
if (!exe) {
c(originalPath);
return;
}
const hasSpace = originalPath.indexOf(' ') !== -1;
const pathBasename = basename(exe, '.exe');
const isPowerShell = pathBasename === 'pwsh' ||
title === 'pwsh' ||
pathBasename === 'powershell' ||
title === 'powershell';
if (isPowerShell && (hasSpace || originalPath.indexOf('\'') !== -1)) {
c(`& '${originalPath.replace(/'/g, '\'\'')}'`);
return;
}
if (isWindows) {
// 17063 is the build number where wsl path was introduced.
// Update Windows uriPath to be executed in WSL.
if (((exe.indexOf('wsl') !== -1) || ((exe.indexOf('bash.exe') !== -1) && (exe.indexOf('git') === -1))) && (this._getWindowsBuildNumber() >= 17063)) {
c(this._getWslPath(originalPath));
return;
} else if (hasSpace) {
c('"' + originalPath + '"');
} else {
c(originalPath);
}
return;
}
c(escapeNonWindowsPath(originalPath));
});
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as platform from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalService } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/electron-browser/terminalInstanceService';
import { TerminalService } from 'vs/workbench/contrib/terminal/electron-browser/terminalService';
import { getDefaultShell } from 'vs/workbench/contrib/terminal/node/terminal';
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration({
id: 'terminal',
order: 100,
title: nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
type: 'object',
properties: {
'terminal.integrated.shell.linux': {
markdownDescription: nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
type: 'string',
default: getDefaultShell(platform.Platform.Linux)
},
'terminal.integrated.shell.osx': {
markdownDescription: nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
type: 'string',
default: getDefaultShell(platform.Platform.Mac)
},
'terminal.integrated.shell.windows': {
markdownDescription: nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
type: 'string',
default: getDefaultShell(platform.Platform.Windows)
}
}
});
registerSingleton(ITerminalService, TerminalService, true);
registerSingleton(ITerminalInstanceService, TerminalInstanceService, true);

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { Terminal as XTermTerminal } from 'vscode-xterm';
import { ITerminalInstance, IWindowsShellHelper, ITerminalConfigHelper, ITerminalProcessManager, IShellLaunchConfig, ITerminalChildProcess } from 'vs/workbench/contrib/terminal/common/terminal';
import { WindowsShellHelper } from 'vs/workbench/contrib/terminal/node/windowsShellHelper';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess';
let Terminal: typeof XTermTerminal;
/**
* A service used by TerminalInstance (and components owned by it) that allows it to break its
* dependency on electron-browser and node layers, while at the same time avoiding a cyclic
* dependency on ITerminalService.
*/
export class TerminalInstanceService implements ITerminalInstanceService {
public _serviceBrand: any;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
}
public async getXtermConstructor(): Promise<typeof XTermTerminal> {
if (!Terminal) {
Terminal = (await import('vscode-xterm')).Terminal;
// Enable xterm.js addons
Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/search/search'));
Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/webLinks/webLinks'));
Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/winptyCompat/winptyCompat'));
// Localize strings
Terminal.strings.blankLine = nls.localize('terminal.integrated.a11yBlankLine', 'Blank line');
Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input');
Terminal.strings.tooMuchOutput = nls.localize('terminal.integrated.a11yTooMuchOutput', 'Too much output to announce, navigate to rows manually to read');
}
return Terminal;
}
public createWindowsShellHelper(shellProcessId: number, instance: ITerminalInstance, xterm: XTermTerminal): IWindowsShellHelper {
return new WindowsShellHelper(shellProcessId, instance, xterm);
}
public createTerminalProcessManager(id: number, configHelper: ITerminalConfigHelper): ITerminalProcessManager {
return this._instantiationService.createInstance(TerminalProcessManager, id, configHelper);
}
public createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess {
return new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, windowsEnableConpty);
}
}

View File

@@ -0,0 +1,153 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as pfs from 'vs/base/node/pfs';
import * as platform from 'vs/base/common/platform';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ITerminalService, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalService as BrowserTerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { getDefaultShell, linuxDistro, getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ipcRenderer as ipc } from 'electron';
import { IOpenFileRequest, IWindowService } from 'vs/platform/windows/common/windows';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
import { coalesce } from 'vs/base/common/arrays';
import { IFileService } from 'vs/platform/files/common/files';
import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { execFile } from 'child_process';
export class TerminalService extends BrowserTerminalService implements ITerminalService {
public get configHelper(): ITerminalConfigHelper { return this._configHelper; }
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IPanelService panelService: IPanelService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IStorageService storageService: IStorageService,
@ILifecycleService lifecycleService: ILifecycleService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@INotificationService notificationService: INotificationService,
@IDialogService dialogService: IDialogService,
@IExtensionService extensionService: IExtensionService,
@IWindowService windowService: IWindowService,
@IFileService fileService: IFileService
) {
super(contextKeyService, panelService, layoutService, lifecycleService, storageService, notificationService, dialogService, instantiationService, windowService, extensionService, fileService);
this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, linuxDistro);
ipc.on('vscode:openFiles', (_event: any, request: IOpenFileRequest) => {
// if the request to open files is coming in from the integrated terminal (identified though
// the termProgram variable) and we are instructed to wait for editors close, wait for the
// marker file to get deleted and then focus back to the integrated terminal.
if (request.termProgram === 'vscode' && request.filesToWait) {
pfs.whenDeleted(request.filesToWait.waitMarkerFilePath).then(() => {
if (this.terminalInstances.length > 0) {
const terminal = this.getActiveInstance();
if (terminal) {
terminal.focus();
}
}
});
}
});
ipc.on('vscode:osResume', () => {
const activeTab = this.getActiveTab();
if (!activeTab) {
return;
}
activeTab.terminalInstances.forEach(instance => instance.forceRedraw());
});
}
protected _getDefaultShell(p: platform.Platform): string {
return getDefaultShell(p);
}
public selectDefaultWindowsShell(): Promise<string | undefined> {
return this._detectWindowsShells().then(shells => {
const options: IPickOptions<IQuickPickItem> = {
placeHolder: nls.localize('terminal.integrated.chooseWindowsShell', "Select your preferred terminal shell, you can change this later in your settings")
};
return this._quickInputService.pick(shells, options).then(value => {
if (!value) {
return undefined;
}
const shell = value.description;
return this._configurationService.updateValue('terminal.integrated.shell.windows', shell, ConfigurationTarget.USER).then(() => shell);
});
});
}
private _detectWindowsShells(): Promise<IQuickPickItem[]> {
// 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
// module doesn't work if this is not the case. See #27915.
const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
const system32Path = `${process.env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}`;
let useWSLexe = false;
if (getWindowsBuildNumber() >= 16299) {
useWSLexe = true;
}
const expectedLocations = {
'Command Prompt': [`${system32Path}\\cmd.exe`],
PowerShell: [`${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`],
'WSL Bash': [`${system32Path}\\${useWSLexe ? 'wsl.exe' : 'bash.exe'}`],
'Git Bash': [
`${process.env['ProgramW6432']}\\Git\\bin\\bash.exe`,
`${process.env['ProgramW6432']}\\Git\\usr\\bin\\bash.exe`,
`${process.env['ProgramFiles']}\\Git\\bin\\bash.exe`,
`${process.env['ProgramFiles']}\\Git\\usr\\bin\\bash.exe`,
`${process.env['LocalAppData']}\\Programs\\Git\\bin\\bash.exe`,
]
};
const promises: PromiseLike<[string, string]>[] = [];
Object.keys(expectedLocations).forEach(key => promises.push(this._validateShellPaths(key, expectedLocations[key])));
return Promise.all(promises)
.then(coalesce)
.then(results => {
return results.map(result => {
return <IQuickPickItem>{
label: result[0],
description: result[1]
};
});
});
}
protected _getWindowsBuildNumber(): number {
return getWindowsBuildNumber();
}
/**
* Converts a path to a path on WSL using the wslpath utility.
* @param path The original path.
*/
protected _getWslPath(path: string): Promise<string> {
if (getWindowsBuildNumber() < 17063) {
throw new Error('wslpath does not exist on Windows build < 17063');
}
return new Promise<string>(c => {
execFile('bash.exe', ['-c', 'echo $(wslpath ' + escapeNonWindowsPath(path) + ')'], {}, (error, stdout, stderr) => {
c(escapeNonWindowsPath(stdout.trim()));
});
});
}
}

View File

@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as platform from 'vs/base/common/platform';
import * as processes from 'vs/base/node/processes';
import { readFile, fileExists } from 'vs/base/node/pfs';
import { LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal';
export function getDefaultShell(p: platform.Platform): string {
if (p === platform.Platform.Windows) {
if (platform.isWindows) {
return getTerminalDefaultShellWindows();
}
// Don't detect Windows shell when not on Windows
return processes.getWindowsShell();
}
// Only use $SHELL for the current OS
if (platform.isLinux && p === platform.Platform.Mac || platform.isMacintosh && p === platform.Platform.Linux) {
return '/bin/bash';
}
return getTerminalDefaultShellUnixLike();
}
let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null;
function getTerminalDefaultShellUnixLike(): string {
if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) {
let unixLikeTerminal = 'sh';
if (!platform.isWindows && process.env.SHELL) {
unixLikeTerminal = process.env.SHELL;
// Some systems have $SHELL set to /bin/false which breaks the terminal
if (unixLikeTerminal === '/bin/false') {
unixLikeTerminal = '/bin/bash';
}
}
if (platform.isWindows) {
unixLikeTerminal = '/bin/bash'; // for WSL
}
_TERMINAL_DEFAULT_SHELL_UNIX_LIKE = unixLikeTerminal;
}
return _TERMINAL_DEFAULT_SHELL_UNIX_LIKE;
}
let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null;
function getTerminalDefaultShellWindows(): string {
if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) {
const isAtLeastWindows10 = platform.isWindows && parseFloat(os.release()) >= 10;
const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
const powerShellPath = `${process.env.windir}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`;
_TERMINAL_DEFAULT_SHELL_WINDOWS = isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell();
}
return _TERMINAL_DEFAULT_SHELL_WINDOWS;
}
let detectedDistro = LinuxDistro.Unknown;
if (platform.isLinux) {
const file = '/etc/os-release';
fileExists(file).then(exists => {
if (!exists) {
return;
}
readFile(file).then(b => {
const contents = b.toString();
if (/NAME="?Fedora"?/.test(contents)) {
detectedDistro = LinuxDistro.Fedora;
} else if (/NAME="?Ubuntu"?/.test(contents)) {
detectedDistro = LinuxDistro.Ubuntu;
}
});
});
}
export const linuxDistro = detectedDistro;
export function getWindowsBuildNumber(): number {
const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release());
let buildNumber: number = 0;
if (osVersion && osVersion.length === 4) {
buildNumber = parseInt(osVersion[3]);
}
return buildNumber;
}

View File

@@ -0,0 +1,216 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import * as pty from 'node-pty';
import * as fs from 'fs';
import { Event, Emitter } from 'vs/base/common/event';
import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IShellLaunchConfig, ITerminalChildProcess } from 'vs/workbench/contrib/terminal/common/terminal';
import { exec } from 'child_process';
export class TerminalProcess implements ITerminalChildProcess, IDisposable {
private _exitCode: number;
private _closeTimeout: any;
private _ptyProcess: pty.IPty;
private _currentTitle: string = '';
private _processStartupComplete: Promise<void>;
private _isDisposed: boolean = false;
private _titleInterval: NodeJS.Timer | null = null;
private _initialCwd: string;
private readonly _onProcessData = new Emitter<string>();
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessExit = new Emitter<number>();
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
private readonly _onProcessIdReady = new Emitter<number>();
public get onProcessIdReady(): Event<number> { return this._onProcessIdReady.event; }
private readonly _onProcessTitleChanged = new Emitter<string>();
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
constructor(
shellLaunchConfig: IShellLaunchConfig,
cwd: string,
cols: number,
rows: number,
env: platform.IProcessEnvironment,
windowsEnableConpty: boolean
) {
let shellName: string;
if (os.platform() === 'win32') {
shellName = path.basename(shellLaunchConfig.executable || '');
} else {
// Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a
// color prompt as defined in the default ~/.bashrc file.
shellName = 'xterm-256color';
}
this._initialCwd = cwd;
const useConpty = windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309;
const options: pty.IPtyForkOptions = {
name: shellName,
cwd,
env,
cols,
rows,
experimentalUseConpty: useConpty
};
try {
this._ptyProcess = pty.spawn(shellLaunchConfig.executable!, shellLaunchConfig.args || [], options);
this._processStartupComplete = new Promise<void>(c => {
this.onProcessIdReady((pid) => {
c();
});
});
} catch (error) {
// The only time this is expected to happen is when the file specified to launch with does not exist.
this._exitCode = 2;
this._queueProcessExit();
this._processStartupComplete = Promise.resolve(undefined);
return;
}
this._ptyProcess.on('data', (data) => {
this._onProcessData.fire(data);
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
this._queueProcessExit();
}
});
this._ptyProcess.on('exit', (code) => {
this._exitCode = code;
this._queueProcessExit();
});
// TODO: We should no longer need to delay this since pty.spawn is sync
setTimeout(() => {
this._sendProcessId();
}, 500);
this._setupTitlePolling();
}
public dispose(): void {
this._isDisposed = true;
if (this._titleInterval) {
clearInterval(this._titleInterval);
}
this._titleInterval = null;
this._onProcessData.dispose();
this._onProcessExit.dispose();
this._onProcessIdReady.dispose();
this._onProcessTitleChanged.dispose();
}
private _setupTitlePolling() {
// Send initial timeout async to give event listeners a chance to init
setTimeout(() => {
this._sendProcessTitle();
}, 0);
// Setup polling
this._titleInterval = setInterval(() => {
if (this._currentTitle !== this._ptyProcess.process) {
this._sendProcessTitle();
}
}, 200);
}
// Allow any trailing data events to be sent before the exit event is sent.
// See https://github.com/Tyriar/node-pty/issues/72
private _queueProcessExit() {
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
}
this._closeTimeout = setTimeout(() => this._kill(), 250);
}
private _kill(): void {
// Wait to kill to process until the start up code has run. This prevents us from firing a process exit before a
// process start.
this._processStartupComplete.then(() => {
if (this._isDisposed) {
return;
}
// Attempt to kill the pty, it may have already been killed at this
// point but we want to make sure
try {
this._ptyProcess.kill();
} catch (ex) {
// Swallow, the pty has already been killed
}
this._onProcessExit.fire(this._exitCode);
this.dispose();
});
}
private _sendProcessId() {
this._onProcessIdReady.fire(this._ptyProcess.pid);
}
private _sendProcessTitle(): void {
if (this._isDisposed) {
return;
}
this._currentTitle = this._ptyProcess.process;
this._onProcessTitleChanged.fire(this._currentTitle);
}
public shutdown(immediate: boolean): void {
if (immediate) {
this._kill();
} else {
this._queueProcessExit();
}
}
public input(data: string): void {
if (this._isDisposed) {
return;
}
this._ptyProcess.write(data);
}
public resize(cols: number, rows: number): void {
if (this._isDisposed) {
return;
}
// Ensure that cols and rows are always >= 1, this prevents a native
// exception in winpty.
this._ptyProcess.resize(Math.max(cols, 1), Math.max(rows, 1));
}
public getInitialCwd(): Promise<string> {
return Promise.resolve(this._initialCwd);
}
public getCwd(): Promise<string> {
if (platform.isMacintosh) {
return new Promise<string>(resolve => {
exec('lsof -p ' + this._ptyProcess.pid + ' | grep cwd', (error, stdout, stderr) => {
if (stdout !== '') {
resolve(stdout.substring(stdout.indexOf('/'), stdout.length - 1));
}
});
});
}
if (platform.isLinux) {
return new Promise<string>(resolve => {
fs.readlink('/proc/' + this._ptyProcess.pid + '/cwd', (err, linkedstr) => {
if (err) {
resolve(this._initialCwd);
}
resolve(linkedstr);
});
});
}
return new Promise<string>(resolve => {
resolve(this._initialCwd);
});
}
}

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as platform from 'vs/base/common/platform';
import { Emitter, Event } from 'vs/base/common/event';
import { ITerminalInstance, IWindowsShellHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { Terminal as XTermTerminal } from 'vscode-xterm';
import WindowsProcessTreeType = require('windows-process-tree');
const SHELL_EXECUTABLES = [
'cmd.exe',
'powershell.exe',
'bash.exe',
'wsl.exe',
'ubuntu.exe',
'ubuntu1804.exe',
'kali.exe',
'debian.exe',
'opensuse-42.exe',
'sles-12.exe'
];
let windowsProcessTree: typeof WindowsProcessTreeType;
export class WindowsShellHelper implements IWindowsShellHelper {
private _onCheckShell: Emitter<Promise<string> | undefined>;
private _isDisposed: boolean;
private _currentRequest: Promise<string> | null;
private _newLineFeed: boolean;
public constructor(
private _rootProcessId: number,
private _terminalInstance: ITerminalInstance,
private _xterm: XTermTerminal
) {
if (!platform.isWindows) {
throw new Error(`WindowsShellHelper cannot be instantiated on ${platform.platform}`);
}
this._isDisposed = false;
(import('windows-process-tree')).then(mod => {
if (this._isDisposed) {
return;
}
windowsProcessTree = mod;
this._onCheckShell = new Emitter<Promise<string>>();
// The debounce is necessary to prevent multiple processes from spawning when
// the enter key or output is spammed
Event.debounce(this._onCheckShell.event, (l, e) => e, 150, true)(() => {
setTimeout(() => {
this.checkShell();
}, 50);
});
// We want to fire a new check for the shell on a linefeed, but only
// when parsing has finished which is indicated by the cursormove event.
// If this is done on every linefeed, parsing ends up taking
// significantly longer due to resetting timers. Note that this is
// private API.
this._xterm.on('linefeed', () => this._newLineFeed = true);
this._xterm.on('cursormove', () => {
if (this._newLineFeed) {
this._onCheckShell.fire(undefined);
}
});
// Fire a new check for the shell when any key is pressed.
this._xterm.on('keypress', () => this._onCheckShell.fire(undefined));
});
}
private checkShell(): void {
if (platform.isWindows && this._terminalInstance.isTitleSetByProcess) {
this.getShellName().then(title => {
if (!this._isDisposed) {
this._terminalInstance.setTitle(title, true);
}
});
}
}
private traverseTree(tree: any): string {
if (!tree) {
return '';
}
if (SHELL_EXECUTABLES.indexOf(tree.name) === -1) {
return tree.name;
}
if (!tree.children || tree.children.length === 0) {
return tree.name;
}
let favouriteChild = 0;
for (; favouriteChild < tree.children.length; favouriteChild++) {
const child = tree.children[favouriteChild];
if (!child.children || child.children.length === 0) {
break;
}
if (child.children[0].name !== 'conhost.exe') {
break;
}
}
if (favouriteChild >= tree.children.length) {
return tree.name;
}
return this.traverseTree(tree.children[favouriteChild]);
}
public dispose(): void {
this._isDisposed = true;
}
/**
* Returns the innermost shell executable running in the terminal
*/
public getShellName(): Promise<string> {
if (this._isDisposed) {
return Promise.resolve('');
}
// Prevent multiple requests at once, instead return current request
if (this._currentRequest) {
return this._currentRequest;
}
this._currentRequest = new Promise<string>(resolve => {
windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => {
const name = this.traverseTree(tree);
this._currentRequest = null;
resolve(name);
});
});
return this._currentRequest;
}
}

View File

@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Extensions as ThemeingExtensions, IColorRegistry, ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ansiColorIdentifiers, registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { ITheme, ThemeType } from 'vs/platform/theme/common/themeService';
import { Color } from 'vs/base/common/color';
registerColors();
let themingRegistry = Registry.as<IColorRegistry>(ThemeingExtensions.ColorContribution);
function getMockTheme(type: ThemeType): ITheme {
let theme = {
selector: '',
label: '',
type: type,
getColor: (colorId: ColorIdentifier) => themingRegistry.resolveDefaultColor(colorId, theme),
defines: () => true
};
return theme;
}
suite('Workbench - TerminalColorRegistry', () => {
test('hc colors', function () {
let theme = getMockTheme('hc');
let colors = ansiColorIdentifiers.map(colorId => Color.Format.CSS.formatHexA(theme.getColor(colorId)!, true));
assert.deepEqual(colors, [
'#000000',
'#cd0000',
'#00cd00',
'#cdcd00',
'#0000ee',
'#cd00cd',
'#00cdcd',
'#e5e5e5',
'#7f7f7f',
'#ff0000',
'#00ff00',
'#ffff00',
'#5c5cff',
'#ff00ff',
'#00ffff',
'#ffffff'
], 'The high contrast terminal colors should be used when the hc theme is active');
});
test('light colors', function () {
let theme = getMockTheme('light');
let colors = ansiColorIdentifiers.map(colorId => Color.Format.CSS.formatHexA(theme.getColor(colorId)!, true));
assert.deepEqual(colors, [
'#000000',
'#cd3131',
'#00bc00',
'#949800',
'#0451a5',
'#bc05bc',
'#0598bc',
'#555555',
'#666666',
'#cd3131',
'#14ce14',
'#b5ba00',
'#0451a5',
'#bc05bc',
'#0598bc',
'#a5a5a5'
], 'The light terminal colors should be used when the light theme is active');
});
test('dark colors', function () {
let theme = getMockTheme('dark');
let colors = ansiColorIdentifiers.map(colorId => Color.Format.CSS.formatHexA(theme.getColor(colorId)!, true));
assert.deepEqual(colors, [
'#000000',
'#cd3131',
'#0dbc79',
'#e5e510',
'#2472c8',
'#bc3fbc',
'#11a8cd',
'#e5e5e5',
'#666666',
'#f14c4c',
'#23d18b',
'#f5f543',
'#3b8eea',
'#d670d6',
'#29b8db',
'#e5e5e5'
], 'The dark terminal colors should be used when a dark theme is active');
});
});

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Terminal, TerminalCore } from 'vscode-xterm';
import { TerminalCommandTracker } from 'vs/workbench/contrib/terminal/browser/terminalCommandTracker';
import { isWindows } from 'vs/base/common/platform';
interface TestTerminalCore extends TerminalCore {
writeBuffer: string[];
_innerWrite(): void;
}
interface TestTerminal extends Terminal {
_core: TestTerminalCore;
}
function syncWrite(term: TestTerminal, data: string): void {
// Terminal.write is asynchronous
term._core.writeBuffer.push(data);
term._core._innerWrite();
}
const ROWS = 10;
const COLS = 10;
suite('Workbench - TerminalCommandTracker', () => {
suite('Command tracking', () => {
test('should track commands when the prompt is of sufficient size', () => {
assert.equal(0, 0);
});
});
});

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal';
suite('Workbench - TerminalConfigHelper', () => {
test('TerminalConfigHelper - getFont fontFamily', function () {
// {{SQL CARBON EDIT}} - Remove tests
});
});

View File

@@ -0,0 +1,262 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Platform, OperatingSystem } from 'vs/base/common/platform';
import { TerminalLinkHandler, LineColumnInfo } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
import * as strings from 'vs/base/common/strings';
class TestTerminalLinkHandler extends TerminalLinkHandler {
public get localLinkRegex(): RegExp {
return this._localLinkRegex;
}
public get gitDiffLinkPreImageRegex(): RegExp {
return this._gitDiffPreImageRegex;
}
public get gitDiffLinkPostImageRegex(): RegExp {
return this._gitDiffPostImageRegex;
}
public preprocessPath(link: string): string | null {
return this._preprocessPath(link);
}
}
class TestXterm {
public webLinksInit() { }
public registerLinkMatcher() { }
}
interface LinkFormatInfo {
urlFormat: string;
line?: string;
column?: string;
}
suite('Workbench - TerminalLinkHandler', () => {
suite('localLinkRegex', () => {
test('Windows', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, {
os: OperatingSystem.Windows,
userHome: ''
} as any, null!, null!, null!, null!, null!);
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`;${link};`), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`(${link})`), linkUrl);
if (lineNo) {
const lineColumnInfo: LineColumnInfo = terminalLinkHandler.extractLineColumnInfo(link);
assert.equal(lineColumnInfo.lineNumber, lineNo);
if (columnNo) {
assert.equal(lineColumnInfo.columnNumber, columnNo);
}
}
}
function generateAndTestLinks() {
const linkUrls = [
'c:\\foo',
'c:/foo',
'.\\foo',
'./foo',
'..\\foo',
'~\\foo',
'~/foo',
'c:/a/long/path',
'c:\\a\\long\\path',
'c:\\mixed/slash\\path',
'a/relative/path',
'plain/path',
'plain\\path'
];
const supportedLinkFormats: LinkFormatInfo[] = [
{ urlFormat: '{0}' },
{ urlFormat: '{0} on line {1}', line: '5' },
{ urlFormat: '{0} on line {1}, column {2}', line: '5', column: '3' },
{ urlFormat: '{0}:line {1}', line: '5' },
{ urlFormat: '{0}:line {1}, column {2}', line: '5', column: '3' },
{ urlFormat: '{0}({1})', line: '5' },
{ urlFormat: '{0} ({1})', line: '5' },
{ urlFormat: '{0}({1},{2})', line: '5', column: '3' },
{ urlFormat: '{0} ({1},{2})', line: '5', column: '3' },
{ urlFormat: '{0}({1}, {2})', line: '5', column: '3' },
{ urlFormat: '{0} ({1}, {2})', line: '5', column: '3' },
{ urlFormat: '{0}:{1}', line: '5' },
{ urlFormat: '{0}:{1}:{2}', line: '5', column: '3' },
{ urlFormat: '{0}[{1}]', line: '5' },
{ urlFormat: '{0} [{1}]', line: '5' },
{ urlFormat: '{0}[{1},{2}]', line: '5', column: '3' },
{ urlFormat: '{0} [{1},{2}]', line: '5', column: '3' },
{ urlFormat: '{0}[{1}, {2}]', line: '5', column: '3' },
{ urlFormat: '{0} [{1}, {2}]', line: '5', column: '3' }
];
linkUrls.forEach(linkUrl => {
supportedLinkFormats.forEach(linkFormatInfo => {
testLink(
strings.format(linkFormatInfo.urlFormat, linkUrl, linkFormatInfo.line, linkFormatInfo.column),
linkUrl,
linkFormatInfo.line,
linkFormatInfo.column
);
});
});
}
generateAndTestLinks();
});
test('Linux', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: ''
} as any, null!, null!, null!, null!, null!);
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`;${link};`), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`(${link})`), linkUrl);
if (lineNo) {
const lineColumnInfo: LineColumnInfo = terminalLinkHandler.extractLineColumnInfo(link);
assert.equal(lineColumnInfo.lineNumber, lineNo);
if (columnNo) {
assert.equal(lineColumnInfo.columnNumber, columnNo);
}
}
}
function generateAndTestLinks() {
const linkUrls = [
'/foo',
'~/foo',
'./foo',
'../foo',
'/a/long/path',
'a/relative/path'
];
const supportedLinkFormats: LinkFormatInfo[] = [
{ urlFormat: '{0}' },
{ urlFormat: '{0} on line {1}', line: '5' },
{ urlFormat: '{0} on line {1}, column {2}', line: '5', column: '3' },
{ urlFormat: '{0}:line {1}', line: '5' },
{ urlFormat: '{0}:line {1}, column {2}', line: '5', column: '3' },
{ urlFormat: '{0}({1})', line: '5' },
{ urlFormat: '{0} ({1})', line: '5' },
{ urlFormat: '{0}({1},{2})', line: '5', column: '3' },
{ urlFormat: '{0} ({1},{2})', line: '5', column: '3' },
{ urlFormat: '{0}:{1}', line: '5' },
{ urlFormat: '{0}:{1}:{2}', line: '5', column: '3' },
{ urlFormat: '{0}[{1}]', line: '5' },
{ urlFormat: '{0} [{1}]', line: '5' },
{ urlFormat: '{0}[{1},{2}]', line: '5', column: '3' },
{ urlFormat: '{0} [{1},{2}]', line: '5', column: '3' }
];
linkUrls.forEach(linkUrl => {
supportedLinkFormats.forEach(linkFormatInfo => {
// console.log('linkFormatInfo: ', linkFormatInfo);
testLink(
strings.format(linkFormatInfo.urlFormat, linkUrl, linkFormatInfo.line, linkFormatInfo.column),
linkUrl,
linkFormatInfo.line,
linkFormatInfo.column
);
});
});
}
generateAndTestLinks();
});
});
suite('preprocessPath', () => {
test('Windows', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\Me'
} as any, null!, null!, null!, null!, null!);
linkHandler.processCwd = 'C:\\base';
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base\\src\\file1');
assert.equal(linkHandler.preprocessPath('src\\file2'), 'C:\\base\\src\\file2');
assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\Me\\src\\file3');
assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\Me\\src\\file4');
assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file5'), 'C:\\absolute\\path\\file5');
});
test('Windows - spaces', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\M e'
} as any, null!, null!, null!, null!, null!);
linkHandler.processCwd = 'C:\\base dir';
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base dir\\src\\file1');
assert.equal(linkHandler.preprocessPath('src\\file2'), 'C:\\base dir\\src\\file2');
assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\M e\\src\\file3');
assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\M e\\src\\file4');
assert.equal(linkHandler.preprocessPath('C:\\abso lute\\path\\file5'), 'C:\\abso lute\\path\\file5');
});
test('Linux', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, null!, null!, null!, null!, null!);
linkHandler.processCwd = '/base';
assert.equal(linkHandler.preprocessPath('./src/file1'), '/base/src/file1');
assert.equal(linkHandler.preprocessPath('src/file2'), '/base/src/file2');
assert.equal(linkHandler.preprocessPath('~/src/file3'), '/home/me/src/file3');
assert.equal(linkHandler.preprocessPath('/absolute/path/file4'), '/absolute/path/file4');
});
test('No Workspace', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, null!, null!, null!, null!, null!);
assert.equal(linkHandler.preprocessPath('./src/file1'), null);
assert.equal(linkHandler.preprocessPath('src/file2'), null);
assert.equal(linkHandler.preprocessPath('~/src/file3'), '/home/me/src/file3');
assert.equal(linkHandler.preprocessPath('/absolute/path/file4'), '/absolute/path/file4');
});
});
test('gitDiffLinkRegex', () => {
// The platform is irrelevant because the links generated by Git are the same format regardless of platform
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: ''
} as any, null!, null!, null!, null!, null!);
function assertAreGoodMatches(matches: RegExpMatchArray | null) {
if (matches) {
assert.equal(matches.length, 2);
assert.equal(matches[1], 'src/file1');
} else {
assert.fail();
}
}
// Happy cases
assertAreGoodMatches('--- a/src/file1'.match(linkHandler.gitDiffLinkPreImageRegex));
assertAreGoodMatches('--- a/src/file1 '.match(linkHandler.gitDiffLinkPreImageRegex));
assertAreGoodMatches('+++ b/src/file1'.match(linkHandler.gitDiffLinkPostImageRegex));
assertAreGoodMatches('+++ b/src/file1 '.match(linkHandler.gitDiffLinkPostImageRegex));
// Make sure /dev/null isn't a match
assert.equal(linkHandler.gitDiffLinkPreImageRegex.test('--- /dev/null'), false);
assert.equal(linkHandler.gitDiffLinkPreImageRegex.test('--- /dev/null '), false);
assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null'), false);
assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null '), false);
});
});

View File

@@ -0,0 +1,131 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { URI as Uri } from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
suite('Workbench - TerminalEnvironment', () => {
test('addTerminalEnvironmentKeys', () => {
const env = { FOO: 'bar' };
const locale = 'en-au';
terminalEnvironment.addTerminalEnvironmentKeys(env, '1.2.3', locale, true);
assert.equal(env['TERM_PROGRAM'], 'vscode');
assert.equal(env['TERM_PROGRAM_VERSION'], '1.2.3');
assert.equal(env['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8');
const env2 = { FOO: 'bar' };
terminalEnvironment.addTerminalEnvironmentKeys(env2, '1.2.3', undefined, true);
assert.equal(env2['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586
const env3 = { LANG: 'replace' };
terminalEnvironment.addTerminalEnvironmentKeys(env3, '1.2.3', undefined, true);
assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is set to the fallback LANG');
const env4 = { LANG: 'en_US.UTF-8' };
terminalEnvironment.addTerminalEnvironmentKeys(env3, '1.2.3', undefined, true);
assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG');
});
suite('mergeEnvironments', () => {
test('should add keys', () => {
const parent = {
a: 'b'
};
const other = {
c: 'd'
};
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
a: 'b',
c: 'd'
});
});
test('should add keys ignoring case on Windows', () => {
if (!platform.isWindows) {
return;
}
const parent = {
a: 'b'
};
const other = {
A: 'c'
};
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
a: 'c'
});
});
test('null values should delete keys from the parent env', () => {
const parent = {
a: 'b',
c: 'd'
};
const other: IStringDictionary<string | null> = {
a: null
};
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
c: 'd'
});
});
test('null values should delete keys from the parent env ignoring case on Windows', () => {
if (!platform.isWindows) {
return;
}
const parent = {
a: 'b',
c: 'd'
};
const other: IStringDictionary<string | null> = {
A: null
};
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
c: 'd'
});
});
});
suite('getCwd', () => {
// This helper checks the paths in a cross-platform friendly manner
function assertPathsMatch(a: string, b: string): void {
assert.equal(Uri.file(a).fsPath, Uri.file(b).fsPath);
}
test('should default to userHome for an empty workspace', () => {
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined), '/userHome/');
});
test('should use to the workspace if it exists', () => {
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', Uri.file('/foo'), undefined), '/foo');
});
test('should use an absolute custom cwd as is', () => {
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, '/foo'), '/foo');
});
test('should normalize a relative custom cwd against the workspace path', () => {
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', Uri.file('/bar'), 'foo'), '/bar/foo');
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', Uri.file('/bar'), './foo'), '/bar/foo');
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', Uri.file('/bar'), '../foo'), '/foo');
});
test('should fall back for relative a custom cwd that doesn\'t have a workspace', () => {
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, 'foo'), '/userHome/');
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, './foo'), '/userHome/');
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, '../foo'), '/userHome/');
});
test('should ignore custom cwd when told to ignore', () => {
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', Uri.file('/bar'), '/foo'), '/bar');
});
});
});