not finished
This commit is contained in:
4
packages/vscode/package.json
Normal file
4
packages/vscode/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@coder/vscode",
|
||||
"description": "VS Code implementation of the browser-based IDE client."
|
||||
}
|
277
packages/vscode/src/element/augment.ts
Normal file
277
packages/vscode/src/element/augment.ts
Normal file
@ -0,0 +1,277 @@
|
||||
|
||||
export function classSplice(element: HTMLElement, removeClasses: string, addClasses: string): HTMLElement {
|
||||
if (removeClasses) { removeClasses.split(/\s+/g).forEach((className) => element.classList.remove(className)); }
|
||||
if (addClasses) { addClasses.split(/\s+/g).forEach((className) => element.classList.add(className)); }
|
||||
return element;
|
||||
}
|
||||
|
||||
export type Side = "LEFT" | "RIGHT" | "TOP" | "BOTTOM";
|
||||
export type BoundaryPos = [Side, Side];
|
||||
export interface IBoundary {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export type PointPos = ["LEFT" | "CENTER" | "RIGHT", "TOP" | "CENTER" | "BOTTOM"];
|
||||
|
||||
export class FloaterPositioning {
|
||||
private static positionClasses = [
|
||||
"--boundary_top_left",
|
||||
"--boundary_top_right",
|
||||
"--boundary_left_top",
|
||||
"--boundary_right_top",
|
||||
"--boundary_left_bottom",
|
||||
"--boundary_right_bottom",
|
||||
"--boundary_bottom_left",
|
||||
"--boundary_bottom_right",
|
||||
|
||||
"--point_top_left",
|
||||
"--point_top_center",
|
||||
"--point_top_right",
|
||||
"--point_center_left",
|
||||
"--point_center_center",
|
||||
"--point_center_right",
|
||||
"--point_bottom_left",
|
||||
"--point_bottom_center",
|
||||
"--point_bottom_right",
|
||||
].join(" ");
|
||||
|
||||
public readonly target: HTMLElement;
|
||||
constructor(target: HTMLElement) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
// this function was surprisingly difficult
|
||||
public moveToBoundary(boundary: IBoundary, pos: BoundaryPos, keepInBounds: boolean = true) {
|
||||
if (keepInBounds) {
|
||||
const height = this.target.offsetHeight;
|
||||
const width = this.target.offsetWidth;
|
||||
if (height === 0 && width === 0) {
|
||||
throw new Error("target must be added to page before it can be in bounds positioned");
|
||||
}
|
||||
const flip = {
|
||||
BOTTOM: "TOP",
|
||||
LEFT: "RIGHT",
|
||||
RIGHT: "LEFT",
|
||||
TOP: "BOTTOM",
|
||||
} as any;
|
||||
|
||||
const getOverlap = (side: string, strong: boolean) => {
|
||||
switch (side) {
|
||||
case "BOTTOM": return ((strong ? boundary.bottom : boundary.top) + height) - window.innerHeight;
|
||||
case "TOP": return 0 - (strong ? boundary.top : boundary.bottom) - height;
|
||||
case "RIGHT": return ((strong ? boundary.right : boundary.left) + width) - window.innerWidth;
|
||||
case "LEFT": return 0 - (strong ? boundary.left : boundary.right) - width;
|
||||
}
|
||||
};
|
||||
|
||||
const firstA = getOverlap(pos[0], true);
|
||||
if (firstA > 0) {
|
||||
const firstB = getOverlap(flip[pos[0]], true);
|
||||
if (firstB < firstA) {
|
||||
pos[0] = flip[pos[0]];
|
||||
}
|
||||
}
|
||||
|
||||
const secA = getOverlap(pos[1], false);
|
||||
if (secA > 0) {
|
||||
const secB = getOverlap(flip[pos[1]], false);
|
||||
if (secB < secA) {
|
||||
pos[1] = flip[pos[1]];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
classSplice(this.target, FloaterPositioning.positionClasses, undefined);
|
||||
this.target.classList.add(`--boundary_${pos.map((val) => val.toLowerCase()).join("_")}`);
|
||||
|
||||
const displayPos: IBoundary = {} as any;
|
||||
switch (pos[0]) {
|
||||
case "BOTTOM": displayPos.top = boundary.bottom; break;
|
||||
case "TOP": displayPos.bottom = window.innerHeight - boundary.top; break;
|
||||
case "LEFT": displayPos.right = window.innerWidth - boundary.left; break;
|
||||
case "RIGHT": displayPos.left = boundary.right; break;
|
||||
}
|
||||
switch (pos[1]) {
|
||||
case "BOTTOM": displayPos.top = boundary.top; break;
|
||||
case "TOP": displayPos.bottom = window.innerHeight - boundary.bottom; break;
|
||||
case "LEFT": displayPos.right = window.innerWidth - boundary.right; break;
|
||||
case "RIGHT": displayPos.left = boundary.left; break;
|
||||
}
|
||||
this.applyPos(displayPos);
|
||||
}
|
||||
|
||||
public moveToPoint(point: { top: number, left: number }, pos: PointPos, keepInBounds: boolean = true): void {
|
||||
if (keepInBounds) {
|
||||
const height = this.target.offsetHeight;
|
||||
const width = this.target.offsetWidth;
|
||||
if (height === 0 && width === 0) {
|
||||
throw new Error("target must be added to page before it can be in bounds positioned");
|
||||
}
|
||||
const flip = {
|
||||
BOTTOM: "TOP",
|
||||
LEFT: "RIGHT",
|
||||
RIGHT: "LEFT",
|
||||
TOP: "BOTTOM",
|
||||
} as any;
|
||||
|
||||
const getOverlap = (side: string) => {
|
||||
switch (side) {
|
||||
case "BOTTOM": return (point.top + height) - window.innerHeight;
|
||||
case "TOP": return -1 * (point.top - height);
|
||||
case "RIGHT": return (point.left + width) - window.innerWidth;
|
||||
case "LEFT": return -1 * (point.left - width);
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const xAlign = pos[0];
|
||||
const normalXOffset = getOverlap(xAlign);
|
||||
if (normalXOffset > 0 && normalXOffset > getOverlap(flip[xAlign])) {
|
||||
pos[0] = flip[xAlign];
|
||||
}
|
||||
|
||||
const yAlign = pos[1];
|
||||
const normalYOffset = getOverlap(yAlign);
|
||||
if (normalYOffset > 0 && normalYOffset > getOverlap(flip[yAlign])) {
|
||||
pos[1] = flip[yAlign];
|
||||
}
|
||||
}
|
||||
|
||||
const displayPos: IBoundary = {} as any;
|
||||
let centerX = false;
|
||||
let centerY = false;
|
||||
switch (pos[0]) {
|
||||
case "CENTER": centerX = true;
|
||||
case "RIGHT": displayPos.left = point.left; break;
|
||||
case "LEFT": displayPos.right = window.innerWidth - point.left; break;
|
||||
}
|
||||
switch (pos[1]) {
|
||||
case "CENTER": centerY = true;
|
||||
case "BOTTOM": displayPos.top = point.top; break;
|
||||
case "TOP": displayPos.bottom = window.innerHeight - point.top; break;
|
||||
}
|
||||
|
||||
classSplice(this.target, FloaterPositioning.positionClasses, undefined);
|
||||
this.target.classList.add(`--point_${pos.map((val) => val.toLowerCase()).reverse().join("_")}`);
|
||||
|
||||
this.applyPos(displayPos);
|
||||
this.target.style.transform = `${centerX ? "translateX(-50)" : ""} ${centerY ? "translateY(-50)" : ""}`;
|
||||
}
|
||||
|
||||
private applyPos(pos: IBoundary) {
|
||||
this.target.style.top = pos.top !== undefined ? (pos.top + "px") : "";
|
||||
this.target.style.bottom = pos.bottom !== undefined ? (pos.bottom + "px") : "";
|
||||
this.target.style.left = pos.left !== undefined ? (pos.left + "px") : "";
|
||||
this.target.style.right = pos.right !== undefined ? (pos.right + "px") : "";
|
||||
}
|
||||
}
|
||||
|
||||
export type Boolable = ((item: HTMLElement) => boolean) | boolean;
|
||||
|
||||
export interface IMakeChildrenSelectableArgs {
|
||||
maxSelectable?: number;
|
||||
selectOnKeyHover?: Boolable;
|
||||
selectOnMouseHover?: Boolable;
|
||||
onHover?: (selectedItem: HTMLElement) => void;
|
||||
onSelect: (selectedItem: HTMLElement, wasAlreadySelected?: boolean) => void;
|
||||
isItemSelectable?: (item: HTMLElement) => boolean;
|
||||
}
|
||||
|
||||
export class SelectableChildren {
|
||||
|
||||
public readonly target: HTMLElement;
|
||||
private keyHoveredItem: HTMLElement;
|
||||
private _selectedItem: HTMLElement;
|
||||
private selectOnMouseHover: Boolable;
|
||||
private onHover: (selectedItem: HTMLElement) => void;
|
||||
private onSelect: (selectedItem: HTMLElement) => void;
|
||||
private isItemSelectable: (item: HTMLElement) => boolean;
|
||||
|
||||
constructor(target: HTMLElement, args: IMakeChildrenSelectableArgs) {
|
||||
this.target = target;
|
||||
|
||||
this.onHover = args.onHover;
|
||||
this.onSelect = args.onSelect;
|
||||
this.selectOnMouseHover = args.selectOnMouseHover || false;
|
||||
this.isItemSelectable = args.isItemSelectable;
|
||||
|
||||
// this.target.addEventListener("keydown", (event) => this.onTargetKeydown(event));
|
||||
this.target.addEventListener("mousemove", (event) => this.onTargetMousemove(event));
|
||||
|
||||
Array.from(this.target.children).forEach((child: HTMLElement) => this.registerChild(child));
|
||||
}
|
||||
|
||||
public registerChild(child: HTMLElement) {
|
||||
child.addEventListener("mouseover", (event) => this.onItemHover(child, event));
|
||||
child.addEventListener("mousedown", (event) => this.onItemMousedown(child, event));
|
||||
}
|
||||
|
||||
public get selectedItem() { return this._selectedItem; }
|
||||
|
||||
public unsetSelection() {
|
||||
if (this.selectedItem) { this.selectedItem.classList.remove("--is_selected"); }
|
||||
this._selectedItem = undefined;
|
||||
}
|
||||
|
||||
public trySelectItem(item: HTMLElement): boolean {
|
||||
if (this.checkItemSelectable(item) === false) { return false; }
|
||||
const alreadySelected = item === this.selectedItem;
|
||||
if (!alreadySelected) {
|
||||
this.unsetSelection();
|
||||
this._selectedItem = item;
|
||||
this.selectedItem.classList.add("--is_selected");
|
||||
this.onSelect(this.selectedItem);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public updateAllItemIsSelectableStates() {
|
||||
this.updateItemIsSelectableState(Array.from(this.target.childNodes) as any);
|
||||
}
|
||||
|
||||
public updateItemIsSelectableState(itemOrItems?: HTMLElement | HTMLElement[]) {
|
||||
const items: HTMLElement[] = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
|
||||
|
||||
items.forEach((item) => {
|
||||
if (!this.isItemSelectable || this.isItemSelectable(item)) {
|
||||
item.classList.remove("--not_selectable");
|
||||
} else {
|
||||
item.classList.add("--not_selectable");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkItemSelectable(item: HTMLElement): boolean {
|
||||
this.updateItemIsSelectableState(item);
|
||||
return item.classList.contains("--not_selectable") === false;
|
||||
}
|
||||
|
||||
private onTargetMousemove(event: MouseEvent) {
|
||||
classSplice(this.target, "--key_naving", "--mouse_naving");
|
||||
if (this.keyHoveredItem) {
|
||||
this.keyHoveredItem.classList.remove("--key_hovered");
|
||||
this.keyHoveredItem = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private onItemHover(item: HTMLElement, event: Event) {
|
||||
if (this.onHover) { this.onHover(item); }
|
||||
if (
|
||||
this.checkItemSelectable(item)
|
||||
&& typeof this.selectOnMouseHover === "boolean"
|
||||
? this.selectOnMouseHover
|
||||
: (this.selectOnMouseHover as any)(item)
|
||||
) {
|
||||
this.trySelectItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
private onItemMousedown(item: HTMLElement, event: Event) {
|
||||
this.trySelectItem(item);
|
||||
}
|
||||
|
||||
}
|
68
packages/vscode/src/element/contextmenu.css
Normal file
68
packages/vscode/src/element/contextmenu.css
Normal file
@ -0,0 +1,68 @@
|
||||
.context-menu-overlay {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.command-menu {
|
||||
position: fixed;
|
||||
background-color: var(--floater, rgba(67, 67, 61, 1));
|
||||
border: 2px solid rgba(66, 66, 60, 1);
|
||||
color: var(--fg, rgb(216, 216, 216));
|
||||
font-size: 14px;
|
||||
box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.1);
|
||||
/* border-radius: 4px; */
|
||||
overflow: hidden;
|
||||
}
|
||||
.command-menu.--boundary_bottom_right, .command-menu.--boundary_right_bottom, .command-menu.--point_bottom_right {
|
||||
border-top-left-radius: 0px;
|
||||
}
|
||||
.command-menu.--boundary_bottom_left, .command-menu.--boundary_left_bottom, .command-menu.--point_bottom_left {
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
.command-menu.--boundary_top_right, .command-menu.--boundary_right_top, .command-menu.--point_top_right {
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
.command-menu.--boundary_top_left, .command-menu.--boundary_left_top, .command-menu.--point_top_left {
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
.command-menu .menuitem {
|
||||
white-space: nowrap;
|
||||
padding: 5px 20px;
|
||||
cursor: pointer;
|
||||
min-width: 150px;
|
||||
}
|
||||
.command-menu .menuitem:not(.--not_selectable):not(.--is_selected):hover {
|
||||
background: var(--floaterHover, rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
.command-menu .menuitem.--is_selected {
|
||||
background: var(--floaterActive, rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
.command-menu .menuitem:not(.--non_selection_item).--not_selectable {
|
||||
color: var(--fgFade7, rgba(255, 255, 255, 0.3));
|
||||
cursor: unset;
|
||||
}
|
||||
.command-menu .menuitem.entry {
|
||||
display: flex;
|
||||
}
|
||||
.command-menu .menuitem.spacer {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.command-menu .menuitem.spacer > hr {
|
||||
margin: 0px;
|
||||
border: none;
|
||||
background: var(--fgFade7, rgba(47, 47, 41, 1));
|
||||
opacity: 0.4;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.command-menu .menuitem.entry > .keybind {
|
||||
margin-left: auto;
|
||||
padding-left: 50px;
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
250
packages/vscode/src/element/contextmenu.ts
Normal file
250
packages/vscode/src/element/contextmenu.ts
Normal file
@ -0,0 +1,250 @@
|
||||
/**
|
||||
* SHOULD BE MOVED. THIS IS NOT A UI SECTION
|
||||
*/
|
||||
|
||||
import * as augment from './augment';
|
||||
import "./contextmenu.css";
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
|
||||
export enum MenuItemType {
|
||||
COMMAND,
|
||||
SUB_MENU,
|
||||
SPACER,
|
||||
CUSTOM_ITEM,
|
||||
GENERATIVE_SUBMENU,
|
||||
}
|
||||
|
||||
export interface IMenuItem {
|
||||
type: MenuItemType;
|
||||
domNode: HTMLElement;
|
||||
priority: number;
|
||||
selectOnHover: boolean;
|
||||
refreshDomNode?: () => void;
|
||||
isSelectable?: (() => boolean) | boolean;
|
||||
onSelect?: () => void;
|
||||
}
|
||||
|
||||
export class ContextMenuManager {
|
||||
|
||||
private readonly domNode: FastDomNode<HTMLDivElement>;
|
||||
|
||||
public constructor() {
|
||||
this.domNode = createFastDomNode(document.createElement("div"));
|
||||
this.domNode.setClassName("context-menu-overlay");
|
||||
// this.display = false;
|
||||
this.domNode.domNode.addEventListener("mousedown", (event) => {
|
||||
event.preventDefault();
|
||||
if (event.target === this.domNode.domNode) {
|
||||
this.display = false;
|
||||
}
|
||||
});
|
||||
this.domNode.domNode.addEventListener("closeAllContextMenus", (event) => {
|
||||
this.display = false;
|
||||
event.stopPropagation();
|
||||
});
|
||||
this.domNode.domNode.addEventListener("contextMenuActive", (event) => {
|
||||
// this.clearStackTill(event.target as HTMLElement);
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
public onceClose(cb: () => void): void {
|
||||
const l = () => {
|
||||
cb();
|
||||
this.domNode.domNode.removeEventListener("closed", l);
|
||||
};
|
||||
this.domNode.domNode.addEventListener("closed", l);
|
||||
}
|
||||
|
||||
public set display(value: boolean) {
|
||||
if (value) {
|
||||
document.body.appendChild(this.domNode.domNode);
|
||||
} else {
|
||||
this.domNode.domNode.remove();
|
||||
this.domNode.domNode.dispatchEvent(new Event("closed"));
|
||||
}
|
||||
}
|
||||
|
||||
public displayMenuAtBoundary<T>(
|
||||
menu: ContextMenu,
|
||||
boundary: augment.IBoundary,
|
||||
positioning: augment.BoundaryPos = ["BOTTOM", "RIGHT"],
|
||||
clearStack: boolean = true,
|
||||
): void {
|
||||
this.displayMenu(menu, clearStack);
|
||||
menu.positioningAugment.moveToBoundary(boundary, positioning);
|
||||
}
|
||||
|
||||
public displayMenuAtPoint<T>(
|
||||
menu: ContextMenu,
|
||||
point: { top: number, left: number },
|
||||
positioning: augment.PointPos = ["RIGHT", "BOTTOM"],
|
||||
clearStack: boolean = true,
|
||||
): void {
|
||||
this.displayMenu(menu, clearStack);
|
||||
menu.positioningAugment.moveToPoint(point, positioning);
|
||||
}
|
||||
|
||||
private displayMenu(menu: ContextMenu, clearStack: boolean) {
|
||||
while (this.domNode.domNode.lastChild) {
|
||||
this.domNode.domNode.removeChild(this.domNode.domNode.lastChild);
|
||||
}
|
||||
this.domNode.appendChild(menu.domNode);
|
||||
this.display = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ContextMenu {
|
||||
|
||||
public readonly id: string;
|
||||
public readonly positioningAugment: augment.FloaterPositioning;
|
||||
public readonly selectionAugment: augment.SelectableChildren;
|
||||
public readonly domNode: FastDomNode<HTMLDivElement>;
|
||||
private readonly manager: ContextMenuManager;
|
||||
|
||||
private cachedActive: HTMLElement;
|
||||
private domNodeToItemMap: Map<HTMLElement, IMenuItem>;
|
||||
private items: IMenuItem[];
|
||||
|
||||
constructor(id: string, manager: ContextMenuManager) {
|
||||
this.id = id;
|
||||
this.manager = manager;
|
||||
this.items = [];
|
||||
this.domNodeToItemMap = new Map();
|
||||
this.domNode = createFastDomNode(document.createElement("div"));
|
||||
this.domNode.setClassName("command-menu");
|
||||
this.positioningAugment = new augment.FloaterPositioning(this.domNode.domNode);
|
||||
|
||||
const selectOnHover = (itemDomNode: HTMLElement) => this.domNodeToItemMap.get(itemDomNode).selectOnHover;
|
||||
this.selectionAugment = new augment.SelectableChildren(this.domNode.domNode, {
|
||||
isItemSelectable: (itemDomNode) => {
|
||||
const item = this.domNodeToItemMap.get(itemDomNode);
|
||||
return typeof item.isSelectable === "boolean" ? item.isSelectable : item.isSelectable();
|
||||
},
|
||||
onHover: (itemDomNode) => {
|
||||
const item = this.domNodeToItemMap.get(itemDomNode);
|
||||
if (item.type !== MenuItemType.SUB_MENU && item.type !== MenuItemType.GENERATIVE_SUBMENU) {
|
||||
this.domNode.domNode.dispatchEvent(new Event("contextMenuActive", { bubbles: true }));
|
||||
this.selectionAugment.unsetSelection();
|
||||
}
|
||||
},
|
||||
onSelect: (itemDomNode) => {
|
||||
const item = this.domNodeToItemMap.get(itemDomNode);
|
||||
if (item.onSelect) { item.onSelect(); }
|
||||
},
|
||||
selectOnKeyHover: selectOnHover,
|
||||
selectOnMouseHover: selectOnHover,
|
||||
});
|
||||
}
|
||||
|
||||
public set display(onOff: boolean) {
|
||||
if (onOff === true) {
|
||||
this.cachedActive = document.activeElement as HTMLElement;
|
||||
if (this.cachedActive) {
|
||||
this.cachedActive.blur();
|
||||
}
|
||||
this.items.forEach((item) => !!item.refreshDomNode ? item.refreshDomNode() : null);
|
||||
this.selectionAugment.updateAllItemIsSelectableStates();
|
||||
} else if (this.cachedActive) {
|
||||
this.cachedActive.focus();
|
||||
this.cachedActive = null;
|
||||
}
|
||||
this.domNode.domNode.style.display = onOff ? "" : "none";
|
||||
}
|
||||
|
||||
public addSpacer(priority: number) {
|
||||
const rootNode = createFastDomNode(document.createElement("div"));
|
||||
rootNode.setClassName("menuitem spacer");
|
||||
const hrNode = createFastDomNode(document.createElement("hr"));
|
||||
rootNode.appendChild(hrNode);
|
||||
this.appendMenuItem({
|
||||
domNode: rootNode.domNode,
|
||||
isSelectable: false,
|
||||
priority,
|
||||
selectOnHover: false,
|
||||
type: MenuItemType.SPACER,
|
||||
});
|
||||
}
|
||||
|
||||
public addEntry(priority: number, label: string, accelerator: string, enabled: boolean, callback: () => void) {
|
||||
const domNode = createFastDomNode(document.createElement("div"));
|
||||
domNode.setClassName("menuitem entry");
|
||||
const labelNode = createFastDomNode(document.createElement("div"));
|
||||
labelNode.setClassName("entrylabel");
|
||||
labelNode.domNode.innerText = label;
|
||||
domNode.appendChild(labelNode);
|
||||
|
||||
if (accelerator) {
|
||||
const accelNode = createFastDomNode(document.createElement("div"));
|
||||
accelNode.setClassName("keybind");
|
||||
accelNode.domNode.innerText = accelerator;
|
||||
domNode.appendChild(accelNode);
|
||||
}
|
||||
|
||||
|
||||
const menuItem: IMenuItem = {
|
||||
domNode: domNode.domNode,
|
||||
isSelectable: () => enabled,
|
||||
onSelect: () => {
|
||||
if (this.cachedActive) {
|
||||
this.cachedActive.focus();
|
||||
this.cachedActive = null;
|
||||
}
|
||||
callback();
|
||||
domNode.domNode.dispatchEvent(new Event("closeAllContextMenus", { bubbles: true }));
|
||||
},
|
||||
priority,
|
||||
selectOnHover: false,
|
||||
type: MenuItemType.COMMAND,
|
||||
};
|
||||
this.appendMenuItem(menuItem);
|
||||
}
|
||||
|
||||
public addSubMenu(priority: number, subMenu: ContextMenu, label: string, description?: string) {
|
||||
const rootNode = createFastDomNode(document.createElement("div"));
|
||||
rootNode.setClassName("menuitem");
|
||||
const subLabel = createFastDomNode(document.createElement("div"));
|
||||
subLabel.setClassName("seg submenulabel");
|
||||
subLabel.domNode.innerText = label;
|
||||
const subArrow = createFastDomNode(document.createElement("div"));
|
||||
subArrow.setClassName("seg submenuarrow");
|
||||
subArrow.domNode.innerText = "->";
|
||||
rootNode.appendChild(subLabel);
|
||||
rootNode.appendChild(subArrow);
|
||||
this.appendMenuItem({
|
||||
domNode: rootNode.domNode,
|
||||
isSelectable: true,
|
||||
onSelect: () => {
|
||||
this.manager.displayMenuAtBoundary(subMenu, rootNode.domNode.getBoundingClientRect(), ["RIGHT", "BOTTOM"], false);
|
||||
},
|
||||
priority,
|
||||
selectOnHover: true,
|
||||
type: MenuItemType.SUB_MENU,
|
||||
});
|
||||
}
|
||||
|
||||
// used for generative sub menu... needs to be less public
|
||||
public removeAllItems() {
|
||||
while (this.items.length) {
|
||||
const removeMe = this.items.pop();
|
||||
removeMe.domNode.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private appendMenuItem(item: IMenuItem) {
|
||||
this.items.push(item);
|
||||
this.domNodeToItemMap.set(item.domNode, item);
|
||||
this.selectionAugment.registerChild(item.domNode);
|
||||
this.items = this.items.sort((a, b) => a.priority - b.priority);
|
||||
this.sortDomNode();
|
||||
}
|
||||
|
||||
private sortDomNode() {
|
||||
while (this.domNode.domNode.lastChild) {
|
||||
this.domNode.domNode.removeChild(this.domNode.domNode.lastChild);
|
||||
}
|
||||
this.items.forEach((item) => this.domNode.domNode.appendChild(item.domNode));
|
||||
}
|
||||
|
||||
}
|
55
packages/vscode/src/entry.ts
Normal file
55
packages/vscode/src/entry.ts
Normal file
@ -0,0 +1,55 @@
|
||||
const loadTime = time(2500);
|
||||
|
||||
import { URI } from "vs/base/common/uri";
|
||||
import { field, logger, time } from "@coder/logger";
|
||||
import { Client, IURI, setUriFactory } from "@coder/ide";
|
||||
import "./firefox";
|
||||
import "./setup";
|
||||
|
||||
setUriFactory({
|
||||
// TODO: not sure why this is an error.
|
||||
// tslint:disable-next-line no-any
|
||||
create: <URI>(uri: IURI): URI => URI.from(uri) as any,
|
||||
file: (path: string): IURI => URI.file(path),
|
||||
parse: (raw: string): IURI => URI.parse(raw),
|
||||
});
|
||||
|
||||
export const client = new Client({
|
||||
mkDirs: [
|
||||
"~/vscode/extensions",
|
||||
"~/.config/User",
|
||||
],
|
||||
});
|
||||
|
||||
const overlayElement = document.getElementById("overlay");
|
||||
const msgElement = overlayElement
|
||||
? overlayElement.querySelector(".message") as HTMLElement
|
||||
: undefined;
|
||||
|
||||
const importTime = time(1500);
|
||||
import(/* webpackPrefetch: true */ "./workbench").then((module) => {
|
||||
logger.info("Loaded workbench bundle", field("duration", importTime));
|
||||
const initTime = time(1500);
|
||||
|
||||
return module.initialize(client).then(() => {
|
||||
logger.info("Initialized workbench", field("duration", initTime));
|
||||
logger.info("Load completed", field("duration", loadTime));
|
||||
if (overlayElement) {
|
||||
overlayElement.style.opacity = "0";
|
||||
overlayElement.addEventListener("transitionend", () => {
|
||||
overlayElement.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
logger.error(error);
|
||||
if (overlayElement) {
|
||||
overlayElement.classList.add("error");
|
||||
}
|
||||
if (msgElement) {
|
||||
msgElement.innerText = `Failed to load: ${error.message}. Retrying in 3 seconds...`;
|
||||
}
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 3000);
|
||||
});
|
6
packages/vscode/src/fill/css.js
Normal file
6
packages/vscode/src/fill/css.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = function(source) {
|
||||
if (this.resourcePath.endsWith(".ts")) {
|
||||
this.resourcePath = this.resourcePath.replace(".ts", ".css");
|
||||
}
|
||||
return `module.exports = require("${this.resourcePath}");`;
|
||||
};
|
8
packages/vscode/src/fill/native-keymap.ts
Normal file
8
packages/vscode/src/fill/native-keymap.ts
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
getCurrentKeyboardLayout: (): null => {
|
||||
return null;
|
||||
},
|
||||
getKeyMap: (): undefined[] => {
|
||||
return [];
|
||||
},
|
||||
};
|
73
packages/vscode/src/fill/node-pty.ts
Normal file
73
packages/vscode/src/fill/node-pty.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import * as cp from "child_process";
|
||||
import { EventEmitter } from "events";
|
||||
import * as nodePty from "node-pty";
|
||||
|
||||
type nodePtyType = typeof nodePty;
|
||||
|
||||
/**
|
||||
* Implementation of nodePty for the browser.
|
||||
*/
|
||||
class Pty implements nodePty.IPty {
|
||||
|
||||
private readonly emitter: EventEmitter;
|
||||
|
||||
public constructor(file: string, args: string[] | string, options: nodePty.IPtyForkOptions) {
|
||||
this.emitter = new EventEmitter();
|
||||
const session = wush.execute({
|
||||
command: `${file} ${Array.isArray(args) ? args.join(" ") : args}`,
|
||||
directory: options.cwd,
|
||||
environment: {
|
||||
...(options.env || {}),
|
||||
TERM: "xterm-color",
|
||||
},
|
||||
size: options && options.cols && options.rows ? {
|
||||
columns: options.cols,
|
||||
rows: options.rows,
|
||||
} : {
|
||||
columns: 100,
|
||||
rows: 100,
|
||||
},
|
||||
});
|
||||
this.on("write", (data) => session.sendStdin(data));
|
||||
this.on("kill", (exitCode) => session.close());
|
||||
this.on("resize", (columns, rows) => session.setSize({ columns, rows }));
|
||||
session.onStdout((data) => this.emitter.emit("data", data));
|
||||
session.onStderr((data) => this.emitter.emit("data", data));
|
||||
session.onDone((exitCode) => this.emitter.emit("exit", exitCode));
|
||||
}
|
||||
|
||||
public get pid(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public get process(): string {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
public on(event: string, listener: (...args) => void): void {
|
||||
this.emitter.on(event, listener);
|
||||
}
|
||||
|
||||
public resize(columns: number, rows: number): void {
|
||||
this.emitter.emit("resize", columns, rows);
|
||||
}
|
||||
|
||||
public write(data: string): void {
|
||||
this.emitter.emit("write", data);
|
||||
}
|
||||
|
||||
public kill(signal?: string): void {
|
||||
this.emitter.emit("kill", signal);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ptyType: nodePtyType = {
|
||||
|
||||
spawn: (file: string, args: string[] | string, options: nodePty.IPtyForkOptions): nodePty.IPty => {
|
||||
return new Pty(file, args, options);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
module.exports = ptyType;
|
9
packages/vscode/src/firefox.scss
Normal file
9
packages/vscode/src/firefox.scss
Normal file
@ -0,0 +1,9 @@
|
||||
// Using @supports to keep the Firefox fixes completely separate from vscode's
|
||||
// CSS that is tailored for Chrome.
|
||||
@supports (-moz-appearance:none) {
|
||||
/* Fix buttons getting cut off on notifications. */
|
||||
.monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button.monaco-text-button {
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
20
packages/vscode/src/firefox.ts
Normal file
20
packages/vscode/src/firefox.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import "./firefox.scss";
|
||||
|
||||
if (!("toElement" in MouseEvent.prototype)) {
|
||||
Object.defineProperty(MouseEvent.prototype, "toElement", {
|
||||
get: function (): EventTarget | null {
|
||||
// @ts-ignore
|
||||
const event = this as MouseEvent;
|
||||
switch (event.type) {
|
||||
case "mouseup":
|
||||
case "focusin":
|
||||
case "mousenter":
|
||||
case "mouseover":
|
||||
case "dragenter":
|
||||
return event.target;
|
||||
default:
|
||||
return event.relatedTarget;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
58
packages/vscode/src/storageService.ts
Normal file
58
packages/vscode/src/storageService.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export class StorageService implements IStorageService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
private _globalObject: object;
|
||||
private _workspaceObject: object;
|
||||
|
||||
public constructor(globalState: object, workspaceState: object) {
|
||||
this._globalObject = globalState;
|
||||
this._workspaceObject = workspaceState;
|
||||
}
|
||||
|
||||
public get globalObject() {
|
||||
return this._globalObject;
|
||||
}
|
||||
|
||||
public get workspaceObject() {
|
||||
return this._workspaceObject;
|
||||
}
|
||||
|
||||
public store(key: string, value: any, scope?: StorageScope): void {
|
||||
this.getObject(scope)[key] = value;
|
||||
}
|
||||
|
||||
public remove(key: string, scope?: StorageScope): void {
|
||||
delete this.getObject(scope)[key];
|
||||
}
|
||||
|
||||
public get(key: string, scope?: StorageScope, defaultValue?: string): string {
|
||||
return this.getObject(scope)[key] || defaultValue;
|
||||
}
|
||||
|
||||
public getInteger(key: string, scope?: StorageScope, defaultValue?: number): number {
|
||||
return parseInt(this.get(key, scope), 10) || defaultValue;
|
||||
}
|
||||
|
||||
public getBoolean(key: string, scope?: StorageScope, defaultValue?: boolean): boolean {
|
||||
const v = this.get(key, scope);
|
||||
if (typeof v !== "undefined") {
|
||||
return v === 'true';
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private getObject(scope = StorageScope.GLOBAL): object {
|
||||
switch (scope) {
|
||||
case StorageScope.GLOBAL:
|
||||
return this._globalObject;
|
||||
case StorageScope.WORKSPACE:
|
||||
return this._workspaceObject;
|
||||
default:
|
||||
throw new Error("unsupported storage scope");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
59
packages/vscode/src/upload.ts
Normal file
59
packages/vscode/src/upload.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Upload as BaseUpload, IURI } from "@coder/ide";
|
||||
import { client } from "./entry";
|
||||
import { INotificationService, Severity } from "vs/platform/notification/common/notification";
|
||||
import { IProgressService2, ProgressLocation } from "vs/workbench/services/progress/common/progress";
|
||||
|
||||
export class Upload extends BaseUpload {
|
||||
|
||||
public constructor(
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IProgressService2 progressService: IProgressService2,
|
||||
) {
|
||||
super({
|
||||
error: (error) => {
|
||||
notificationService.error(error);
|
||||
},
|
||||
prompt: (message, choices) => {
|
||||
return new Promise((resolve) => {
|
||||
notificationService.prompt(
|
||||
Severity.Error,
|
||||
message,
|
||||
choices.map((label) => ({
|
||||
label,
|
||||
run: () => {
|
||||
resolve(label);
|
||||
},
|
||||
})),
|
||||
() => {
|
||||
resolve(undefined);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
}, {
|
||||
start: (title, task) => {
|
||||
let lastProgress = 0;
|
||||
|
||||
return progressService.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title,
|
||||
cancellable: true,
|
||||
}, (progress) => {
|
||||
return task({
|
||||
report: (p) => {
|
||||
progress.report({ increment: p - lastProgress });
|
||||
lastProgress = p;
|
||||
},
|
||||
});
|
||||
}, () => {
|
||||
this.cancel();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async uploadDropped(event: DragEvent, uri?: IURI): Promise<string[]> {
|
||||
return super.uploadDropped(event, uri || (await client.workspace).mountUri);
|
||||
}
|
||||
|
||||
}
|
245
packages/vscode/src/workbench.ts
Normal file
245
packages/vscode/src/workbench.ts
Normal file
@ -0,0 +1,245 @@
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
Client, Emitter, getFactory, IPosition, IFileConflict, ConflictResolution,
|
||||
Event,
|
||||
IDisposable,
|
||||
IDocumentContentChangedEvent, IURI, IRange, escapePath,
|
||||
IOrphanedChangedEvent,
|
||||
} from 'coder/common';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration } from 'vs/editor/common/model';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerContextMenuListener } from 'vs/base/parts/contextmenu/electron-main/contextmenu';
|
||||
import { Workbench } from 'vs/workbench/electron-browser/workbench';
|
||||
import { StorageService } from 'coder/storageService';
|
||||
import { IContentData, IFileService, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IStat, FileType } from 'vs/platform/files/common/files';
|
||||
import { onInstantiation as onFileServiceInstantiation } from 'vs/workbench/services/files/electron-browser/fileService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import product from 'vs/platform/node/product';
|
||||
import { CONFLICT_RESOLUTION_SCHEME } from 'vs/workbench/parts/files/electron-browser/saveErrorHandler';
|
||||
import { ITextFileService, ModelState } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { field, logger } from 'coder/logger';
|
||||
import { events } from 'coder/analytics';
|
||||
import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations';
|
||||
import { registerCollaboratorDecorations } from 'coder/collaborators';
|
||||
import { IInitData as ISharedProcessInitData } from 'vs/code/electron-browser/sharedProcess/sharedProcessClient';
|
||||
import { LogLevel } from 'vs/platform/log/common/log';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
let protoResolve: (protocol: Protocol) => void;
|
||||
export const protocolPromise = new Promise<Protocol>((res) => {
|
||||
protoResolve = res;
|
||||
});
|
||||
let storageResolve: (storageService: StorageService) => void;
|
||||
export const getStorageService = new Promise<StorageService>((res) => {
|
||||
storageResolve = res;
|
||||
});
|
||||
export let systemExtensionsLocation: string;
|
||||
export let forkedBinLocation: string;
|
||||
|
||||
const hasNativeClipboard = typeof navigator !== "undefined" && typeof (navigator as any).clipboard !== "undefined" && typeof (navigator as any).clipboard.readText !== "undefined";
|
||||
let isEnabled: boolean = false;
|
||||
const clipboardEnabledEmitter = new Emitter<boolean>();
|
||||
export const nativeClipboard: {
|
||||
readonly contextKey: RawContextKey<boolean>;
|
||||
readonly instance: {
|
||||
readText(): Promise<string>;
|
||||
writeText(value: string): Promise<void>;
|
||||
};
|
||||
readonly onChange: Event<boolean>;
|
||||
readonly isEnabled: boolean;
|
||||
} = {
|
||||
contextKey: new RawContextKey('nativeClipboard', hasNativeClipboard),
|
||||
instance: hasNativeClipboard ? (navigator as any).clipboard : undefined,
|
||||
get onChange(): Event<boolean> {
|
||||
return clipboardEnabledEmitter.event;
|
||||
},
|
||||
get isEnabled(): boolean {
|
||||
return isEnabled;
|
||||
}
|
||||
};
|
||||
|
||||
let workbench: Workbench;
|
||||
|
||||
function getModelService(): IModelService {
|
||||
return workbench.workbenchParams.serviceCollection.get<IModelService>(IModelService) as IModelService;
|
||||
}
|
||||
|
||||
function getCodeEditorService(): ICodeEditorService {
|
||||
return workbench.workbenchParams.serviceCollection.get(ICodeEditorService) as ICodeEditorService;
|
||||
}
|
||||
|
||||
function getFileService(): IFileService {
|
||||
return workbench.workbenchParams.serviceCollection.get(IFileService) as IFileService;
|
||||
}
|
||||
|
||||
function getTextFileService(): ITextFileService {
|
||||
return workbench.workbenchParams.serviceCollection.get(ITextFileService) as ITextFileService;
|
||||
}
|
||||
|
||||
function getNotificationService(): INotificationService {
|
||||
return workbench.workbenchParams.serviceCollection.get(INotificationService) as INotificationService;
|
||||
}
|
||||
|
||||
export const initialize = async (client: Client): Promise<void> {
|
||||
window.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
const storageServicePromise = client.wrapTask("Set configurations", 5, async (state) => {
|
||||
const storageService = new StorageService(state.global, state.workspace);
|
||||
storageResolve(storageService);
|
||||
|
||||
return storageService;
|
||||
}, client.state);
|
||||
|
||||
// Set up window ID for logging. We'll also use a static logging directory
|
||||
// otherwise we'd have to get the log directory back from the currently
|
||||
// running shared process. This allows us to not wait for that. Each window
|
||||
// will still have its own logging within that directory.
|
||||
const windowId = parseInt(toLocalISOString(new Date()).replace(/[-:.TZ]/g, ""), 10);
|
||||
process.env.VSCODE_LOGS = "/tmp/vscode-logs";
|
||||
|
||||
client.wrapTask("Start shared process", 5, async (api, wush, mountPath) => {
|
||||
const session = wush.execute({
|
||||
command: "bash -c 'VSCODE_ALLOW_IO=true"
|
||||
+ " AMD_ENTRYPOINT=vs/code/electron-browser/sharedProcess/sharedProcessClient"
|
||||
+ ` nice -n -17 ${nodePath} ${bootstrapForkLocation} --client'`,
|
||||
});
|
||||
|
||||
const sharedProcessLogger = logger.named("shr proc");
|
||||
session.onStderr((data) => {
|
||||
sharedProcessLogger.error("stderr: " + data);
|
||||
});
|
||||
|
||||
session.onDone(() => {
|
||||
workbenchPromise.then(() => {
|
||||
getNotificationService().prompt(
|
||||
Severity.Error,
|
||||
"Shared process terminated unexpectedly.",
|
||||
[{
|
||||
label: "Reload IDE",
|
||||
run: (): void => {
|
||||
window.location.reload();
|
||||
},
|
||||
}],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const protocol = Protocol.fromStdio({
|
||||
onMessage: (cb) => {
|
||||
session.onStdout((data) => {
|
||||
cb(Buffer.from(data as any));
|
||||
}, true);
|
||||
},
|
||||
sendMessage: (data) => {
|
||||
session.sendStdin(data);
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const listener = protocol.onMessage((message) => {
|
||||
const messageStr = message.toString();
|
||||
sharedProcessLogger.debug(messageStr);
|
||||
switch (messageStr) {
|
||||
case "handshake:hello":
|
||||
protocol.send(Buffer.from(JSON.stringify({
|
||||
// Using the mount path so if we get a new mount, it spins up a new shared
|
||||
// process since it or the builtin extensions could contain changes.
|
||||
sharedIPCHandle: `/tmp/vscode-shared${mountPath.replace(/\//g, "-")}.sock`,
|
||||
serviceUrl: api.environment.appURL("extensions-api"),
|
||||
logsDir: process.env.VSCODE_LOGS,
|
||||
nodePath,
|
||||
bootstrapForkLocation,
|
||||
args: {},
|
||||
windowId,
|
||||
logLevel: LogLevel.Info,
|
||||
} as ISharedProcessInitData)));
|
||||
break;
|
||||
case "handshake:ready":
|
||||
listener.dispose();
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
protoResolve(protocol);
|
||||
}, client.api, client.wush, mountPromise);
|
||||
|
||||
const { startup, URI } = require('vs/workbench/workbench.main');
|
||||
|
||||
require("os").homedir = () => {
|
||||
// TODO: update this as well as folderURL
|
||||
return "/root";
|
||||
};
|
||||
require("path").posix = require("path");
|
||||
|
||||
registerContextMenuListener();
|
||||
|
||||
const workbenchPromise = client.wrapTask("Start workbench", 1000, async (workspace, mountPath) => {
|
||||
const workbenchShellPromise = startup({
|
||||
machineId: "1",
|
||||
windowId,
|
||||
logLevel: LogLevel.Info,
|
||||
mainPid: 1,
|
||||
appRoot: mountPath,
|
||||
execPath: "/tmp",
|
||||
userEnv: {},
|
||||
nodeCachedDataDir: "/tmp",
|
||||
perfEntries: [],
|
||||
_: undefined,
|
||||
folderUri: URI.file(workspace.mountUri.path),
|
||||
});
|
||||
|
||||
const workbenchShell = await workbenchShellPromise;
|
||||
workbench = workbenchShell.workbench;
|
||||
|
||||
const contextKeys = workbench.workbenchParams.serviceCollection.get(IContextKeyService) as IContextKeyService;
|
||||
const bounded = nativeClipboard.contextKey.bindTo(contextKeys);
|
||||
|
||||
const navigatorClip = (navigator as any).clipboard;
|
||||
const navigatorPerms = (navigator as any).permissions;
|
||||
if (navigatorClip && navigatorPerms) {
|
||||
navigatorPerms.query({
|
||||
name: "clipboard-read",
|
||||
}).then((permissionStatus) => {
|
||||
const updateStatus = () => {
|
||||
if (permissionStatus.state === "denied") {
|
||||
isEnabled = false;
|
||||
clipboardEnabledEmitter.emit(false);
|
||||
bounded.set(false);
|
||||
} else {
|
||||
isEnabled = true;
|
||||
clipboardEnabledEmitter.emit(true);
|
||||
bounded.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
updateStatus();
|
||||
|
||||
permissionStatus.onchange = () => {
|
||||
updateStatus();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const decorations = workbench.workbenchParams.serviceCollection.get(IDecorationsService) as IDecorationsService;
|
||||
await registerCollaboratorDecorations(client, decorations);
|
||||
|
||||
return workbenchShell;
|
||||
}, client.workspace.then((w) => w.connect()), mountPromise, client.mkDirs);
|
||||
|
||||
await workbenchPromise;
|
||||
};
|
Reference in New Issue
Block a user