deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,31 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - TERMINAL EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/terminal` extension contributes the ability to spawn integrated terminals in the application which can be used in a variety of different scenarios.
## Additional Information
- [API documentation for `@theia/terminal`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_terminal.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,60 @@
{
"name": "@theia/terminal",
"version": "1.68.0",
"description": "Theia - Terminal Extension",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/editor": "1.68.0",
"@theia/file-search": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/process": "1.68.0",
"@theia/variable-resolver": "1.68.0",
"@theia/workspace": "1.68.0",
"tslib": "^2.6.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
"xterm-addon-webgl": "^0.16.0"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/terminal-frontend-module",
"secondaryWindow": "lib/browser/terminal-frontend-module",
"backend": "lib/node/terminal-backend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,60 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Event } from '@theia/core/lib/common/event';
import { WidgetOpenerOptions } from '@theia/core/lib/browser';
import { TerminalWidgetOptions, TerminalWidget } from './terminal-widget';
/**
* Service manipulating terminal widgets.
*/
export const TerminalService = Symbol('TerminalService');
export interface TerminalService {
/**
* Create new terminal with predefined options.
* @param options - terminal options.
*/
newTerminal(options: TerminalWidgetOptions): Promise<TerminalWidget>;
open(terminal: TerminalWidget, options?: WidgetOpenerOptions): void;
readonly all: TerminalWidget[];
/**
* @param id - the widget id (NOT the terminal id!)
* @return the widget
*/
getById(id: string): TerminalWidget | undefined;
/**
* @param id - the terminal id (NOT the terminal widget id!)
* @return the widget
*/
getByTerminalId(terminalId: number): TerminalWidget | undefined;
/**
* Returns detected default shell.
*/
getDefaultShell(): Promise<string>;
readonly onDidCreateTerminal: Event<TerminalWidget>;
readonly currentTerminal: TerminalWidget | undefined;
readonly onDidChangeCurrentTerminal: Event<TerminalWidget | undefined>;
readonly lastUsedTerminal: TerminalWidget | undefined;
}

View File

@@ -0,0 +1,285 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Event, ViewColumn } from '@theia/core';
import { BaseWidget } from '@theia/core/lib/browser';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
import { ThemeIcon } from '@theia/core/lib/common/theme';
import { CommandLineOptions } from '@theia/process/lib/common/shell-command-builder';
import { TerminalSearchWidget } from '../search/terminal-search-widget';
import { TerminalProcessInfo, TerminalExitReason } from '../../common/base-terminal-protocol';
import URI from '@theia/core/lib/common/uri';
export interface TerminalDimensions {
cols: number;
rows: number;
}
export interface TerminalExitStatus {
readonly code: number | undefined;
readonly reason: TerminalExitReason;
}
export type TerminalLocationOptions = TerminalLocation | TerminalEditorLocation | TerminalSplitLocation;
export enum TerminalLocation {
Panel = 1,
Editor = 2
}
export interface TerminalEditorLocation {
readonly viewColumn: ViewColumn;
readonly preserveFocus?: boolean;
}
export interface TerminalSplitLocation {
readonly parentTerminal: string;
}
export interface TerminalBuffer {
readonly length: number;
/**
* @param start zero based index of the first line to return
* @param length the max number or lines to return
*/
getLines(start: number, length: number): string[];
}
/**
* Terminal UI widget.
*/
export abstract class TerminalWidget extends BaseWidget {
abstract processId: Promise<number>;
/**
* Get the current executable and arguments.
*/
abstract processInfo: Promise<TerminalProcessInfo>;
/** The ids of extensions contributing to the environment of this terminal mapped to the provided description for their changes. */
abstract envVarCollectionDescriptionsByExtension: Promise<Map<string, (string | MarkdownString | undefined)[]>>;
/** Terminal kind that indicates whether a terminal is created by a user or by some extension for a user */
abstract readonly kind: 'user' | string;
abstract readonly terminalId: number;
abstract readonly dimensions: TerminalDimensions;
abstract readonly exitStatus: TerminalExitStatus | undefined;
/** Terminal widget can be hidden from users until explicitly shown once. */
abstract readonly hiddenFromUser: boolean;
/** The position of the terminal widget. */
abstract readonly location: TerminalLocationOptions;
/** The last CWD assigned to the terminal, useful when attempting getCwdURI on a task terminal fails */
lastCwd: URI;
/**
* Start terminal and return terminal id.
* @param id - terminal id.
*/
abstract start(id?: number): Promise<number>;
/**
* Send text to the terminal server.
* @param text - text content.
*/
abstract sendText(text: string): void;
/**
* Resolves when the command is successfully sent, this doesn't mean that it
* was evaluated. Might reject if terminal wasn't properly started yet.
*
* Note that this method will try to escape your arguments as if it was
* someone inputting everything in a shell.
*
* Supported shells: `bash`, `cmd.exe`, `wsl.exe`, `pwsh/powershell.exe`
*/
abstract executeCommand(commandOptions: CommandLineOptions): Promise<void>;
/** Event that fires when the terminal is connected or reconnected */
abstract onDidOpen: Event<void>;
/** Event that fires when the terminal fails to connect or reconnect */
abstract onDidOpenFailure: Event<void>;
/** Event that fires when the terminal size changed */
abstract onSizeChanged: Event<{ cols: number; rows: number; }>;
/** Event that fires when the terminal receives a key event. */
abstract onKey: Event<{ key: string, domEvent: KeyboardEvent }>;
/** Event that fires when the terminal input data */
abstract onData: Event<string>;
/** Event that fires when the terminal shell type is changed */
abstract onShellTypeChanged: Event<string>;
abstract onOutput: Event<string>;
abstract buffer: TerminalBuffer;
abstract scrollLineUp(): void;
abstract scrollLineDown(): void;
abstract scrollToTop(): void;
abstract scrollToBottom(): void;
abstract scrollPageUp(): void;
abstract scrollPageDown(): void;
abstract resetTerminal(): void;
/**
* Event which fires when terminal did closed. Event value contains closed terminal widget definition.
*/
abstract onTerminalDidClose: Event<TerminalWidget>;
/**
* Cleat terminal output.
*/
abstract clearOutput(): void;
/**
* Select entire content in the terminal.
*/
abstract selectAll(): void;
abstract writeLine(line: string): void;
abstract write(data: string): void;
abstract resize(cols: number, rows: number): void;
/**
* Return Terminal search box widget.
*/
abstract getSearchBox(): TerminalSearchWidget;
/**
* Whether the terminal process has child processes.
*/
abstract hasChildProcesses(): Promise<boolean>;
abstract setTitle(title: string): void;
abstract waitOnExit(waitOnExit?: boolean | string): void;
}
/**
* Terminal widget options.
*/
export const TerminalWidgetOptions = Symbol('TerminalWidgetOptions');
export interface TerminalWidgetOptions {
/**
* Human readable terminal representation on the UI.
*/
readonly title?: string;
/**
* icon class with or without color modifier
*/
readonly iconClass?: string | ThemeIcon;
/**
* Path to the executable shell. For example: `/bin/bash`, `bash`, `sh`.
*/
readonly shellPath?: string;
/**
* Args for the custom shell executable. A string can be used on Windows only which allows
* specifying shell args in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6).
*/
readonly shellArgs?: string[] | string;
/**
* Current working directory.
*/
readonly cwd?: string | URI;
/**
* Environment variables for terminal.
*/
readonly env?: { [key: string]: string | null };
/**
* Whether the terminal process environment should be exactly as provided in `env`.
*/
readonly strictEnv?: boolean;
/**
* In case `destroyTermOnClose` is true - terminal process will be destroyed on close terminal widget, otherwise will be kept
* alive.
*/
readonly destroyTermOnClose?: boolean;
/**
* Terminal server side can send to the client `terminal title` to display this value on the UI. If
* useServerTitle = true then display this title, otherwise display title defined by 'title' argument.
*/
readonly useServerTitle?: boolean;
/**
* Whether it is a pseudo terminal where an extension controls its input and output.
*/
readonly isPseudoTerminal?: boolean;
/**
* Terminal id. Should be unique for all DOM.
*/
readonly id?: string;
/**
* Terminal attributes. Can be useful to apply some implementation specific information.
*/
readonly attributes?: { [key: string]: string | null };
/**
* Terminal kind that indicates whether a terminal is created by a user or by some extension for a user
*/
readonly kind?: 'user' | string;
/**
* When enabled the terminal will run the process as normal but not be surfaced to the user until `Terminal.show` is called.
*/
readonly hideFromUser?: boolean;
readonly location?: TerminalLocationOptions;
/**
* When enabled, the terminal will not be persisted across window reloads.
*/
readonly isTransient?: boolean;
/**
* The nonce to use to verify shell integration sequences are coming from a trusted source.
* An example impact of UX of this is if the command line is reported with a nonce, it will
* not need to verify with the user that the command line is correct before rerunning it
* via the [shell integration command decoration](https://code.visualstudio.com/docs/terminal/shell-integration#_command-decorations-and-the-overview-ruler).
*
* This should be used if the terminal includes [custom shell integration support](https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences).
* It should be set to a random GUID which will then set the `VSCODE_NONCE` environment
* variable. Inside the shell, this should then be removed from the environment so as to
* protect it from general access. Once that is done it can be passed through in the
* relevant sequences to make them trusted.
*/
readonly shellIntegrationNonce?: string;
}

View File

@@ -0,0 +1,17 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './terminal-frontend-module';

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import { TerminalSearchWidget, TerminalSearchWidgetFactory } from './terminal-search-widget';
import { Terminal } from 'xterm';
export function createTerminalSearchFactory(container: interfaces.Container): TerminalSearchWidgetFactory {
container.bind(TerminalSearchWidget).toSelf().inSingletonScope();
return (terminal: Terminal) => {
container.bind(Terminal).toConstantValue(terminal);
return container.get(TerminalSearchWidget);
};
}

View File

@@ -0,0 +1,177 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import * as React from '@theia/core/shared/react';
import '../../../src/browser/style/terminal-search.css';
import { Terminal } from 'xterm';
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
import { codicon, Key } from '@theia/core/lib/browser';
import { nls } from '@theia/core';
export const TERMINAL_SEARCH_WIDGET_FACTORY_ID = 'terminal-search';
export const TerminalSearchWidgetFactory = Symbol('TerminalSearchWidgetFactory');
export type TerminalSearchWidgetFactory = (terminal: Terminal) => TerminalSearchWidget;
@injectable()
export class TerminalSearchWidget extends ReactWidget {
private searchInput: HTMLInputElement | null;
private searchBox: HTMLDivElement | null;
private searchOptions: ISearchOptions = {};
private searchAddon: SearchAddon;
@inject(Terminal)
protected terminal: Terminal;
@postConstruct()
protected init(): void {
this.node.classList.add('theia-search-terminal-widget-parent');
this.searchAddon = new SearchAddon();
this.terminal.loadAddon(this.searchAddon);
this.hide();
this.update();
}
protected render(): React.ReactNode {
return <div className='theia-search-terminal-widget'>
<div className='theia-search-elem-box' ref={searchBox => this.searchBox = searchBox} >
<input
title={nls.localizeByDefault('Find')}
type='text'
spellCheck='false'
placeholder={nls.localizeByDefault('Find')}
ref={ip => this.searchInput = ip}
onKeyUp={this.onInputChanged}
onFocus={this.onSearchInputFocus}
onBlur={this.onSearchInputBlur}
/>
<div
title={nls.localizeByDefault('Match Case')}
tabIndex={0}
className={'search-elem ' + codicon('case-sensitive')}
onClick={this.handleCaseSensitiveOptionClicked}
/>
<div
title={nls.localizeByDefault('Match Whole Word')}
tabIndex={0}
className={'search-elem ' + codicon('whole-word')}
onClick={this.handleWholeWordOptionClicked}
/>
<div
title={nls.localizeByDefault('Use Regular Expression')}
tabIndex={0}
className={'search-elem ' + codicon('regex')}
onClick={this.handleRegexOptionClicked}
/>
</div>
<button title={nls.localizeByDefault('Previous Match')} className={'search-elem ' + codicon('arrow-up')} onClick={this.handlePreviousButtonClicked}></button>
<button title={nls.localizeByDefault('Next Match')} className={'search-elem ' + codicon('arrow-down')} onClick={this.handleNextButtonClicked}></button>
<button title={nls.localizeByDefault('Close')} className={'search-elem ' + codicon('close')} onClick={this.handleHide}></button>
</div>;
}
onSearchInputFocus = (): void => {
if (this.searchBox) {
this.searchBox.classList.add('focused');
}
};
onSearchInputBlur = (): void => {
if (this.searchBox) {
this.searchBox.classList.remove('focused');
}
};
private handleHide = (): void => {
this.hide();
};
private handleCaseSensitiveOptionClicked = (event: React.MouseEvent<HTMLSpanElement>): void => {
this.searchOptions.caseSensitive = !this.searchOptions.caseSensitive;
this.updateSearchInputBox(this.searchOptions.caseSensitive, event.currentTarget);
};
private handleWholeWordOptionClicked = (event: React.MouseEvent<HTMLSpanElement>): void => {
this.searchOptions.wholeWord = !this.searchOptions.wholeWord;
this.updateSearchInputBox(this.searchOptions.wholeWord, event.currentTarget);
};
private handleRegexOptionClicked = (event: React.MouseEvent<HTMLSpanElement>): void => {
this.searchOptions.regex = !this.searchOptions.regex;
this.updateSearchInputBox(this.searchOptions.regex, event.currentTarget);
};
private updateSearchInputBox(enable: boolean, optionElement: HTMLSpanElement): void {
if (enable) {
optionElement.classList.add('option-enabled');
} else {
optionElement.classList.remove('option-enabled');
}
this.searchInput!.focus();
}
private onInputChanged = (event: React.KeyboardEvent): void => {
// move to previous search result on `Shift + Enter`
if (event && event.shiftKey && event.keyCode === Key.ENTER.keyCode) {
this.search(false, 'previous');
return;
}
// move to next search result on `Enter`
if (event && event.keyCode === Key.ENTER.keyCode) {
this.search(false, 'next');
return;
}
this.search(true, 'next');
};
search(incremental: boolean, searchDirection: 'next' | 'previous'): void {
if (this.searchInput) {
this.searchOptions.incremental = incremental;
const searchText = this.searchInput.value;
if (searchDirection === 'next') {
this.searchAddon.findNext(searchText, this.searchOptions);
}
if (searchDirection === 'previous') {
this.searchAddon.findPrevious(searchText, this.searchOptions);
}
}
}
private handleNextButtonClicked = (): void => {
this.search(false, 'next');
};
private handlePreviousButtonClicked = (): void => {
this.search(false, 'previous');
};
override onAfterHide(): void {
this.terminal.focus();
}
override onAfterShow(): void {
if (this.searchInput) {
this.searchInput.select();
}
}
}

View File

@@ -0,0 +1,45 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { URI } from '@theia/core';
import { TerminalService } from './base/terminal-service';
import { TerminalWidget, TerminalWidgetOptions } from './base/terminal-widget';
import { TerminalProfile } from './terminal-profile-service';
export class ShellTerminalProfile implements TerminalProfile {
get shellPath(): string | undefined {
return this.options.shellPath;
}
constructor(protected readonly terminalService: TerminalService, protected readonly options: TerminalWidgetOptions) { }
async start(): Promise<TerminalWidget> {
const widget = await this.terminalService.newTerminal(this.options);
widget.start();
return widget;
}
/**
* Makes a copy of this profile modified with the options given
* as an argument.
* @param options the options to override
* @returns a modified copy of this profile
*/
modify(options: { cwd?: string | URI }): TerminalProfile {
return new ShellTerminalProfile(this.terminalService, { ...this.options, ...options });
}
}

View File

@@ -0,0 +1,99 @@
/********************************************************************************
* Copyright (C) 2019 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
.theia-search-terminal-widget-parent {
background: var(--theia-sideBar-background);
position: absolute;
margin: 0px;
border: var(--theia-border-width) solid transparent;
padding: 0px;
top: 1px;
right: 19px;
z-index: 10;
}
.theia-search-terminal-widget-parent .theia-search-elem-box {
display: flex;
margin: 0px;
border: var(--theia-border-width) solid transparent;
padding: 0px;
align-items: center;
color: var(--theia-input-foreground);
background: var(--theia-input-background);
}
.theia-search-terminal-widget-parent .theia-search-elem-box input {
margin-left: 5px;
padding: 0px;
width: 100px;
height: 18px;
color: inherit;
background-color: inherit;
border: var(--theia-border-width) solid transparent;
outline: none;
}
.theia-search-terminal-widget-parent
.theia-search-elem-box
.search-elem.codicon {
height: 16px;
width: 18px;
}
.theia-search-terminal-widget-parent .search-elem.codicon {
border: var(--theia-border-width) solid transparent;
height: 20px;
width: 20px;
opacity: 0.7;
outline: none;
color: var(--theia-input-foreground);
padding: 0px;
margin-left: 3px;
}
.theia-search-terminal-widget-parent .search-elem:hover {
opacity: 1;
}
.theia-search-terminal-widget-parent .theia-search-elem-box.focused {
border: var(--theia-border-width) solid var(--theia-focusBorder);
}
.theia-search-terminal-widget-parent
.theia-search-elem-box
.search-elem.option-enabled {
border: var(--theia-border-width) solid var(--theia-inputOption-activeBorder);
background-color: var(--theia-inputOption-activeBackground);
}
.theia-search-terminal-widget-parent .theia-search-terminal-widget {
margin: 2px;
display: flex;
align-items: center;
font: var(--theia-content-font-size);
color: var(--theia-input-foreground);
}
.theia-search-terminal-widget-parent .theia-search-terminal-widget button {
background-color: transparent;
}
.theia-search-terminal-widget-parent
.theia-search-terminal-widget
button:focus {
border: var(--theia-border-width) var(--theia-focusBorder) solid;
}

View File

@@ -0,0 +1,32 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
.terminal-container {
width: 100%;
height: 100%;
padding: var(--theia-code-padding);
background: var(--theia-terminal-background);
}
.xterm .xterm-screen canvas {
/* fix random 1px white border on terminal in Firefox. See https://github.com/eclipse-theia/theia/issues/4665 */
border: 1px solid var(--theia-terminal-background);
}
.terminal-container .xterm .xterm-helper-textarea {
/* fix secondary cursor-like issue. See https://github.com/eclipse-theia/theia/issues/8158 */
opacity: 0 !important;
}

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { TerminalContribution } from './terminal-widget-impl';
/** @deprecated @since 1.28.0 import from `terminal-widget-impl` instead. */
export { TerminalContribution };

View File

@@ -0,0 +1,92 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import { isFirefox } from '@theia/core/lib/browser';
@injectable()
export class TerminalCopyOnSelectionHandler {
private textToCopy: string;
private interceptCopy: boolean;
private copyListener = (ev: ClipboardEvent) => {
if (this.interceptCopy && ev.clipboardData) {
ev.clipboardData.setData('text/plain', this.textToCopy);
ev.preventDefault();
}
};
@postConstruct()
protected init(): void {
document.addEventListener('copy', this.copyListener);
}
private async clipBoardCopyIsGranted(): Promise<boolean> {
// Unfortunately Firefox doesn't support permission check `clipboard-write`, so let try to copy anyway,
if (isFirefox) {
return true;
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const permissions = (navigator as any).permissions;
const { state } = await permissions.query({ name: 'clipboard-write' });
if (state === 'granted') {
return true;
}
} catch (e) { }
return false;
}
private executeCommandCopy(): void {
try {
this.interceptCopy = true;
document.execCommand('copy');
this.interceptCopy = false;
} catch (e) {
// do nothing
}
}
private async writeToClipBoard(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const clipboard = (navigator as any).clipboard;
if (!clipboard) {
this.executeCommandCopy();
return;
}
try {
await clipboard.writeText(this.textToCopy);
} catch (e) {
this.executeCommandCopy();
}
}
async copy(text: string): Promise<void> {
this.textToCopy = text;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const permissions = (navigator as any).permissions;
if (permissions && permissions.query && await this.clipBoardCopyIsGranted()) {
await this.writeToClipBoard();
} else {
this.executeCommandCopy();
}
}
}

View File

@@ -0,0 +1,289 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { OS, Path, QuickInputService } from '@theia/core';
import { OpenerService } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Position } from '@theia/editor/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { TerminalWidget } from './base/terminal-widget';
import { TerminalLink, TerminalLinkProvider } from './terminal-link-provider';
import { TerminalWidgetImpl } from './terminal-widget-impl';
import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
@injectable()
export class FileLinkProvider implements TerminalLinkProvider {
@inject(OpenerService) protected readonly openerService: OpenerService;
@inject(QuickInputService) protected readonly quickInputService: QuickInputService;
@inject(FileService) protected fileService: FileService;
@inject(FileSearchService) protected searchService: FileSearchService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
async provideLinks(line: string, terminal: TerminalWidget): Promise<TerminalLink[]> {
const links: TerminalLink[] = [];
const regExp = await this.createRegExp();
let regExpResult: RegExpExecArray | null;
while (regExpResult = regExp.exec(line)) {
const match = regExpResult[0];
if (await this.isValidFile(match, terminal)) {
links.push({
startIndex: regExp.lastIndex - match.length,
length: match.length,
handle: () => this.open(match, terminal)
});
}
}
return links;
}
protected async createRegExp(): Promise<RegExp> {
const baseLocalLinkClause = OS.backend.isWindows ? winLocalLinkClause : unixLocalLinkClause;
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`, 'g');
}
protected async isValidFile(match: string, terminal: TerminalWidget): Promise<boolean> {
try {
const toOpen = await this.toURI(match, await this.getCwd(terminal));
if (toOpen) {
// TODO: would be better to ask the opener service, but it returns positively even for unknown files.
return this.isValidFileURI(toOpen);
}
} catch (err) {
console.trace('Error validating ' + match, err);
}
return false;
}
protected async isValidFileURI(uri: URI): Promise<boolean> {
try {
const stat = await this.fileService.resolve(uri);
return !stat.isDirectory;
} catch { }
return false;
}
protected async toURI(match: string, cwd: URI): Promise<URI | undefined> {
const path = await this.extractPath(match);
if (!path) {
return;
}
const pathObj = new Path(path);
return pathObj.isAbsolute ? cwd.withPath(path) : cwd.resolve(path);
}
protected async getCwd(terminal: TerminalWidget): Promise<URI> {
if (terminal instanceof TerminalWidgetImpl) {
return terminal.cwd;
}
return terminal.lastCwd;
}
protected async extractPath(link: string): Promise<string | undefined> {
const matches: string[] | null = (await this.createRegExp()).exec(link);
if (!matches) {
return undefined;
}
return matches[1];
}
async open(match: string, terminal: TerminalWidget): Promise<void> {
const toOpen = await this.toURI(match, await this.getCwd(terminal));
if (!toOpen) {
return;
}
const position = await this.extractPosition(match);
return this.openURI(toOpen, position);
}
async openURI(toOpen: URI, position: Position): Promise<void> {
let options = {};
if (position) {
options = { selection: { start: position } };
}
try {
const opener = await this.openerService.getOpener(toOpen, options);
opener.open(toOpen, options);
} catch (err) {
console.error('Cannot open link ' + toOpen, err);
}
}
protected async extractPosition(link: string): Promise<Position> {
const matches: string[] | null = (await this.createRegExp()).exec(link);
const info: Position = { line: 1, character: 1 };
if (!matches) {
return info;
}
const lineAndColumnMatchIndex = this.getLineAndColumnMatchIndex();
for (let i = 0; i < lineAndColumnClause.length; i++) {
const lineMatchIndex = lineAndColumnMatchIndex + (lineAndColumnClauseGroupCount * i);
const rowNumber = matches[lineMatchIndex];
if (rowNumber) {
info.line = parseInt(rowNumber, 10) - 1;
const columnNumber = matches[lineMatchIndex + 2];
if (columnNumber) {
info.character = parseInt(columnNumber, 10) - 1;
}
break;
}
}
return info;
}
protected getLineAndColumnMatchIndex(): number {
return OS.backend.isWindows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex;
}
}
@injectable()
export class FileDiffPreLinkProvider extends FileLinkProvider {
override async createRegExp(): Promise<RegExp> {
return /^--- a\/(\S*)/g;
}
}
@injectable()
export class FileDiffPostLinkProvider extends FileLinkProvider {
override async createRegExp(): Promise<RegExp> {
return /^\+\+\+ b\/(\S*)/g;
}
}
@injectable()
export class LocalFileLinkProvider extends FileLinkProvider {
override async createRegExp(): Promise<RegExp> {
// match links that might not start with a separator, e.g. 'foo.bar', but don't match single words e.g. 'foo'
const baseLocalUnixLinkClause =
'((' + pathPrefix + '|' +
'(' + excludedPathCharactersClause + '+(' + pathSeparatorClause + '|' + '\\.' + ')' + excludedPathCharactersClause + '+))' +
'(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)*)';
const baseLocalWindowsLinkClause =
'((' + winPathPrefix + '|' +
'(' + winExcludedPathCharactersClause + '+(' + winPathSeparatorClause + '|' + '\\.' + ')' + winExcludedPathCharactersClause + '+))' +
'(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)*)';
const baseLocalLinkClause = OS.backend.isWindows ? baseLocalWindowsLinkClause : baseLocalUnixLinkClause;
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`, 'g');
}
override async provideLinks(line: string, terminal: TerminalWidget): Promise<TerminalLink[]> {
const links: TerminalLink[] = [];
const regExp = await this.createRegExp();
let regExpResult: RegExpExecArray | null;
while (regExpResult = regExp.exec(line)) {
const match = regExpResult[0];
const searchTerm = await this.extractPath(match);
if (searchTerm) {
links.push({
startIndex: regExp.lastIndex - match.length,
length: match.length,
handle: async () => {
const fileUri = await this.isValidWorkspaceFile(searchTerm, terminal);
if (fileUri) {
const position = await this.extractPosition(match);
this.openURI(fileUri, position);
} else {
this.quickInputService.open(match);
}
}
});
}
}
return links;
}
protected override getLineAndColumnMatchIndex(): number {
return OS.backend.isWindows ? 14 : 12;
}
protected async isValidWorkspaceFile(searchTerm: string | undefined, terminal: TerminalWidget): Promise<URI | undefined> {
if (!searchTerm) {
return undefined;
}
const cwd = await this.getCwd(terminal);
// remove any leading ./, ../ etc. as they can't be searched
searchTerm = searchTerm.replace(/^(\.+[\\/])+/, '');
const workspaceRoots = this.workspaceService.tryGetRoots().map(root => root.resource.toString());
// try and find a matching file in the workspace
const files = (await this.searchService.find(searchTerm, {
rootUris: [cwd.toString(), ...workspaceRoots],
fuzzyMatch: true,
limit: 1
}));
// checks if the string ends in a separator + searchTerm
const regex = new RegExp(`[\\\\|\\/]${searchTerm}$`);
if (files.length && regex.test(files[0])) {
const fileUri = new URI(files[0]);
const valid = await this.isValidFileURI(fileUri);
if (valid) {
return fileUri;
}
}
}
}
// The following regular expressions are taken from:
// https://github.com/microsoft/vscode/blob/b118105bf28d773fbbce683f7230d058be2f89a7/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts#L34-L58
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
// '":; are allowed in paths but they are often separators so ignore them
// Also disallow \\ to prevent a catastrophic backtracking case #24795
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 c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)';
/** As xterm reads from DOM, space in that case is non-breaking char ASCII code - 160, replacing space with nonBreakingSpace or space ASCII code - 32. */
const lineAndColumnClause = [
// "(file path)", line 45 [see #40468]
'((\\S*)[\'"], line ((\\d+)( column (\\d+))?))',
// "(file path)",45 [see #78205]
'((\\S*)[\'"],((\\d+)(:(\\d+))?))',
// (file path) on line 8, column 13
'((\\S*) on line ((\\d+)(, column (\\d+))?))',
// (file path):line 8, column 13
'((\\S*):line ((\\d+)(, column (\\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\\(\\)]*)(\\s?[\\(\\[](\\d+)(,\\s?(\\d+))?)[\\)\\]])',
// (file path):336, (file path):336:9
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)'
].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;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,135 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import '../../src/browser/style/terminal.css';
import 'xterm/css/xterm.css';
import { ContainerModule, Container } from '@theia/core/shared/inversify';
import { CommandContribution, MenuContribution, nls } from '@theia/core/lib/common';
import { bindContributionProvider } from '@theia/core';
import { KeybindingContribution, WebSocketConnectionProvider, WidgetFactory, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { TerminalFrontendContribution } from './terminal-frontend-contribution';
import { TerminalWidgetImpl, TERMINAL_WIDGET_FACTORY_ID } from './terminal-widget-impl';
import { TerminalWidget, TerminalWidgetOptions } from './base/terminal-widget';
import { ITerminalServer, terminalPath } from '../common/terminal-protocol';
import { TerminalWatcher } from '../common/terminal-watcher';
import { IShellTerminalServer, shellTerminalPath, ShellTerminalServerProxy } from '../common/shell-terminal-protocol';
import { TerminalService } from './base/terminal-service';
import { bindTerminalPreferences } from '../common/terminal-preferences';
import { TerminalContribution } from './terminal-contribution';
import { TerminalSearchWidgetFactory } from './search/terminal-search-widget';
import { TerminalQuickOpenService, TerminalQuickOpenContribution } from './terminal-quick-open-service';
import { createTerminalSearchFactory } from './search/terminal-search-container';
import { TerminalCopyOnSelectionHandler } from './terminal-copy-on-selection-handler';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { TerminalThemeService } from './terminal-theme-service';
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
import { createXtermLinkFactory, TerminalLinkProvider, TerminalLinkProviderContribution, XtermLinkFactory } from './terminal-link-provider';
import { UrlLinkProvider } from './terminal-url-link-provider';
import { FileDiffPostLinkProvider, FileDiffPreLinkProvider, FileLinkProvider, LocalFileLinkProvider } from './terminal-file-link-provider';
import {
ContributedTerminalProfileStore, DefaultProfileStore, DefaultTerminalProfileService,
TerminalProfileService, TerminalProfileStore, UserTerminalProfileStore
} from './terminal-profile-service';
export default new ContainerModule(bind => {
bindTerminalPreferences(bind);
bind(TerminalWidget).to(TerminalWidgetImpl).inTransientScope();
bind(TerminalWatcher).toSelf().inSingletonScope();
let terminalNum = 0;
bind(WidgetFactory).toDynamicValue(ctx => ({
id: TERMINAL_WIDGET_FACTORY_ID,
createWidget: (options: TerminalWidgetOptions) => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = ctx.container;
const counter = terminalNum++;
const domId = options.id || 'terminal-' + counter;
const widgetOptions: TerminalWidgetOptions = {
title: nls.localizeByDefault('Terminal {0}', counter),
useServerTitle: true,
destroyTermOnClose: true,
...options
};
child.bind(TerminalWidgetOptions).toConstantValue(widgetOptions);
child.bind('terminal-dom-id').toConstantValue(domId);
child.bind(TerminalSearchWidgetFactory).toDynamicValue(context => createTerminalSearchFactory(context.container));
return child.get(TerminalWidget);
}
}));
bind(TerminalQuickOpenService).toSelf().inSingletonScope();
bind(TerminalCopyOnSelectionHandler).toSelf().inSingletonScope();
bind(TerminalQuickOpenContribution).toSelf().inSingletonScope();
for (const identifier of [CommandContribution, QuickAccessContribution]) {
bind(identifier).toService(TerminalQuickOpenContribution);
}
bind(TerminalThemeService).toSelf().inSingletonScope();
bind(TerminalFrontendContribution).toSelf().inSingletonScope();
bind(TerminalService).toService(TerminalFrontendContribution);
for (const identifier of [CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, ColorContribution]) {
bind(identifier).toService(TerminalFrontendContribution);
}
bind(ITerminalServer).toDynamicValue(ctx => {
const connection = ctx.container.get(WebSocketConnectionProvider);
const terminalWatcher = ctx.container.get(TerminalWatcher);
return connection.createProxy<ITerminalServer>(terminalPath, terminalWatcher.getTerminalClient());
}).inSingletonScope();
bind(ShellTerminalServerProxy).toDynamicValue(ctx => {
const connection = ctx.container.get(WebSocketConnectionProvider);
const terminalWatcher = ctx.container.get(TerminalWatcher);
return connection.createProxy<IShellTerminalServer>(shellTerminalPath, terminalWatcher.getTerminalClient());
}).inSingletonScope();
bind(IShellTerminalServer).toService(ShellTerminalServerProxy);
bindContributionProvider(bind, TerminalContribution);
// terminal link provider contribution point
bindContributionProvider(bind, TerminalLinkProvider);
bind(TerminalLinkProviderContribution).toSelf().inSingletonScope();
bind(TerminalContribution).toService(TerminalLinkProviderContribution);
bind(XtermLinkFactory).toFactory(createXtermLinkFactory);
// default terminal link provider
bind(UrlLinkProvider).toSelf().inSingletonScope();
bind(TerminalLinkProvider).toService(UrlLinkProvider);
bind(FileLinkProvider).toSelf().inSingletonScope();
bind(TerminalLinkProvider).toService(FileLinkProvider);
bind(FileDiffPreLinkProvider).toSelf().inSingletonScope();
bind(TerminalLinkProvider).toService(FileDiffPreLinkProvider);
bind(FileDiffPostLinkProvider).toSelf().inSingletonScope();
bind(TerminalLinkProvider).toService(FileDiffPostLinkProvider);
bind(LocalFileLinkProvider).toSelf().inSingletonScope();
bind(TerminalLinkProvider).toService(LocalFileLinkProvider);
bind(ContributedTerminalProfileStore).to(DefaultProfileStore).inSingletonScope();
bind(UserTerminalProfileStore).to(DefaultProfileStore).inSingletonScope();
bind(TerminalProfileService).toDynamicValue(ctx => {
const userStore = ctx.container.get<TerminalProfileStore>(UserTerminalProfileStore);
const contributedStore = ctx.container.get<TerminalProfileStore>(ContributedTerminalProfileStore);
return new DefaultTerminalProfileService(userStore, contributedStore);
}).inSingletonScope();
bind(FrontendApplicationContribution).toService(TerminalFrontendContribution);
});

View File

@@ -0,0 +1,187 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import type { IBufferRange, IBufferLine, IBuffer, Terminal } from 'xterm';
export const LinkContext = Symbol('LinkContext');
export interface LinkContext {
text: string;
startLine: number;
lines: IBufferLine[];
}
/**
* Mimics VS Code IRange
*/
interface TerminalRange {
readonly startLineNumber: number;
readonly startColumn: number;
readonly endLineNumber: number;
readonly endColumn: number;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation and others. All rights reserved.
* Licensed under the MIT License.
*--------------------------------------------------------------------------------------------*/
export function getLinkContext(terminal: Terminal, line: number, maxLinkLength = 2000): LinkContext {
// The following method is based on VS Code's TerminalLinkDetectorAdapter._provideLinks()
// https://github.com/microsoft/vscode/blob/7888ff3a6b104e9e2e3d0f7890ca92dd0828215f/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts#L51
let startLine = line - 1;
let endLine = startLine;
const lines: IBufferLine[] = [terminal.buffer.active.getLine(startLine)!];
// Cap the maximum context on either side of the line being provided, by taking the context
// around the line being provided for this ensures the line the pointer is on will have
// links provided.
const maxLineContext = Math.max(maxLinkLength / terminal.cols);
const minStartLine = Math.max(startLine - maxLineContext, 0);
const maxEndLine = Math.min(endLine + maxLineContext, terminal.buffer.active.length);
while (startLine >= minStartLine && terminal.buffer.active.getLine(startLine)?.isWrapped) {
lines.unshift(terminal.buffer.active.getLine(startLine - 1)!);
startLine--;
}
while (endLine < maxEndLine && terminal.buffer.active.getLine(endLine + 1)?.isWrapped) {
lines.push(terminal.buffer.active.getLine(endLine + 1)!);
endLine++;
}
const text = getXtermLineContent(terminal.buffer.active, startLine, endLine, terminal.cols);
return { text, startLine, lines };
}
// The following code is taken as is from
// https://github.com/microsoft/vscode/blob/7888ff3a6b104e9e2e3d0f7890ca92dd0828215f/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts#L1
/**
* Converts a possibly wrapped link's range (comprised of string indices) into a buffer range that plays nicely with xterm.js
*
* @param lines A single line (not the entire buffer)
* @param bufferWidth The number of columns in the terminal
* @param range The link range - string indices
* @param startLine The absolute y position (on the buffer) of the line
*/
export function convertLinkRangeToBuffer(
lines: IBufferLine[],
bufferWidth: number,
range: TerminalRange,
startLine: number
): IBufferRange {
const bufferRange: IBufferRange = {
start: {
x: range.startColumn,
y: range.startLineNumber + startLine
},
end: {
x: range.endColumn - 1,
y: range.endLineNumber + startLine
}
};
// Shift start range right for each wide character before the link
let startOffset = 0;
const startWrappedLineCount = Math.ceil(range.startColumn / bufferWidth);
for (let y = 0; y < Math.min(startWrappedLineCount); y++) {
const lineLength = Math.min(bufferWidth, range.startColumn - y * bufferWidth);
let lineOffset = 0;
const line = lines[y];
// Sanity check for line, apparently this can happen but it's not clear under what
// circumstances this happens. Continue on, skipping the remainder of start offset if this
// happens to minimize impact.
if (!line) {
break;
}
for (let x = 0; x < Math.min(bufferWidth, lineLength + lineOffset); x++) {
const cell = line.getCell(x)!;
const width = cell.getWidth();
if (width === 2) {
lineOffset++;
}
const char = cell.getChars();
if (char.length > 1) {
lineOffset -= char.length - 1;
}
}
startOffset += lineOffset;
}
// Shift end range right for each wide character inside the link
let endOffset = 0;
const endWrappedLineCount = Math.ceil(range.endColumn / bufferWidth);
for (let y = Math.max(0, startWrappedLineCount - 1); y < endWrappedLineCount; y++) {
const start = (y === startWrappedLineCount - 1 ? (range.startColumn + startOffset) % bufferWidth : 0);
const lineLength = Math.min(bufferWidth, range.endColumn + startOffset - y * bufferWidth);
const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0);
let lineOffset = 0;
const line = lines[y];
// Sanity check for line, apparently this can happen but it's not clear under what
// circumstances this happens. Continue on, skipping the remainder of start offset if this
// happens to minimize impact.
if (!line) {
break;
}
for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) {
const cell = line.getCell(x)!;
const width = cell.getWidth();
// Offset for 0 cells following wide characters
if (width === 2) {
lineOffset++;
}
// Offset for early wrapping when the last cell in row is a wide character
if (x === bufferWidth - 1 && cell.getChars() === '') {
lineOffset++;
}
}
endOffset += lineOffset;
}
// Apply the width character offsets to the result
bufferRange.start.x += startOffset;
bufferRange.end.x += startOffset + endOffset;
// Convert back to wrapped lines
while (bufferRange.start.x > bufferWidth) {
bufferRange.start.x -= bufferWidth;
bufferRange.start.y++;
}
while (bufferRange.end.x > bufferWidth) {
bufferRange.end.x -= bufferWidth;
bufferRange.end.y++;
}
return bufferRange;
}
function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string {
// Cap the maximum number of lines generated to prevent potential performance problems. This is
// more of a sanity check as the wrapped line should already be trimmed down at this point.
const maxLineLength = Math.max(2048 / cols * 2);
lineEnd = Math.min(lineEnd, lineStart + maxLineLength);
let content = '';
for (let i = lineStart; i <= lineEnd; i++) {
// Make sure only 0 to cols are considered as resizing when windows mode is enabled will
// retain buffer data outside of the terminal width as reflow is disabled.
const line = buffer.getLine(i);
if (line) {
content += line.translateToString(true, 0, cols);
}
}
return content;
}

View File

@@ -0,0 +1,203 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CancellationToken, ContributionProvider, DisposableCollection, disposableTimeout, isOSX } from '@theia/core';
import { PreferenceService } from '@theia/core/lib/common';
import { inject, injectable, interfaces, named, postConstruct } from '@theia/core/shared/inversify';
import { IBufferRange, ILink, ILinkDecorations } from 'xterm';
import { TerminalWidget } from './base/terminal-widget';
import { TerminalContribution } from './terminal-contribution';
import { convertLinkRangeToBuffer, getLinkContext, LinkContext } from './terminal-link-helpers';
import { TerminalWidgetImpl } from './terminal-widget-impl';
export const TerminalLinkProvider = Symbol('TerminalLinkProvider');
export interface TerminalLinkProvider {
provideLinks(line: string, terminal: TerminalWidget, cancellationToken?: CancellationToken): Promise<TerminalLink[]>;
}
export const TerminalLink = Symbol('TerminalLink');
export interface TerminalLink {
startIndex: number;
length: number;
tooltip?: string;
handle(): Promise<void>;
}
export const XtermLink = Symbol('XtermLink');
export const XtermLinkFactory = Symbol('XtermLinkFactory');
export type XtermLinkFactory = (link: TerminalLink, terminal: TerminalWidgetImpl, context: LinkContext) => ILink;
export function createXtermLinkFactory(ctx: interfaces.Context): XtermLinkFactory {
return (link: TerminalLink, terminal: TerminalWidgetImpl, context: LinkContext): ILink => {
const container = ctx.container.createChild();
container.bind(TerminalLink).toConstantValue(link);
container.bind(TerminalWidgetImpl).toConstantValue(terminal);
container.bind(LinkContext).toConstantValue(context);
container.bind(XtermLinkAdapter).toSelf().inSingletonScope();
container.bind(XtermLink).toService(XtermLinkAdapter);
const provider = container.get<ILink>(XtermLink);
return provider;
};
}
@injectable()
export class TerminalLinkProviderContribution implements TerminalContribution {
@inject(ContributionProvider) @named(TerminalLinkProvider)
protected readonly terminalLinkContributionProvider: ContributionProvider<TerminalLinkProvider>;
@inject(XtermLinkFactory)
protected readonly xtermLinkFactory: XtermLinkFactory;
onCreate(terminalWidget: TerminalWidgetImpl): void {
terminalWidget.getTerminal().registerLinkProvider({
provideLinks: (line, provideLinks) => this.provideTerminalLinks(terminalWidget, line, provideLinks)
});
}
protected async provideTerminalLinks(terminal: TerminalWidgetImpl, line: number, provideLinks: (links?: ILink[]) => void): Promise<void> {
const context = getLinkContext(terminal.getTerminal(), line);
const linkProviderPromises: Promise<TerminalLink[]>[] = [];
for (const provider of this.terminalLinkContributionProvider.getContributions(true)) {
linkProviderPromises.push(provider.provideLinks(context.text, terminal));
}
const xtermLinks: ILink[] = [];
for (const providerResult of await Promise.allSettled(linkProviderPromises)) {
if (providerResult.status === 'fulfilled') {
const providedLinks = providerResult.value;
xtermLinks.push(...providedLinks.map(link => this.xtermLinkFactory(link, terminal, context)));
} else {
console.warn('Terminal link provider failed to provide links', providerResult.reason);
}
}
provideLinks(xtermLinks);
}
}
const DELAY_PREFERENCE = 'workbench.hover.delay';
@injectable()
export class XtermLinkAdapter implements ILink {
text: string;
range: IBufferRange;
decorations: ILinkDecorations;
@inject(TerminalLink) protected link: TerminalLink;
@inject(TerminalWidgetImpl) protected terminalWidget: TerminalWidgetImpl;
@inject(LinkContext) protected context: LinkContext;
@inject(PreferenceService) protected readonly preferences: PreferenceService;
protected toDispose = new DisposableCollection();
protected mouseEnteredHover = false;
protected mouseLeftHover = false;
@postConstruct()
initializeLinkFields(): void {
const range = {
startColumn: this.link.startIndex + 1,
startLineNumber: 1,
endColumn: this.link.startIndex + this.link.length + 1,
endLineNumber: 1
};
const terminal = this.terminalWidget.getTerminal();
this.range = convertLinkRangeToBuffer(this.context.lines, terminal.cols, range, this.context.startLine);
this.text = this.context.text.substring(this.link.startIndex, this.link.startIndex + this.link.length) || '';
}
hover(event: MouseEvent, text: string): void {
this.scheduleHover(event);
}
protected scheduleHover(event: MouseEvent): void {
this.cancelHover();
const delay: number = this.preferences.get(DELAY_PREFERENCE) ?? 500;
this.toDispose.push(disposableTimeout(() => this.showHover(event), delay));
}
protected showHover(event: MouseEvent): void {
this.toDispose.push(this.terminalWidget.onMouseEnterLinkHover(() => this.mouseEnteredHover = true));
this.toDispose.push(this.terminalWidget.onMouseLeaveLinkHover(mouseEvent => {
this.mouseLeftHover = true;
this.leave(mouseEvent);
}));
this.terminalWidget.showLinkHover(
() => this.executeLinkHandler(),
event.clientX,
event.clientY,
this.link.tooltip
);
}
leave(event: MouseEvent): void {
this.toDispose.push(disposableTimeout(() => {
if (!this.mouseEnteredHover || this.mouseLeftHover) {
this.cancelHover();
}
}, 50));
}
protected cancelHover(): void {
this.mouseEnteredHover = false;
this.mouseLeftHover = false;
this.toDispose.dispose();
this.terminalWidget.hideLinkHover();
}
activate(event: MouseEvent, text: string): void {
event.preventDefault();
if (this.isModifierKeyDown(event) || this.wasTouchEvent(event, this.terminalWidget.lastTouchEndEvent)) {
this.executeLinkHandler();
} else {
this.terminalWidget.getTerminal().focus();
}
}
protected executeLinkHandler(): void {
this.link.handle();
this.cancelHover();
}
protected isModifierKeyDown(event: MouseEvent | KeyboardEvent): boolean {
return isOSX ? event.metaKey : event.ctrlKey;
}
protected wasTouchEvent(event: MouseEvent, lastTouchEnd?: TouchEvent): boolean {
if (!lastTouchEnd) {
return false;
}
if ((event.timeStamp - lastTouchEnd.timeStamp) > 400) {
// A 'touchend' event typically precedes a matching 'click' event by 50ms.
return false;
}
if (Math.abs(event.pageX - (lastTouchEnd as unknown as MouseEvent).pageX) > 5) {
// Matching 'touchend' and 'click' events typically have the same page coordinates,
// plus or minus 1 pixel.
return false;
}
if (Math.abs(event.pageY - (lastTouchEnd as unknown as MouseEvent).pageY) > 5) {
return false;
}
// We have a match! This link was tapped.
return true;
}
}

View File

@@ -0,0 +1,180 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Emitter, Event } from '@theia/core';
import { injectable, unmanaged } from '@theia/core/shared/inversify';
import { TerminalWidget } from './base/terminal-widget';
import { ShellTerminalProfile } from './shell-terminal-profile';
export const TerminalProfileService = Symbol('TerminalProfileService');
export const ContributedTerminalProfileStore = Symbol('ContributedTerminalProfileStore');
export const UserTerminalProfileStore = Symbol('UserTerminalProfileStore');
export interface TerminalProfile {
start(): Promise<TerminalWidget>;
}
export const NULL_PROFILE: TerminalProfile = {
start: async () => { throw new Error('you cannot start a null profile'); }
};
export interface TerminalProfileService {
onAdded: Event<string>;
onRemoved: Event<string>;
getProfile(id: string): TerminalProfile | undefined
readonly all: [string, TerminalProfile][];
setDefaultProfile(id: string): void;
readonly onDidChangeDefaultShell: Event<string>;
readonly defaultProfile: TerminalProfile | undefined;
}
export interface TerminalProfileStore {
onAdded: Event<[string, TerminalProfile]>;
onRemoved: Event<string>;
registerTerminalProfile(id: string, profile: TerminalProfile): void;
unregisterTerminalProfile(id: string): void;
hasProfile(id: string): boolean;
getProfile(id: string): TerminalProfile | undefined
readonly all: [string, TerminalProfile][];
}
@injectable()
export class DefaultProfileStore implements TerminalProfileStore {
protected readonly onAddedEmitter: Emitter<[string, TerminalProfile]> = new Emitter();
protected readonly onRemovedEmitter: Emitter<string> = new Emitter();
protected readonly profiles: Map<string, TerminalProfile> = new Map();
onAdded: Event<[string, TerminalProfile]> = this.onAddedEmitter.event;
onRemoved: Event<string> = this.onRemovedEmitter.event;
registerTerminalProfile(id: string, profile: TerminalProfile): void {
this.profiles.set(id, profile);
this.onAddedEmitter.fire([id, profile]);
}
unregisterTerminalProfile(id: string): void {
this.profiles.delete(id);
this.onRemovedEmitter.fire(id);
}
hasProfile(id: string): boolean {
return this.profiles.has(id);
}
getProfile(id: string): TerminalProfile | undefined {
return this.profiles.get(id);
}
get all(): [string, TerminalProfile][] {
return [...this.profiles.entries()];
}
}
@injectable()
export class DefaultTerminalProfileService implements TerminalProfileService {
protected defaultProfileIndex = 0;
protected order: string[] = [];
protected readonly stores: TerminalProfileStore[];
protected readonly onAddedEmitter: Emitter<string> = new Emitter();
protected readonly onRemovedEmitter: Emitter<string> = new Emitter();
protected readonly onDidChangeDefaultShellEmitter: Emitter<string> = new Emitter();
onAdded: Event<string> = this.onAddedEmitter.event;
onRemoved: Event<string> = this.onRemovedEmitter.event;
onDidChangeDefaultShell: Event<string> = this.onDidChangeDefaultShellEmitter.event;
constructor(@unmanaged() ...stores: TerminalProfileStore[]) {
this.stores = stores;
for (const store of this.stores) {
store.onAdded(e => {
if (e[1] === NULL_PROFILE) {
this.handleRemoved(e[0]);
} else {
this.handleAdded(e[0]);
}
});
store.onRemoved(id => {
if (!this.getProfile(id)) {
this.handleRemoved(id);
} else {
// we may have removed a null profile
this.handleAdded(id);
}
});
}
}
handleRemoved(id: string): void {
const index = this.order.indexOf(id);
if (index >= 0 && !this.getProfile(id)) {
// the profile was removed, but it's still in the `order` array
this.order.splice(index, 1);
this.defaultProfileIndex = Math.max(0, Math.min(this.order.length - 1, index));
this.onRemovedEmitter.fire(id);
}
}
handleAdded(id: string): void {
const index = this.order.indexOf(id);
if (index < 0) {
this.order.push(id);
this.onAddedEmitter.fire(id);
}
}
get defaultProfile(): TerminalProfile | undefined {
const id = this.order[this.defaultProfileIndex];
if (id) {
return this.getProfile(id);
}
return undefined;
}
setDefaultProfile(id: string): void {
const profile = this.getProfile(id);
if (!profile) {
throw new Error(`Cannot set default to unknown profile '${id}' `);
}
this.defaultProfileIndex = this.order.indexOf(id);
if (profile instanceof ShellTerminalProfile && profile.shellPath) {
this.onDidChangeDefaultShellEmitter.fire(profile.shellPath);
} else {
this.onDidChangeDefaultShellEmitter.fire('');
}
}
getProfile(id: string): TerminalProfile | undefined {
for (const store of this.stores) {
if (store.hasProfile(id)) {
const found = store.getProfile(id);
return found === NULL_PROFILE ? undefined : found;
}
}
return undefined;
}
getId(profile: TerminalProfile): string | undefined {
for (const [id, p] of this.all) {
if (p === profile) {
return id;
}
}
}
get all(): [string, TerminalProfile][] {
return this.order.filter(id => !!this.getProfile(id)).map(id => [id, this.getProfile(id)!]);
}
}

View File

@@ -0,0 +1,132 @@
// *****************************************************************************
// Copyright (C) 2019 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, optional } from '@theia/core/shared/inversify';
import {
codiconArray,
QuickAccessContribution,
QuickAccessProvider,
QuickAccessRegistry,
QuickInputService
} from '@theia/core/lib/browser';
import { CancellationToken, CommandContribution, CommandRegistry, CommandService, nls } from '@theia/core/lib/common';
import { TerminalWidget } from './base/terminal-widget';
import { TerminalService } from './base/terminal-service';
import { TerminalCommands } from './terminal-frontend-contribution';
import { filterItems, QuickPickItem, QuickPicks } from '@theia/core/lib/browser/quick-input/quick-input-service';
@injectable()
export class TerminalQuickOpenService implements QuickAccessProvider {
static readonly PREFIX = 'term ';
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(QuickAccessRegistry)
protected readonly quickAccessRegistry: QuickAccessRegistry;
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(TerminalService)
protected readonly terminalService: TerminalService;
open(): void {
this.quickInputService?.open(TerminalQuickOpenService.PREFIX);
}
async getPicks(filter: string, token: CancellationToken): Promise<QuickPicks> {
const items: QuickPickItem[] = [];
// Get the sorted list of currently opened terminal widgets that aren't hidden from users
const widgets: TerminalWidget[] = this.terminalService.all.filter(widget => !widget.hiddenFromUser)
.sort((a: TerminalWidget, b: TerminalWidget) => this.compareItems(a, b));
for (const widget of widgets) {
items.push(this.toItem(widget));
}
// Append a quick open item to create a new terminal.
items.push({
label: nls.localizeByDefault('Create New Terminal'),
iconClasses: codiconArray('add'),
execute: () => this.doCreateNewTerminal()
});
return filterItems(items, filter);
}
registerQuickAccessProvider(): void {
this.quickAccessRegistry.registerQuickAccessProvider({
getInstance: () => this,
prefix: TerminalQuickOpenService.PREFIX,
placeholder: '',
helpEntries: [{ description: nls.localizeByDefault('Show All Opened Terminals'), needsEditor: false }]
});
}
/**
* Compare two terminal widgets by label. If labels are identical, compare by the widget id.
* @param a `TerminalWidget` for comparison
* @param b `TerminalWidget` for comparison
*/
protected compareItems(a: TerminalWidget, b: TerminalWidget): number {
const normalize = (str: string) => str.trim().toLowerCase();
if (normalize(a.title.label) !== normalize(b.title.label)) {
return normalize(a.title.label).localeCompare(normalize(b.title.label));
} else {
return normalize(a.id).localeCompare(normalize(b.id));
}
}
protected doCreateNewTerminal(): void {
this.commandService.executeCommand(TerminalCommands.NEW.id);
}
/**
* Convert the terminal widget to the quick pick item.
* @param {TerminalWidget} widget - the terminal widget.
* @returns quick pick item.
*/
protected toItem(widget: TerminalWidget): QuickPickItem {
return {
label: widget.title.label,
description: widget.id,
ariaLabel: widget.title.label,
execute: () => this.terminalService.open(widget)
};
}
}
/**
* TODO: merge it to TerminalFrontendContribution.
*/
@injectable()
export class TerminalQuickOpenContribution implements CommandContribution, QuickAccessContribution {
@inject(TerminalQuickOpenService)
protected readonly terminalQuickOpenService: TerminalQuickOpenService;
registerQuickAccessProvider(): void {
this.terminalQuickOpenService.registerQuickAccessProvider();
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(TerminalCommands.SHOW_ALL_OPENED_TERMINALS, {
execute: () => this.terminalQuickOpenService.open()
});
}
}

View File

@@ -0,0 +1,63 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ITheme } from 'xterm';
import { injectable, inject } from '@theia/core/shared/inversify';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ThemeChangeEvent } from '@theia/core/lib/common/theme';
import { Event } from '@theia/core';
import { terminalAnsiColorMap } from '../common/terminal-preferences';
@injectable()
export class TerminalThemeService {
@inject(ColorRegistry) protected readonly colorRegistry: ColorRegistry;
@inject(ThemeService) protected readonly themeService: ThemeService;
get onDidChange(): Event<ThemeChangeEvent> {
return this.themeService.onDidColorThemeChange;
}
get theme(): ITheme {
const foregroundColor = this.colorRegistry.getCurrentColor('terminal.foreground');
const backgroundColor = this.colorRegistry.getCurrentColor('terminal.background') || this.colorRegistry.getCurrentColor('panel.background');
const cursorColor = this.colorRegistry.getCurrentColor('terminalCursor.foreground') || foregroundColor;
const cursorAccentColor = this.colorRegistry.getCurrentColor('terminalCursor.background') || backgroundColor;
const selectionBackgroundColor = this.colorRegistry.getCurrentColor('terminal.selectionBackground');
const selectionInactiveBackground = this.colorRegistry.getCurrentColor('terminal.inactiveSelectionBackground');
const selectionForegroundColor = this.colorRegistry.getCurrentColor('terminal.selectionForeground');
const theme: ITheme = {
background: backgroundColor,
foreground: foregroundColor,
cursor: cursorColor,
cursorAccent: cursorAccentColor,
selectionBackground: selectionBackgroundColor,
selectionInactiveBackground: selectionInactiveBackground,
selectionForeground: selectionForegroundColor
};
// eslint-disable-next-line guard-for-in
for (const id in terminalAnsiColorMap) {
const colorId = id.substring(13);
const colorName = colorId.charAt(0).toLowerCase() + colorId.slice(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(theme as any)[colorName] = this.colorRegistry.getCurrentColor(id);
}
return theme;
}
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { OpenerService, open } from '@theia/core/lib/browser';
import { TerminalWidget } from './base/terminal-widget';
import { TerminalLink, TerminalLinkProvider } from './terminal-link-provider';
import URI from '@theia/core/lib/common/uri';
@injectable()
export class UrlLinkProvider implements TerminalLinkProvider {
@inject(OpenerService) protected readonly openerService: OpenerService;
protected readonly urlRegExp = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g;
protected readonly localhostRegExp = /(https?:\/\/)?(localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]{1,5})?([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g;
async provideLinks(line: string, terminal: TerminalWidget): Promise<TerminalLink[]> {
return [...this.matchUrlLinks(line), ...this.matchLocalhostLinks(line)];
}
protected matchUrlLinks(line: string): TerminalLink[] {
const links: TerminalLink[] = [];
let regExpResult: RegExpExecArray | null;
while (regExpResult = this.urlRegExp.exec(line)) {
const match = regExpResult![0];
links.push({
startIndex: this.urlRegExp.lastIndex - match.length,
length: match.length,
handle: () => open(this.openerService, new URI(match)).then()
});
}
return links;
}
protected matchLocalhostLinks(line: string): TerminalLink[] {
const links: TerminalLink[] = [];
let regExpResult: RegExpExecArray | null;
while (regExpResult = this.localhostRegExp.exec(line)) {
const match = regExpResult![0];
links.push({
startIndex: this.localhostRegExp.lastIndex - match.length,
length: match.length,
handle: async () => {
const uri = match.startsWith('http') ? match : `http://${match}`;
open(this.openerService, new URI(uri));
}
});
}
return links;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { RpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Disposable } from '@theia/core';
export interface TerminalProcessInfo {
executable: string
arguments: string[]
}
export interface IBaseTerminalServerOptions { }
export interface IBaseTerminalServer extends RpcServer<IBaseTerminalClient> {
create(IBaseTerminalServerOptions: object): Promise<number>;
getProcessId(id: number): Promise<number>;
getProcessInfo(id: number): Promise<TerminalProcessInfo>;
getCwdURI(id: number): Promise<string>;
resize(id: number, cols: number, rows: number): Promise<void>;
attach(id: number): Promise<number>;
onAttachAttempted(id: number): Promise<void>;
close(id: number): Promise<void>;
getDefaultShell(): Promise<string>;
}
export namespace IBaseTerminalServer {
export function validateId(id?: number): boolean {
return typeof id === 'number' && id !== -1;
}
}
export interface IBaseTerminalExitEvent {
terminalId: number;
// Either code and reason will be set or signal.
code?: number;
reason?: TerminalExitReason;
signal?: string;
attached?: boolean;
}
export enum TerminalExitReason {
Unknown = 0,
Shutdown = 1,
Process = 2,
User = 3,
Extension = 4,
}
export interface IBaseTerminalErrorEvent {
terminalId: number;
error: Error;
attached?: boolean;
}
export interface IBaseTerminalClient {
onTerminalExitChanged(event: IBaseTerminalExitEvent): void;
onTerminalError(event: IBaseTerminalErrorEvent): void;
updateTerminalEnvVariables(): void;
storeTerminalEnvVariables(data: string): void;
}
export class DispatchingBaseTerminalClient {
protected readonly clients = new Set<IBaseTerminalClient>();
push(client: IBaseTerminalClient): Disposable {
this.clients.add(client);
return Disposable.create(() => this.clients.delete(client));
}
onTerminalExitChanged(event: IBaseTerminalExitEvent): void {
this.clients.forEach(c => {
try {
c.onTerminalExitChanged(event);
} catch (e) {
console.error(e);
}
});
}
onTerminalError(event: IBaseTerminalErrorEvent): void {
this.clients.forEach(c => {
try {
c.onTerminalError(event);
} catch (e) {
console.error(e);
}
});
}
updateTerminalEnvVariables(): void {
this.clients.forEach(c => {
try {
c.updateTerminalEnvVariables();
} catch (e) {
console.error(e);
}
});
}
storeTerminalEnvVariables(data: string): void {
this.clients.forEach(c => {
try {
c.storeTerminalEnvVariables(data);
} catch (e) {
console.error(e);
}
});
}
}

View File

@@ -0,0 +1,103 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { RpcProxy } from '@theia/core';
import { IBaseTerminalServer, IBaseTerminalServerOptions } from './base-terminal-protocol';
import { OS } from '@theia/core/lib/common/os';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
export const IShellTerminalServer = Symbol('IShellTerminalServer');
export interface IShellTerminalServer extends IBaseTerminalServer {
hasChildProcesses(processId: number | undefined): Promise<boolean>;
getEnvVarCollectionDescriptionsByExtension(id: number): Promise<Map<string, (string | MarkdownString | undefined)[]>>;
getEnvVarCollections(): Promise<[string, string, boolean, SerializableEnvironmentVariableCollection][]>;
restorePersisted(jsonValue: string): void;
/**
* Sets an extension's environment variable collection.
*/
setCollection(extensionIdentifier: string, rootUri: string, persistent: boolean,
collection: SerializableEnvironmentVariableCollection, description: string | MarkdownString | undefined): void;
/**
* Deletes an extension's environment variable collection.
*/
deleteCollection(extensionIdentifier: string): void;
}
export const shellTerminalPath = '/services/shell-terminal';
export type ShellTerminalOSPreferences<T> = {
[key in OS.Type]: T
};
export interface IShellTerminalPreferences {
shell: ShellTerminalOSPreferences<string | undefined>,
shellArgs: ShellTerminalOSPreferences<string[]>
};
export interface IShellTerminalServerOptions extends IBaseTerminalServerOptions {
shell?: string,
args?: string[] | string,
rootURI?: string,
cols?: number,
rows?: number,
env?: { [key: string]: string | null },
strictEnv?: boolean,
isPseudo?: boolean,
}
export const ShellTerminalServerProxy = Symbol('ShellTerminalServerProxy');
export type ShellTerminalServerProxy = RpcProxy<IShellTerminalServer>;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.0/src/vs/workbench/contrib/terminal/common/environmentVariable.ts
export const NO_ROOT_URI = '<none>';
export interface EnvironmentVariableCollection {
readonly variableMutators: ReadonlyMap<string, EnvironmentVariableMutator>;
readonly description: string | MarkdownString | undefined;
}
export interface EnvironmentVariableCollectionWithPersistence extends EnvironmentVariableCollection {
readonly persistent: boolean;
}
export enum EnvironmentVariableMutatorType {
Replace = 1,
Append = 2,
Prepend = 3
}
export interface EnvironmentVariableMutatorOptions {
applyAtProcessCreation?: boolean;
}
export interface EnvironmentVariableMutator {
readonly value: string;
readonly type: EnvironmentVariableMutatorType;
readonly options: EnvironmentVariableMutatorOptions;
}
export interface SerializableEnvironmentVariableCollection {
readonly description: string | MarkdownString | undefined;
readonly mutators: [string, EnvironmentVariableMutator][]
};

View File

@@ -0,0 +1,200 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { expect } from 'chai';
import { OS } from '@theia/core';
import { GeneralShellType, guessShellTypeFromExecutable, WindowsShellType } from './shell-type';
// Save original environment state
const originalIsWindows = OS.backend.isWindows;
// Helper functions to set test environment
function setWindowsEnvironment(): void {
Object.defineProperty(OS.backend, 'isWindows', { value: true });
}
function setUnixEnvironment(): void {
Object.defineProperty(OS.backend, 'isWindows', { value: false });
}
afterEach(() => {
// Restore original OS.backend.isWindows value after each test
Object.defineProperty(OS.backend, 'isWindows', { value: originalIsWindows });
});
describe('shell-type', () => {
describe('guessShellTypeFromExecutable', () => {
it('should return undefined for undefined input', () => {
expect(guessShellTypeFromExecutable(undefined)).to.be.undefined;
});
describe('Windows environment', () => {
beforeEach(() => {
setWindowsEnvironment();
});
it('should detect cmd.exe as Command Prompt', () => {
expect(guessShellTypeFromExecutable('C:/Windows/System32/cmd.exe')).to.equal(WindowsShellType.CommandPrompt);
});
it('should detect relative cmd.exe path as Command Prompt', () => {
expect(guessShellTypeFromExecutable('cmd.exe')).to.equal(WindowsShellType.CommandPrompt);
});
it('should detect bash.exe as Git Bash in Windows', () => {
expect(guessShellTypeFromExecutable('C:/Program Files/Git/bin/bash.exe')).to.equal(WindowsShellType.GitBash);
});
it('should detect wsl.exe as WSL', () => {
expect(guessShellTypeFromExecutable('C:/Windows/System32/wsl.exe')).to.equal(WindowsShellType.Wsl);
});
it('should detect powershell.exe as PowerShell', () => {
expect(guessShellTypeFromExecutable('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe')).to.equal(GeneralShellType.PowerShell);
});
it('should detect pwsh.exe as PowerShell', () => {
expect(guessShellTypeFromExecutable('C:/Program Files/PowerShell/7/pwsh.exe')).to.equal(GeneralShellType.PowerShell);
});
it('should detect pwsh-preview.exe as PowerShell', () => {
expect(guessShellTypeFromExecutable('C:/Program Files/PowerShell/7-preview/pwsh-preview.exe')).to.equal(GeneralShellType.PowerShell);
});
it('should detect python.exe as Python', () => {
expect(guessShellTypeFromExecutable('C:/Python310/python.exe')).to.equal(GeneralShellType.Python);
});
it('should detect py.exe as Python', () => {
expect(guessShellTypeFromExecutable('C:/Windows/py.exe')).to.equal(GeneralShellType.Python);
});
it('should not detect unknown executable', () => {
expect(guessShellTypeFromExecutable('C:/Program Files/SomeApp/unknown.exe')).to.be.undefined;
});
});
describe('Linux environment', () => {
beforeEach(() => {
setUnixEnvironment();
});
it('should detect bash', () => {
expect(guessShellTypeFromExecutable('/bin/bash')).to.equal(GeneralShellType.Bash);
});
it('should detect sh', () => {
expect(guessShellTypeFromExecutable('/bin/sh')).to.equal(GeneralShellType.Sh);
});
it('should detect zsh', () => {
expect(guessShellTypeFromExecutable('/usr/bin/zsh')).to.equal(GeneralShellType.Zsh);
});
it('should detect fish', () => {
expect(guessShellTypeFromExecutable('/usr/bin/fish')).to.equal(GeneralShellType.Fish);
});
it('should detect csh', () => {
expect(guessShellTypeFromExecutable('/bin/csh')).to.equal(GeneralShellType.Csh);
});
it('should detect ksh', () => {
expect(guessShellTypeFromExecutable('/bin/ksh')).to.equal(GeneralShellType.Ksh);
});
it('should detect node', () => {
expect(guessShellTypeFromExecutable('/usr/bin/node')).to.equal(GeneralShellType.Node);
});
it('should detect julia', () => {
expect(guessShellTypeFromExecutable('/usr/local/bin/julia')).to.equal(GeneralShellType.Julia);
});
it('should detect nushell', () => {
expect(guessShellTypeFromExecutable('/usr/bin/nu')).to.equal(GeneralShellType.NuShell);
});
it('should detect pwsh', () => {
expect(guessShellTypeFromExecutable('/usr/bin/pwsh')).to.equal(GeneralShellType.PowerShell);
});
it('should not detect Windows-specific shells', () => {
expect(guessShellTypeFromExecutable('/usr/bin/cmd')).to.not.equal(WindowsShellType.CommandPrompt);
});
it('should not detect unknown executable', () => {
expect(guessShellTypeFromExecutable('/usr/bin/unknown')).to.be.undefined;
});
});
describe('macOS environment', () => {
beforeEach(() => {
setUnixEnvironment(); // macOS is a Unix-based OS
});
it('should detect bash', () => {
expect(guessShellTypeFromExecutable('/bin/bash')).to.equal(GeneralShellType.Bash);
});
it('should detect zsh (macOS default)', () => {
expect(guessShellTypeFromExecutable('/bin/zsh')).to.equal(GeneralShellType.Zsh);
});
it('should detect fish from homebrew', () => {
expect(guessShellTypeFromExecutable('/usr/local/bin/fish')).to.equal(GeneralShellType.Fish);
});
it('should detect python from homebrew', () => {
expect(guessShellTypeFromExecutable('/usr/local/bin/python')).to.equal(GeneralShellType.Python);
});
it('should detect python3', () => {
expect(guessShellTypeFromExecutable('/usr/bin/python3')).to.equal(GeneralShellType.Python);
});
it('should detect node from homebrew', () => {
expect(guessShellTypeFromExecutable('/usr/local/bin/node')).to.equal(GeneralShellType.Node);
});
it('should not detect Windows-specific shells', () => {
expect(guessShellTypeFromExecutable('/usr/bin/cmd')).to.not.equal(WindowsShellType.CommandPrompt);
});
it('should not detect unknown executable', () => {
expect(guessShellTypeFromExecutable('/Applications/Unknown.app/Contents/MacOS/Unknown')).to.be.undefined;
});
});
describe('Edge cases', () => {
it('should handle empty string', () => {
expect(guessShellTypeFromExecutable('')).to.be.undefined;
});
it('should handle executable with spaces in Windows', () => {
setWindowsEnvironment();
expect(guessShellTypeFromExecutable('C:/Program Files/PowerShell/7/pwsh.exe')).to.equal(GeneralShellType.PowerShell);
});
it('should ignore case in Unix paths (which is not standard but handles user input errors)', () => {
setUnixEnvironment();
expect(guessShellTypeFromExecutable('/usr/bin/BASH')).to.be.undefined;
});
});
});
});

View File

@@ -0,0 +1,95 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { OS } from '@theia/core';
import * as path from 'path';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// code copied and modified from https://github.com/microsoft/vscode/blob/1.99.0/src/vs/platform/terminal/common/terminal.ts#L135-L155
export const enum GeneralShellType {
Bash = 'bash',
Csh = 'csh',
Fish = 'fish',
Julia = 'julia',
Ksh = 'ksh',
Node = 'node',
NuShell = 'nu',
PowerShell = 'pwsh',
Python = 'python',
Sh = 'sh',
Zsh = 'zsh',
}
export const enum WindowsShellType {
CommandPrompt = 'cmd',
GitBash = 'gitbash',
Wsl = 'wsl'
}
export type ShellType = GeneralShellType | WindowsShellType;
export const windowShellTypesToRegex: Map<string, RegExp> = new Map([
[WindowsShellType.CommandPrompt, /^cmd$/],
[WindowsShellType.GitBash, /^bash$/],
[WindowsShellType.Wsl, /^wsl$/]
]);
export const shellTypesToRegex: Map<string, RegExp> = new Map([
[GeneralShellType.Bash, /^bash$/],
[GeneralShellType.Csh, /^csh$/],
[GeneralShellType.Fish, /^fish$/],
[GeneralShellType.Julia, /^julia$/],
[GeneralShellType.Ksh, /^ksh$/],
[GeneralShellType.Node, /^node$/],
[GeneralShellType.NuShell, /^nu$/],
[GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/],
[GeneralShellType.Python, /^py(?:thon)?(?:\d+)?$/],
[GeneralShellType.Sh, /^sh$/],
[GeneralShellType.Zsh, /^zsh$/]
]);
export function guessShellTypeFromExecutable(executable: string | undefined): string | undefined {
if (!executable) {
return undefined;
}
if (OS.backend.isWindows) {
const windowsExecutableName = path.basename(executable, '.exe');
for (const [shellType, pattern] of windowShellTypesToRegex) {
if (windowsExecutableName.match(pattern)) {
return shellType;
}
}
// check also for generic ones as python
for (const [shellType, pattern] of shellTypesToRegex) {
if (windowsExecutableName.match(pattern)) {
return shellType;
}
}
}
const executableName = path.basename(executable);
for (const [shellType, pattern] of shellTypesToRegex) {
if (executableName.match(pattern)) {
return shellType;
}
}
return undefined;
}

View File

@@ -0,0 +1,588 @@
// *****************************************************************************
// Copyright (C) 2018 Bitsler and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* eslint-disable max-len */
import { interfaces } from '@theia/core/shared/inversify';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
import { PreferenceService } from '@theia/core/lib/common';
import { createPreferenceProxy, PreferenceProxy } from '@theia/core/lib/common/preferences/preference-proxy';
import { nls } from '@theia/core/lib/common/nls';
import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/common/editor-generated-preference-schema';
import { OS } from '@theia/core';
import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
import { ColorDefaults } from '@theia/core/lib/common/color';
/**
* It should be aligned with https://github.com/microsoft/vscode/blob/0dfa355b3ad185a6289ba28a99c141ab9e72d2be/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts#L40
*/
export const terminalAnsiColorMap: { [key: string]: { index: number, defaults: ColorDefaults } } = {
'terminal.ansiBlack': {
index: 0,
defaults: {
light: '#000000',
dark: '#000000',
hcDark: '#000000',
hcLight: '#292929'
}
},
'terminal.ansiRed': {
index: 1,
defaults: {
light: '#cd3131',
dark: '#cd3131',
hcDark: '#cd0000',
hcLight: '#cd3131'
}
},
'terminal.ansiGreen': {
index: 2,
defaults: {
light: '#00BC00',
dark: '#0DBC79',
hcDark: '#00cd00',
hcLight: '#00bc00'
}
},
'terminal.ansiYellow': {
index: 3,
defaults: {
light: '#949800',
dark: '#e5e510',
hcDark: '#cdcd00',
hcLight: '#949800'
}
},
'terminal.ansiBlue': {
index: 4,
defaults: {
light: '#0451a5',
dark: '#2472c8',
hcDark: '#0000ee',
hcLight: '#0451a5'
}
},
'terminal.ansiMagenta': {
index: 5,
defaults: {
light: '#bc05bc',
dark: '#bc3fbc',
hcDark: '#cd00cd',
hcLight: '#bc05bc'
}
},
'terminal.ansiCyan': {
index: 6,
defaults: {
light: '#0598bc',
dark: '#11a8cd',
hcDark: '#00cdcd',
hcLight: '#0598b'
}
},
'terminal.ansiWhite': {
index: 7,
defaults: {
light: '#555555',
dark: '#e5e5e5',
hcDark: '#e5e5e5',
hcLight: '#555555'
}
},
'terminal.ansiBrightBlack': {
index: 8,
defaults: {
light: '#666666',
dark: '#666666',
hcDark: '#7f7f7f',
hcLight: '#666666'
}
},
'terminal.ansiBrightRed': {
index: 9,
defaults: {
light: '#cd3131',
dark: '#f14c4c',
hcDark: '#ff0000',
hcLight: '#cd3131'
}
},
'terminal.ansiBrightGreen': {
index: 10,
defaults: {
light: '#14CE14',
dark: '#23d18b',
hcDark: '#00ff00',
hcLight: '#00bc00'
}
},
'terminal.ansiBrightYellow': {
index: 11,
defaults: {
light: '#b5ba00',
dark: '#f5f543',
hcDark: '#ffff00',
hcLight: '#b5ba00'
}
},
'terminal.ansiBrightBlue': {
index: 12,
defaults: {
light: '#0451a5',
dark: '#3b8eea',
hcDark: '#5c5cff',
hcLight: '#0451a5'
}
},
'terminal.ansiBrightMagenta': {
index: 13,
defaults: {
light: '#bc05bc',
dark: '#d670d6',
hcDark: '#ff00ff',
hcLight: '#bc05bc'
}
},
'terminal.ansiBrightCyan': {
index: 14,
defaults: {
light: '#0598bc',
dark: '#29b8db',
hcDark: '#00ffff',
hcLight: '#0598bc'
}
},
'terminal.ansiBrightWhite': {
index: 15,
defaults: {
light: '#a5a5a5',
dark: '#e5e5e5',
hcDark: '#ffffff',
hcLight: '#a5a5a5'
}
}
};
const commonProfileProperties: PreferenceSchema['properties'] = {
env: {
type: 'object',
additionalProperties: {
type: 'string'
},
markdownDescription: nls.localizeByDefault('An object with environment variables that will be added to the terminal profile process. Set to `null` to delete environment variables from the base environment.'),
},
overrideName: {
type: 'boolean',
description: nls.localizeByDefault('Whether or not to replace the dynamic terminal title that detects what program is running with the static profile name.')
},
icon: {
type: 'string',
markdownDescription: nls.localize('theia/terminal/profileIcon', 'A codicon ID to associate with the terminal icon.\nterminal-tmux:"$(terminal-tmux)"')
},
color: {
type: 'string',
enum: Object.getOwnPropertyNames(terminalAnsiColorMap),
description: nls.localize('theia/terminal/profileColor', 'A terminal theme color ID to associate with the terminal.')
}
};
const stringOrStringArray: IJSONSchema = {
oneOf: [
{ type: 'string' },
{
type: 'array',
items: {
type: 'string'
}
}
]
};
const pathProperty: IJSONSchema = {
description: nls.localize('theia/terminal/profilePath', 'The path of the shell that this profile uses.'),
...stringOrStringArray
};
function shellArgsDeprecatedMessage(type: OS.Type): string {
return nls.localize('theia/terminal/shell.deprecated', 'This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in \'terminal.integrated.profiles.{0}\' and setting its profile name as the default in \'terminal.integrated.defaultProfile.{0}.\'', type.toString().toLowerCase());
}
export const TerminalConfigSchema: PreferenceSchema = {
properties: {
'terminal.enableCopy': {
type: 'boolean',
description: nls.localize('theia/terminal/enableCopy', 'Enable ctrl-c (cmd-c on macOS) to copy selected text'),
default: true
},
'terminal.enablePaste': {
type: 'boolean',
description: nls.localize('theia/terminal/enablePaste', 'Enable ctrl-v (cmd-v on macOS) to paste from clipboard'),
default: true
},
'terminal.integrated.fontFamily': {
type: 'string',
markdownDescription: nls.localizeByDefault('Controls the font family of the terminal. Defaults to {0}\'s value.', '`#editor.fontFamily#`'),
default: editorGeneratedPreferenceProperties['editor.fontFamily'].default,
},
'terminal.integrated.fontSize': {
type: 'number',
description: nls.localizeByDefault('Controls the font size in pixels of the terminal.'),
minimum: 6,
default: editorGeneratedPreferenceProperties['editor.fontSize'].default
},
'terminal.integrated.fontWeight': {
type: 'string',
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
description: nls.localizeByDefault('The font weight to use within the terminal for non-bold text. Accepts \"normal\" and \"bold\" keywords or numbers between 1 and 1000.'),
default: 'normal'
},
'terminal.integrated.fontWeightBold': {
type: 'string',
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
description: nls.localizeByDefault('The font weight to use within the terminal for bold text. Accepts \"normal\" and \"bold\" keywords or numbers between 1 and 1000.'),
default: 'bold'
},
'terminal.integrated.drawBoldTextInBrightColors': {
description: nls.localizeByDefault('Controls whether bold text in the terminal will always use the \"bright\" ANSI color variant.'),
type: 'boolean',
default: true,
},
'terminal.integrated.letterSpacing': {
description: nls.localizeByDefault('Controls the letter spacing of the terminal. This is an integer value which represents the number of additional pixels to add between characters.'),
type: 'number',
default: 1
},
'terminal.integrated.lineHeight': {
description: nls.localizeByDefault('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',
minimum: 1,
default: 1
},
'terminal.integrated.scrollback': {
description: nls.localizeByDefault('Controls the maximum number of lines the terminal keeps in its buffer. We pre-allocate memory based on this value in order to ensure a smooth experience. As such, as the value increases, so will the amount of memory.'),
type: 'number',
default: 1000
},
'terminal.integrated.fastScrollSensitivity': {
markdownDescription: nls.localizeByDefault('Scrolling speed multiplier when pressing `Alt`.'),
type: 'number',
default: 5,
},
'terminal.integrated.rendererType': {
description: nls.localize('theia/terminal/rendererType', 'Controls how the terminal is rendered.'),
type: 'string',
enum: ['canvas', 'dom'],
default: 'canvas',
deprecationMessage: nls.localize('theia/terminal/rendererTypeDeprecationMessage', 'The renderer type is no longer supported as an option.')
},
'terminal.integrated.copyOnSelection': {
description: nls.localizeByDefault('Controls whether text selected in the terminal will be copied to the clipboard.'),
type: 'boolean',
default: false,
},
'terminal.integrated.cursorBlinking': {
description: nls.localizeByDefault('Controls whether the terminal cursor blinks.'),
type: 'boolean',
default: false
},
'terminal.integrated.cursorStyle': {
description: nls.localizeByDefault('Controls the style of terminal cursor when the terminal is focused.'),
enum: ['block', 'underline', 'line'],
default: 'block'
},
'terminal.integrated.cursorWidth': {
markdownDescription: nls.localizeByDefault('Controls the width of the cursor when {0} is set to {1}.', '`#terminal.integrated.cursorStyle#`', '`line`'),
type: 'number',
default: 1
},
'terminal.integrated.shell.windows': {
type: ['string', 'null'],
typeDetails: { isFilepath: true },
markdownDescription: nls.localize('theia/terminal/shellWindows', 'The path of the shell that the terminal uses on Windows. (default: \'{0}\').', 'C:\\Windows\\System32\\cmd.exe'),
default: undefined,
deprecationMessage: shellArgsDeprecatedMessage(OS.Type.Windows),
},
'terminal.integrated.shell.osx': {
type: ['string', 'null'],
markdownDescription: nls.localize('theia/terminal/shellOsx', 'The path of the shell that the terminal uses on macOS (default: \'{0}\'}).', '/bin/bash'),
default: undefined,
deprecationMessage: shellArgsDeprecatedMessage(OS.Type.OSX),
},
'terminal.integrated.shell.linux': {
type: ['string', 'null'],
markdownDescription: nls.localize('theia/terminal/shellLinux', 'The path of the shell that the terminal uses on Linux (default: \'{0}\'}).', '/bin/bash'),
default: undefined,
deprecationMessage: shellArgsDeprecatedMessage(OS.Type.Linux),
},
'terminal.integrated.shellArgs.windows': {
type: 'array',
markdownDescription: nls.localize('theia/terminal/shellArgsWindows', 'The command line arguments to use when on the Windows terminal.'),
default: [],
deprecationMessage: shellArgsDeprecatedMessage(OS.Type.Windows),
},
'terminal.integrated.shellArgs.osx': {
type: 'array',
markdownDescription: nls.localize('theia/terminal/shellArgsOsx', 'The command line arguments to use when on the macOS terminal.'),
default: [
'-l'
],
deprecationMessage: shellArgsDeprecatedMessage(OS.Type.OSX),
},
'terminal.integrated.shellArgs.linux': {
type: 'array',
markdownDescription: nls.localize('theia/terminal/shellArgsLinux', 'The command line arguments to use when on the Linux terminal.'),
default: [],
deprecationMessage: shellArgsDeprecatedMessage(OS.Type.Linux),
},
// TODO: This preference currently features no implementation but is only available for plugins to use.
'terminal.integrated.commandsToSkipShell': {
type: 'array',
markdownDescription: nls.localizeByDefault('A set of command IDs whose keybindings will not be sent to the shell but instead always be handled by VS Code. This allows keybindings that would normally be consumed by the shell to act instead the same as when the terminal is not focused, for example `Ctrl+P` to launch Quick Open.\n\n&nbsp;\n\nMany commands are skipped by default. To override a default and pass that command\'s keybinding to the shell instead, add the command prefixed with the `-` character. For example add `-workbench.action.quickOpen` to allow `Ctrl+P` to reach the shell.\n\n&nbsp;\n\nThe following list of default skipped commands is truncated when viewed in Settings Editor. To see the full list, {1} and search for the first command from the list below.\n\n&nbsp;\n\nDefault Skipped Commands:\n\n{0}'),
items: {
type: 'string'
},
default: []
},
'terminal.integrated.confirmOnExit': {
type: 'string',
description: nls.localizeByDefault('Controls whether to confirm when the window closes if there are active terminal sessions. Background terminals like those launched by some extensions will not trigger the confirmation.'),
enum: ['never', 'always', 'hasChildProcesses'],
enumDescriptions: [
nls.localizeByDefault('Never confirm.'),
nls.localizeByDefault('Always confirm if there are terminals.'),
nls.localizeByDefault('Confirm if there are any terminals that have child processes.'),
],
default: 'never'
},
'terminal.integrated.enablePersistentSessions': {
type: 'boolean',
description: nls.localizeByDefault('Persist terminal sessions/history for the workspace across window reloads.'),
default: true
},
'terminal.integrated.defaultProfile.windows': {
type: 'string',
description: nls.localize('theia/terminal/defaultProfile', 'The default profile used on {0}', OS.Type.Windows.toString())
},
'terminal.integrated.defaultProfile.linux': {
type: 'string',
description: nls.localize('theia/terminal/defaultProfile', 'The default profile used on {0}', OS.Type.Linux.toString())
},
'terminal.integrated.defaultProfile.osx': {
type: 'string',
description: nls.localize('theia/terminal/defaultProfile', 'The default profile used on {0}', OS.Type.OSX.toString())
},
'terminal.integrated.profiles.windows': {
markdownDescription: nls.localize('theia/terminal/profiles', 'The profiles to present when creating a new terminal. Set the path property manually with optional args.\nSet an existing profile to `null` to hide the profile from the list, for example: `"{0}": null`.', 'cmd'),
anyOf: [
{
type: 'object',
properties: {
},
additionalProperties: {
oneOf: [{
type: 'object',
additionalProperties: false,
properties: {
path: pathProperty,
args: {
...stringOrStringArray,
description: nls.localize('theia/terminal/profileArgs', 'The shell arguments that this profile uses.'),
},
...commonProfileProperties
},
required: ['path']
},
{
type: 'object',
additionalProperties: false,
properties: {
source: {
type: 'string',
description: nls.localizeByDefault('A profile source that will auto detect the paths to the shell. Note that non-standard executable locations are not supported and must be created manually in a new profile.')
},
args: {
...stringOrStringArray,
description: nls.localize('theia/terminal/profileArgs', 'The shell arguments that this profile uses.'),
},
...commonProfileProperties
},
required: ['source'],
default: {
path: 'C:\\Windows\\System32\\cmd.exe'
}
}, {
type: 'null'
}]
},
default: {
cmd: {
path: 'C:\\Windows\\System32\\cmd.exe'
}
}
},
{ type: 'null' }
]
},
'terminal.integrated.profiles.linux': {
markdownDescription: nls.localize('theia/terminal/profiles', 'The profiles to present when creating a new terminal. Set the path property manually with optional args.\nSet an existing profile to `null` to hide the profile from the list, for example: `"{0}": null`.', 'bash'),
anyOf: [{
type: 'object',
properties: {
},
additionalProperties: {
oneOf: [
{
type: 'object',
properties: {
path: pathProperty,
args: {
type: 'array',
items: { type: 'string' },
description: nls.localize('theia/terminal/profileArgs', 'The shell arguments that this profile uses.'),
},
...commonProfileProperties
},
required: ['path'],
additionalProperties: false,
},
{ type: 'null' }
]
},
default: {
path: '${env:SHELL}',
args: ['-l']
}
},
{ type: 'null' }
]
},
'terminal.integrated.profiles.osx': {
markdownDescription: nls.localize('theia/terminal/profiles', 'The profiles to present when creating a new terminal. Set the path property manually with optional args.\nSet an existing profile to `null` to hide the profile from the list, for example: `"{0}": null`.', 'zsh'),
anyOf: [{
type: 'object',
properties: {
},
additionalProperties: {
oneOf: [
{
type: 'object',
properties: {
path: pathProperty,
args: {
type: 'array',
items: { type: 'string' },
description: nls.localize('theia/terminal/profileArgs', 'The shell arguments that this profile uses.'),
},
...commonProfileProperties
},
required: ['path'],
additionalProperties: false,
},
{ type: 'null' }
]
},
default: {
path: '${env:SHELL}',
args: ['-l']
}
},
{ type: 'null' }
]
},
}
};
export type Profiles = null | {
[key: string]: {
path?: string | string[],
source?: string,
args?: string | string[],
env?: { [key: string]: string },
overrideName?: boolean;
icon?: string,
color?: string
}
};
export interface TerminalConfiguration {
'terminal.enableCopy': boolean
'terminal.enablePaste': boolean
// xterm compatible, see https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/
'terminal.integrated.fontFamily': string
'terminal.integrated.fontSize': number
'terminal.integrated.fontWeight': FontWeight
'terminal.integrated.fontWeightBold': FontWeight,
'terminal.integrated.drawBoldTextInBrightColors': boolean,
'terminal.integrated.letterSpacing': number
'terminal.integrated.lineHeight': number,
'terminal.integrated.scrollback': number,
'terminal.integrated.fastScrollSensitivity': number,
'terminal.integrated.rendererType': TerminalRendererType,
'terminal.integrated.copyOnSelection': boolean,
'terminal.integrated.cursorBlinking': boolean,
'terminal.integrated.cursorStyle': CursorStyleVSCode,
'terminal.integrated.cursorWidth': number,
'terminal.integrated.shell.windows': string | null | undefined,
'terminal.integrated.shell.osx': string | null | undefined,
'terminal.integrated.shell.linux': string | null | undefined,
'terminal.integrated.shellArgs.windows': string[],
'terminal.integrated.shellArgs.osx': string[],
'terminal.integrated.shellArgs.linux': string[],
'terminal.integrated.defaultProfile.windows': string,
'terminal.integrated.defaultProfile.linux': string,
'terminal.integrated.defaultProfile.osx': string,
'terminal.integrated.profiles.windows': Profiles
'terminal.integrated.profiles.linux': Profiles,
'terminal.integrated.profiles.osx': Profiles,
'terminal.integrated.confirmOnExit': ConfirmOnExitType
'terminal.integrated.enablePersistentSessions': boolean
}
type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
export type CursorStyle = 'block' | 'underline' | 'bar';
// VS Code uses 'line' to represent 'bar'. The following conversion is necessary to support their preferences.
export type CursorStyleVSCode = CursorStyle | 'line';
export type TerminalRendererType = 'canvas' | 'dom';
export type ConfirmOnExitType = 'never' | 'always' | 'hasChildProcesses';
export const DEFAULT_TERMINAL_RENDERER_TYPE = 'canvas';
export function isTerminalRendererType(arg: unknown): arg is TerminalRendererType {
return typeof arg === 'string' && (arg === 'canvas' || arg === 'dom');
}
export const TerminalPreferenceContribution = Symbol('TerminalPreferenceContribution');
export const TerminalPreferences = Symbol('TerminalPreferences');
export type TerminalPreferences = PreferenceProxy<TerminalConfiguration>;
export function createTerminalPreferences(preferences: PreferenceService, schema: PreferenceSchema = TerminalConfigSchema): TerminalPreferences {
return createPreferenceProxy(preferences, schema);
}
export function bindTerminalPreferences(bind: interfaces.Bind): void {
bind(TerminalPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(TerminalPreferenceContribution);
return createTerminalPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(TerminalPreferenceContribution).toConstantValue({ schema: TerminalConfigSchema });
bind(PreferenceContribution).toService(TerminalPreferenceContribution);
}

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { IBaseTerminalServer, IBaseTerminalServerOptions } from './base-terminal-protocol';
export const ITerminalServer = Symbol('ITerminalServer');
export const terminalPath = '/services/terminal';
export const terminalsPath = '/services/terminals';
export interface ITerminalServer extends IBaseTerminalServer {
create(ITerminalServerOptions: object): Promise<number>;
}
export interface ITerminalServerOptions extends IBaseTerminalServerOptions {
command: string,
args?: string[],
options?: object
}

View File

@@ -0,0 +1,69 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import {
IBaseTerminalClient,
IBaseTerminalExitEvent,
IBaseTerminalErrorEvent
} from './base-terminal-protocol';
@injectable()
export class TerminalWatcher {
getTerminalClient(): IBaseTerminalClient {
const exitEmitter = this.onTerminalExitEmitter;
const errorEmitter = this.onTerminalErrorEmitter;
const storeTerminalEnvVariablesEmitter = this.onStoreTerminalEnvVariablesRequestedEmitter;
const updateTerminalEnvVariablesEmitter = this.onUpdateTerminalEnvVariablesRequestedEmitter;
return {
storeTerminalEnvVariables(data: string): void {
storeTerminalEnvVariablesEmitter.fire(data);
},
updateTerminalEnvVariables(): void {
updateTerminalEnvVariablesEmitter.fire(undefined);
},
onTerminalExitChanged(event: IBaseTerminalExitEvent): void {
exitEmitter.fire(event);
},
onTerminalError(event: IBaseTerminalErrorEvent): void {
errorEmitter.fire(event);
}
};
}
private onTerminalExitEmitter = new Emitter<IBaseTerminalExitEvent>();
private onTerminalErrorEmitter = new Emitter<IBaseTerminalErrorEvent>();
private onStoreTerminalEnvVariablesRequestedEmitter = new Emitter<string>();
private onUpdateTerminalEnvVariablesRequestedEmitter = new Emitter<undefined>();
get onTerminalExit(): Event<IBaseTerminalExitEvent> {
return this.onTerminalExitEmitter.event;
}
get onTerminalError(): Event<IBaseTerminalErrorEvent> {
return this.onTerminalErrorEmitter.event;
}
get onStoreTerminalEnvVariablesRequested(): Event<string> {
return this.onStoreTerminalEnvVariablesRequestedEmitter.event;
}
get onUpdateTerminalEnvVariablesRequested(): Event<undefined> {
return this.onUpdateTerminalEnvVariablesRequestedEmitter.event;
}
}

View File

@@ -0,0 +1,173 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { ILogger, DisposableCollection } from '@theia/core/lib/common';
import {
IBaseTerminalServer,
IBaseTerminalServerOptions,
IBaseTerminalClient,
TerminalProcessInfo,
TerminalExitReason
} from '../common/base-terminal-protocol';
import { TerminalProcess, ProcessManager, TaskTerminalProcess } from '@theia/process/lib/node';
import { ShellProcess } from './shell-process';
@injectable()
export abstract class BaseTerminalServer implements IBaseTerminalServer {
protected client: IBaseTerminalClient | undefined = undefined;
protected terminalToDispose = new Map<number, DisposableCollection>();
constructor(
@inject(ProcessManager) protected readonly processManager: ProcessManager,
@inject(ILogger) @named('terminal') protected readonly logger: ILogger
) {
processManager.onDelete(id => {
const toDispose = this.terminalToDispose.get(id);
if (toDispose !== undefined) {
toDispose.dispose();
this.terminalToDispose.delete(id);
}
});
}
abstract create(options: IBaseTerminalServerOptions): Promise<number>;
async attach(id: number): Promise<number> {
const term = this.processManager.get(id);
if (term && term instanceof TerminalProcess) {
return term.id;
} else {
this.logger.warn(`Couldn't attach - can't find terminal with id: ${id} `);
return -1;
}
}
async onAttachAttempted(id: number): Promise<void> {
const terminal = this.processManager.get(id);
if (terminal instanceof TaskTerminalProcess) {
terminal.attachmentAttempted = true;
if (terminal.exited) {
// Didn't execute `unregisterProcess` on terminal `exit` event to enable attaching task output to terminal,
// Fixes https://github.com/eclipse-theia/theia/issues/2961
terminal.unregisterProcess();
} else {
this.postAttachAttempted(terminal);
}
}
}
async getProcessId(id: number): Promise<number> {
const terminal = this.processManager.get(id);
if (!(terminal instanceof TerminalProcess)) {
throw new Error(`terminal "${id}" does not exist`);
}
return terminal.pid;
}
async getProcessInfo(id: number): Promise<TerminalProcessInfo> {
const terminal = this.processManager.get(id);
if (!(terminal instanceof TerminalProcess)) {
throw new Error(`terminal "${id}" does not exist`);
}
return {
executable: terminal.executable,
arguments: terminal.arguments,
};
}
async getCwdURI(id: number): Promise<string> {
const terminal = this.processManager.get(id);
if (!(terminal instanceof TerminalProcess)) {
throw new Error(`terminal "${id}" does not exist`);
}
return terminal.getCwdURI();
}
async close(id: number): Promise<void> {
const term = this.processManager.get(id);
if (term instanceof TerminalProcess) {
term.kill();
}
}
async getDefaultShell(): Promise<string> {
return ShellProcess.getShellExecutablePath();
}
dispose(): void {
// noop
}
async resize(id: number, cols: number, rows: number): Promise<void> {
const term = this.processManager.get(id);
if (term && term instanceof TerminalProcess) {
term.resize(cols, rows);
} else {
console.warn("Couldn't resize terminal " + id + ", because it doesn't exist.");
}
}
/* Set the client to receive notifications on. */
setClient(client: IBaseTerminalClient | undefined): void {
this.client = client;
if (!this.client) {
return;
}
this.client.updateTerminalEnvVariables();
}
protected notifyClientOnExit(term: TerminalProcess): DisposableCollection {
const toDispose = new DisposableCollection();
toDispose.push(term.onError(error => {
this.logger.error(`Terminal pid: ${term.pid} error: ${error}, closing it.`);
if (this.client !== undefined) {
this.client.onTerminalError({
terminalId: term.id,
error: new Error(`Failed to execute terminal process (${error.code})`),
attached: term instanceof TaskTerminalProcess && term.attachmentAttempted
});
}
}));
toDispose.push(term.onExit(event => {
if (this.client !== undefined) {
this.client.onTerminalExitChanged({
terminalId: term.id,
code: event.code,
reason: TerminalExitReason.Process,
signal: event.signal,
attached: term instanceof TaskTerminalProcess && term.attachmentAttempted
});
}
}));
return toDispose;
}
protected postCreate(term: TerminalProcess): void {
const toDispose = this.notifyClientOnExit(term);
this.terminalToDispose.set(term.id, toDispose);
}
protected postAttachAttempted(term: TaskTerminalProcess): void {
const toDispose = this.notifyClientOnExit(term);
this.terminalToDispose.set(term.id, toDispose);
}
}

View File

@@ -0,0 +1,46 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { wait } from '@theia/core/lib/common/promise-util';
import { expect } from 'chai';
import { BufferBufferingStream } from './buffering-stream';
describe('BufferringStream', () => {
it('should emit whatever data was buffered before the timeout', async () => {
const buffer = new BufferBufferingStream({ emitInterval: 1000 });
const chunkPromise = waitForChunk(buffer);
buffer.push(Buffer.from([0]));
await wait(100);
buffer.push(Buffer.from([1]));
await wait(100);
buffer.push(Buffer.from([2, 3, 4]));
const chunk = await chunkPromise;
expect(chunk).deep.eq(Buffer.from([0, 1, 2, 3, 4]));
});
it('should not emit chunks bigger than maxChunkSize', async () => {
const buffer = new BufferBufferingStream({ maxChunkSize: 2 });
buffer.push(Buffer.from([0, 1, 2, 3, 4, 5]));
expect(await waitForChunk(buffer)).deep.eq(Buffer.from([0, 1]));
expect(await waitForChunk(buffer)).deep.eq(Buffer.from([2, 3]));
expect(await waitForChunk(buffer)).deep.eq(Buffer.from([4, 5]));
});
function waitForChunk(buffer: BufferBufferingStream): Promise<Buffer> {
return new Promise(resolve => buffer.onData(resolve));
}
});

View File

@@ -0,0 +1,95 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Emitter, Event } from '@theia/core/lib/common/event';
export interface BufferingStreamOptions {
/**
* Max size in bytes of the chunks being emitted.
*/
maxChunkSize?: number
/**
* Amount of time in milliseconds to wait between the moment we start
* buffering data and when we emit the buffered chunk.
*/
emitInterval?: number
}
/**
* This component will buffer whatever is pushed to it and emit chunks back
* every {@link BufferingStreamOptions.emitInterval}. It will also ensure that
* the emitted chunks never exceed {@link BufferingStreamOptions.maxChunkSize}.
*/
export class BufferingStream<T> {
protected buffer?: T;
protected timeout?: NodeJS.Timeout;
protected maxChunkSize: number;
protected emitInterval: number;
protected onDataEmitter = new Emitter<T>();
protected readonly concat: (left: T, right: T) => T;
protected readonly slice: (what: T, start?: number, end?: number) => T;
protected readonly length: (what: T) => number;
constructor(options: BufferingStreamOptions = {}, concat: (left: T, right: T) => T, slice: (what: T, start?: number, end?: number) => T, length: (what: T) => number) {
this.emitInterval = options.emitInterval ?? 16; // ms
this.maxChunkSize = options.maxChunkSize ?? (256 * 1024); // bytes
this.concat = concat;
this.slice = slice;
this.length = length;
}
get onData(): Event<T> {
return this.onDataEmitter.event;
}
push(chunk: T): void {
if (this.buffer) {
this.buffer = this.concat(this.buffer, chunk);
} else {
this.buffer = chunk;
this.timeout = setTimeout(() => this.emitBufferedChunk(), this.emitInterval);
}
}
dispose(): void {
clearTimeout(this.timeout);
this.buffer = undefined;
this.onDataEmitter.dispose();
}
protected emitBufferedChunk(): void {
this.onDataEmitter.fire(this.slice(this.buffer!, 0, this.maxChunkSize));
if (this.length(this.buffer!) <= this.maxChunkSize) {
this.buffer = undefined;
} else {
this.buffer = this.slice(this.buffer!, this.maxChunkSize);
this.timeout = setTimeout(() => this.emitBufferedChunk(), this.emitInterval);
}
}
}
export class StringBufferingStream extends BufferingStream<string> {
constructor(options: BufferingStreamOptions = {}) {
super(options, (left, right) => left.concat(right), (what, start, end) => what.slice(start, end), what => what.length);
}
}
export class BufferBufferingStream extends BufferingStream<Buffer> {
constructor(options: BufferingStreamOptions = {}) {
super(options, (left, right) => Buffer.concat([left, right]), (what, start, end) => what.slice(start, end), what => what.length);
}
}

View File

@@ -0,0 +1,17 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './terminal-backend-module';

View File

@@ -0,0 +1,102 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, named } from '@theia/core/shared/inversify';
import * as os from 'os';
import { ILogger } from '@theia/core/lib/common/logger';
import { TerminalProcess, TerminalProcessOptions, ProcessManager, MultiRingBuffer } from '@theia/process/lib/node';
import { isWindows, isOSX } from '@theia/core/lib/common';
import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { EnvironmentUtils } from '@theia/core/lib/node/environment-utils';
import { parseArgs } from '@theia/process/lib/node/utils';
export const ShellProcessFactory = Symbol('ShellProcessFactory');
export type ShellProcessFactory = (options: ShellProcessOptions) => ShellProcess;
export const ShellProcessOptions = Symbol('ShellProcessOptions');
export interface ShellProcessOptions {
shell?: string,
args?: string[] | string,
rootURI?: string,
cols?: number,
rows?: number,
env?: { [key: string]: string | null },
strictEnv?: boolean,
isPseudo?: boolean,
}
export function getRootPath(rootURI?: string): string {
if (rootURI) {
const uri = new URI(rootURI);
return FileUri.fsPath(uri);
} else {
return os.homedir();
}
}
@injectable()
export class ShellProcess extends TerminalProcess {
protected static defaultCols = 80;
protected static defaultRows = 24;
constructor( // eslint-disable-next-line @typescript-eslint/indent
@inject(ShellProcessOptions) options: ShellProcessOptions,
@inject(ProcessManager) processManager: ProcessManager,
@inject(MultiRingBuffer) ringBuffer: MultiRingBuffer,
@inject(ILogger) @named('terminal') logger: ILogger,
@inject(EnvironmentUtils) environmentUtils: EnvironmentUtils,
) {
const env = { 'COLORTERM': 'truecolor' };
super(<TerminalProcessOptions>{
command: options.shell || ShellProcess.getShellExecutablePath(),
args: options.args || ShellProcess.getShellExecutableArgs(),
options: {
name: 'xterm-256color',
cols: options.cols || ShellProcess.defaultCols,
rows: options.rows || ShellProcess.defaultRows,
cwd: getRootPath(options.rootURI),
env: options.strictEnv !== true ? Object.assign(env, environmentUtils.mergeProcessEnv(options.env)) : Object.assign(env, options.env),
},
isPseudo: options.isPseudo,
}, processManager, ringBuffer, logger);
}
public static getShellExecutablePath(): string {
const shell = process.env.THEIA_SHELL;
if (shell) {
return shell;
}
if (isWindows) {
return 'cmd.exe';
} else {
return process.env.SHELL!;
}
}
public static getShellExecutableArgs(): string[] {
const args = process.env.THEIA_SHELL_ARGS;
if (args) {
return parseArgs(args);
}
if (isOSX) {
return ['-l'];
} else {
return [];
}
}
}

View File

@@ -0,0 +1,40 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as chai from 'chai';
import { createTerminalTestContainer } from './test/terminal-test-container';
import { IShellTerminalServer } from '../common/shell-terminal-protocol';
/**
* Globals
*/
const expect = chai.expect;
describe('ShellServer', function (): void {
this.timeout(5000);
let shellTerminalServer: IShellTerminalServer;
beforeEach(() => {
shellTerminalServer = createTerminalTestContainer().get(IShellTerminalServer);
});
it('test shell terminal create', async function (): Promise<void> {
const createResult = shellTerminalServer.create({});
expect(await createResult).to.be.greaterThan(-1);
});
});

View File

@@ -0,0 +1,245 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { exec } from 'child_process';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { EnvironmentUtils } from '@theia/core/lib/node/environment-utils';
import { BaseTerminalServer } from './base-terminal-server';
import { ShellProcessFactory, getRootPath } from './shell-process';
import { ProcessManager, TerminalProcess } from '@theia/process/lib/node';
import { isWindows } from '@theia/core/lib/common/os';
import * as cp from 'child_process';
import {
EnvironmentVariableCollectionWithPersistence, EnvironmentVariableMutatorType, NO_ROOT_URI, SerializableEnvironmentVariableCollection,
IShellTerminalServer, IShellTerminalServerOptions
}
from '../common/shell-terminal-protocol';
import { URI } from '@theia/core';
import { MultiKeyMap } from '@theia/core/lib/common/collections';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
interface SerializedExtensionEnvironmentVariableCollection {
extensionIdentifier: string,
rootUri: string,
collection: SerializableEnvironmentVariableCollection,
}
interface WindowsProcess {
ProcessId: number;
ParentProcessId: number;
}
@injectable()
export class ShellTerminalServer extends BaseTerminalServer implements IShellTerminalServer {
@inject(EnvironmentUtils) protected environmentUtils: EnvironmentUtils;
readonly collections: MultiKeyMap<string, EnvironmentVariableCollectionWithPersistence> = new MultiKeyMap(2);
constructor(
@inject(ShellProcessFactory) protected readonly shellFactory: ShellProcessFactory,
@inject(ProcessManager) processManager: ProcessManager,
@inject(ILogger) @named('terminal') logger: ILogger) {
super(processManager, logger);
}
async create(options: IShellTerminalServerOptions): Promise<number> {
try {
if (options.strictEnv !== true) {
options.env = this.environmentUtils.mergeProcessEnv(options.env);
this.applyToProcessEnvironment(URI.fromFilePath(getRootPath(options.rootURI)), options.env);
}
const term = this.shellFactory(options);
this.postCreate(term);
return term.id;
} catch (error) {
this.logger.error('Error while creating terminal', error);
return -1;
}
}
// copied and modified from https://github.com/microsoft/vscode/blob/4636be2b71c87bfb0bfe3c94278b447a5efcc1f1/src/vs/workbench/contrib/debug/node/terminals.ts#L32-L75
private spawnAsPromised(command: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
let stdout = '';
const child = cp.spawn(command, args, {
shell: true
});
if (child.pid) {
child.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});
}
child.on('error', err => {
reject(err);
});
child.on('close', code => {
resolve(stdout);
});
});
}
public hasChildProcesses(processId: number | undefined): Promise<boolean> {
if (processId) {
// if shell has at least one child process, assume that shell is busy
if (isWindows) {
return new Promise(resolve => {
exec(
'powershell -Command "Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId | ConvertTo-Json"',
(error, stdout) => {
if (error) {
this.logger.error(`Failed to get Windows process list: ${error}`);
return resolve(true); // assume busy on error
}
try {
const processes: WindowsProcess[] = JSON.parse(stdout);
const hasChild = processes.some(proc => proc.ParentProcessId === processId);
return resolve(hasChild);
} catch (parseError) {
this.logger.error(`Failed to parse process list JSON: ${parseError}`);
return resolve(true); // assume busy on parse error
}
},
);
});
} else {
return this.spawnAsPromised('/usr/bin/pgrep', ['-lP', String(processId)]).then(stdout => {
const r = stdout.trim();
if (r.length === 0 || r.indexOf(' tmux') >= 0) { // ignore 'tmux';
return false;
} else {
return true;
}
}, error => true);
}
}
// fall back to safe side
return Promise.resolve(true);
}
applyToProcessEnvironment(cwdUri: URI, env: { [key: string]: string | null }): void {
let lowerToActualVariableNames: {
[lowerKey: string]: string | undefined
} | undefined;
if (isWindows) {
lowerToActualVariableNames = {};
Object.keys(env).forEach(e => lowerToActualVariableNames![e.toLowerCase()] = e);
}
this.collections.forEach((mutators, [extensionIdentifier, rootUri]) => {
if (rootUri === NO_ROOT_URI || this.matchesRootUri(cwdUri, rootUri)) {
mutators.variableMutators.forEach((mutator, variable) => {
const actualVariable = isWindows ? lowerToActualVariableNames![variable.toLowerCase()] || variable : variable;
switch (mutator.type) {
case EnvironmentVariableMutatorType.Append:
env[actualVariable] = (env[actualVariable] || '') + mutator.value;
break;
case EnvironmentVariableMutatorType.Prepend:
env[actualVariable] = mutator.value + (env[actualVariable] || '');
break;
case EnvironmentVariableMutatorType.Replace:
env[actualVariable] = mutator.value;
break;
}
});
}
});
}
matchesRootUri(cwdUri: URI, rootUri: string): boolean {
return new URI(rootUri).isEqualOrParent(cwdUri);
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.0/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts
setCollection(extensionIdentifier: string, baseUri: string, persistent: boolean,
collection: SerializableEnvironmentVariableCollection): void {
this.doSetCollection(extensionIdentifier, baseUri, persistent, collection);
this.updateCollections();
}
private doSetCollection(extensionIdentifier: string, baseUri: string, persistent: boolean,
collection: SerializableEnvironmentVariableCollection): void {
this.collections.set([extensionIdentifier, baseUri], {
persistent: persistent,
description: collection.description,
variableMutators: new Map(collection.mutators)
});
}
restorePersisted(jsonValue: string): void {
const collectionsJson: SerializedExtensionEnvironmentVariableCollection[] = JSON.parse(jsonValue);
collectionsJson.forEach(c => this.doSetCollection(c.extensionIdentifier, c.rootUri ?? NO_ROOT_URI, true, c.collection));
}
deleteCollection(extensionIdentifier: string): void {
this.collections.delete([extensionIdentifier]);
this.updateCollections();
}
private updateCollections(): void {
this.persistCollections();
}
protected persistCollections(): void {
const collectionsJson: SerializedExtensionEnvironmentVariableCollection[] = [];
this.collections.forEach((collection, [extensionIdentifier, rootUri]) => {
if (collection.persistent) {
collectionsJson.push({
extensionIdentifier,
rootUri,
collection: {
description: collection.description,
mutators: [...this.collections.get([extensionIdentifier, rootUri])!.variableMutators.entries()]
},
});
}
});
if (this.client) {
const stringifiedJson = JSON.stringify(collectionsJson);
this.client.storeTerminalEnvVariables(stringifiedJson);
}
}
async getEnvVarCollectionDescriptionsByExtension(id: number): Promise<Map<string, (string | MarkdownString | undefined)[]>> {
const terminal = this.processManager.get(id);
if (!(terminal instanceof TerminalProcess)) {
throw new Error(`terminal "${id}" does not exist`);
}
const result = new Map<string, (string | MarkdownString | undefined)[]>();
this.collections.forEach((value, key) => {
const prev = result.get(key[0]) || [];
prev.push(value.description);
result.set(key[0], prev);
});
return result;
}
async getEnvVarCollections(): Promise<[string, string, boolean, SerializableEnvironmentVariableCollection][]> {
const result: [string, string, boolean, SerializableEnvironmentVariableCollection][] = [];
this.collections.forEach((value, [extensionIdentifier, rootUri]) => {
result.push([extensionIdentifier, rootUri, value.persistent, { description: value.description, mutators: [...value.variableMutators.entries()] }]);
});
return result;
}
}

View File

@@ -0,0 +1,63 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { createTerminalTestContainer } from './test/terminal-test-container';
import { BackendApplication } from '@theia/core/lib/node/backend-application';
import { IShellTerminalServer } from '../common/shell-terminal-protocol';
import * as http from 'http';
import * as https from 'https';
import { terminalsPath } from '../common/terminal-protocol';
import { TestWebSocketChannelSetup } from '@theia/core/lib/node/messaging/test/test-web-socket-channel';
describe('Terminal Backend Contribution', function (): void {
this.timeout(10000);
let server: http.Server | https.Server;
let shellTerminalServer: IShellTerminalServer;
beforeEach(async () => {
const container = createTerminalTestContainer();
const application = container.get(BackendApplication);
shellTerminalServer = container.get(IShellTerminalServer);
server = await application.start(3000, 'localhost');
});
afterEach(() => {
const s = server;
server = undefined!;
shellTerminalServer = undefined!;
s.close();
});
it('is data received from the terminal ws server', async () => {
const terminalId = await shellTerminalServer.create({});
await new Promise<void>((resolve, reject) => {
const path = `${terminalsPath}/${terminalId}`;
const { connectionProvider } = new TestWebSocketChannelSetup({ server, path });
connectionProvider.listen(path, (path2, channel) => {
channel.onError(reject);
channel.onClose(event => reject(new Error(`channel is closed with '${event.code}' code and '${event.reason}' reason}`)));
if (path2 === path) {
resolve();
channel.close();
}
}, false);
});
});
});

View File

@@ -0,0 +1,60 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common';
import { TerminalProcess, ProcessManager } from '@theia/process/lib/node';
import { terminalsPath } from '../common/terminal-protocol';
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
import { StringBufferingStream } from './buffering-stream';
@injectable()
export class TerminalBackendContribution implements MessagingService.Contribution {
protected readonly decoder = new TextDecoder('utf-8');
@inject(ProcessManager)
protected readonly processManager: ProcessManager;
@inject(ILogger) @named('terminal')
protected readonly logger: ILogger;
configure(service: MessagingService): void {
service.registerChannelHandler(`${terminalsPath}/:id`, (params: { id: string }, channel) => {
const id = parseInt(params.id, 10);
const termProcess = this.processManager.get(id);
if (termProcess instanceof TerminalProcess) {
const output = termProcess.createOutputStream();
// Create a RPC connection to the terminal process
// eslint-disable-next-line @typescript-eslint/no-explicit-any
channel.onMessage(e => {
termProcess.write(e().readString());
});
const buffer = new StringBufferingStream();
buffer.onData(chunk => {
channel.getWriteBuffer().writeString(chunk).commit();
});
output.on('data', chunk => {
buffer.push(chunk);
});
channel.onClose(() => {
buffer.dispose();
output.dispose();
});
}
});
}
}

View File

@@ -0,0 +1,81 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule, Container, interfaces } from '@theia/core/shared/inversify';
import { TerminalBackendContribution } from './terminal-backend-contribution';
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common/messaging';
import { ShellProcess, ShellProcessFactory, ShellProcessOptions } from './shell-process';
import { ITerminalServer, terminalPath } from '../common/terminal-protocol';
import { IBaseTerminalClient, DispatchingBaseTerminalClient, IBaseTerminalServer } from '../common/base-terminal-protocol';
import { TerminalServer } from './terminal-server';
import { IShellTerminalServer, shellTerminalPath } from '../common/shell-terminal-protocol';
import { ShellTerminalServer } from '../node/shell-terminal-server';
import { TerminalWatcher } from '../common/terminal-watcher';
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
import { bindTerminalPreferences } from '../common/terminal-preferences';
export function bindTerminalServer(bind: interfaces.Bind, { path, identifier, constructor }: {
path: string,
identifier: interfaces.ServiceIdentifier<IBaseTerminalServer>,
constructor: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new(...args: any[]): IBaseTerminalServer;
}
}): void {
const dispatchingClient = new DispatchingBaseTerminalClient();
bind<IBaseTerminalServer>(identifier).to(constructor).inSingletonScope().onActivation((context, terminalServer) => {
terminalServer.setClient(dispatchingClient);
dispatchingClient.push(context.container.get(TerminalWatcher).getTerminalClient());
terminalServer.setClient = () => {
throw new Error('use TerminalWatcher');
};
return terminalServer;
});
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler<IBaseTerminalClient>(path, client => {
const disposable = dispatchingClient.push(client);
client.onDidCloseConnection(() => disposable.dispose());
return ctx.container.get(identifier);
})
).inSingletonScope();
}
export default new ContainerModule(bind => {
bind(MessagingService.Contribution).to(TerminalBackendContribution).inSingletonScope();
bind(ShellProcess).toSelf().inTransientScope();
bind(ShellProcessFactory).toFactory(ctx =>
(options: ShellProcessOptions) => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = ctx.container;
child.bind(ShellProcessOptions).toConstantValue(options);
return child.get(ShellProcess);
}
);
bind(TerminalWatcher).toSelf().inSingletonScope();
bindTerminalServer(bind, {
path: terminalPath,
identifier: ITerminalServer,
constructor: TerminalServer
});
bindTerminalServer(bind, {
path: shellTerminalPath,
identifier: IShellTerminalServer,
constructor: ShellTerminalServer
});
bindTerminalPreferences(bind);
});

View File

@@ -0,0 +1,42 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as chai from 'chai';
import { createTerminalTestContainer } from './test/terminal-test-container';
import { ITerminalServer } from '../common/terminal-protocol';
/**
* Globals
*/
const expect = chai.expect;
describe('TerminalServer', function (): void {
this.timeout(5000);
let terminalServer: ITerminalServer;
beforeEach(() => {
const container = createTerminalTestContainer();
terminalServer = container.get(ITerminalServer);
});
it('test terminal create', async function (): Promise<void> {
const args = ['--version'];
const createResult = await terminalServer.create({ command: process.execPath, 'args': args });
expect(createResult).to.be.greaterThan(-1);
});
});

View File

@@ -0,0 +1,52 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import {
ITerminalServer,
ITerminalServerOptions
} from '../common/terminal-protocol';
import { BaseTerminalServer } from './base-terminal-server';
import { TerminalProcessFactory, ProcessManager } from '@theia/process/lib/node';
@injectable()
export class TerminalServer extends BaseTerminalServer implements ITerminalServer {
@inject(TerminalProcessFactory) protected readonly terminalFactory: TerminalProcessFactory;
constructor(
@inject(ProcessManager) processManager: ProcessManager,
@inject(ILogger) @named('terminal') logger: ILogger,
) {
super(processManager, logger);
}
create(options: ITerminalServerOptions): Promise<number> {
return new Promise<number>((resolve, reject) => {
const term = this.terminalFactory(options);
term.onStart(_ => {
this.postCreate(term);
resolve(term.id);
});
term.onError(error => {
this.logger.error('Error while creating terminal', error);
resolve(-1);
});
});
}
}

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Container } from '@theia/core/shared/inversify';
import { bindLogger } from '@theia/core/lib/node/logger-backend-module';
import { backendApplicationModule } from '@theia/core/lib/node/backend-application-module';
import processBackendModule from '@theia/process/lib/node/process-backend-module';
import { messagingBackendModule } from '@theia/core/lib/node/messaging/messaging-backend-module';
import terminalBackendModule from '../terminal-backend-module';
import { ApplicationPackage } from '@theia/core/shared/@theia/application-package';
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
export function createTerminalTestContainer(): Container {
const container = new Container();
container.load(backendApplicationModule);
container.rebind(ApplicationPackage).toConstantValue({} as ApplicationPackage);
container.rebind(ProcessUtils).toConstantValue(new class extends ProcessUtils {
override terminateProcessTree(): void { }
});
bindLogger(container.bind.bind(container));
container.load(messagingBackendModule);
container.load(processBackendModule);
container.load(terminalBackendModule);
return container;
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('terminal package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,34 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../editor"
},
{
"path": "../file-search"
},
{
"path": "../filesystem"
},
{
"path": "../process"
},
{
"path": "../variable-resolver"
},
{
"path": "../workspace"
}
]
}