Archived
1
0

Extension host (#20)

* Implement net.Server

* Move Socket class into Client

This way we don't need to expose anything.

* Remove some unused imports

* Pass environment variables to bootstrap fork

* Add debug log for when socket disconnects from server

* Use VSCODE_ALLOW_IO for shared process only

* Extension host can send messages now

* Support callback for logging

This lets us do potentially expensive operations which will only be
performed if the log level is sufficiently low.

* Stop extension host from committing suicide

* Blank line

* Add static serve (#21)

* Add extension URLs

* how did i remove this

* Fix writing an empty string

* Implement dialogs on window service
This commit is contained in:
Asher
2019-01-25 18:18:21 -06:00
committed by Kyle Carberry
parent e43e7b36e7
commit c6d35d098a
27 changed files with 431 additions and 793 deletions

View File

@ -1,277 +0,0 @@
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);
}
}

View File

@ -1,68 +0,0 @@
.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;
}

View File

@ -1,250 +0,0 @@
/**
* 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));
}
}

View File

@ -0,0 +1,9 @@
class Watchdog {
public start(): void {
// TODO: Should it do something?
}
}
export = new Watchdog();

View File

@ -5,7 +5,11 @@ const product = {
nameLong: "vscode online",
dataFolderName: ".vscode-online",
extensionsGallery: {
serviceUrl: "",
serviceUrl: "https://marketplace.visualstudio.com/_apis/public/gallery",
cacheUrl: "https://vscode.blob.core.windows.net/gallery/index",
itemUrl: "https://marketplace.visualstudio.com/items",
controlUrl: "https://az764295.vo.msecnd.net/extensions/marketplace.json",
recommendationsUrl: "https://az764295.vo.msecnd.net/extensions/workspaceRecommendations.json.gz",
},
extensionExecutionEnvironments: {
"wayou.vscode-todo-highlight": "worker",

View File

@ -51,16 +51,31 @@ class WindowsService implements IWindowsService {
throw new Error("not implemented");
}
public showMessageBox(_windowId: number, _options: MessageBoxOptions): Promise<IMessageBoxResult> {
throw new Error("not implemented");
public showMessageBox(windowId: number, options: MessageBoxOptions): Promise<IMessageBoxResult> {
return new Promise((resolve): void => {
electron.dialog.showMessageBox(this.getWindowById(windowId), options, (response, checkboxChecked) => {
resolve({
button: response,
checkboxChecked,
});
});
});
}
public showSaveDialog(_windowId: number, _options: SaveDialogOptions): Promise<string> {
throw new Error("not implemented");
public showSaveDialog(windowId: number, options: SaveDialogOptions): Promise<string> {
return new Promise((resolve): void => {
electron.dialog.showSaveDialog(this.getWindowById(windowId), options, (filename, _bookmark) => {
resolve(filename);
});
});
}
public showOpenDialog(_windowId: number, _options: OpenDialogOptions): Promise<string[]> {
throw new Error("not implemented");
public showOpenDialog(windowId: number, options: OpenDialogOptions): Promise<string[]> {
return new Promise((resolve): void => {
electron.dialog.showOpenDialog(this.getWindowById(windowId), options, (filePaths, _bookmarks) => {
resolve(filePaths);
});
});
}
public reloadWindow(windowId: number, _args?: ParsedArgs): Promise<void> {

View File

@ -64,6 +64,7 @@ module.exports = (env) => {
"windows-process-tree": path.resolve(fills, "empty.ts"),
"electron": path.join(vscodeFills, "stdioElectron.ts"),
"native-watchdog": path.join(vscodeFills, "native-watchdog.ts"),
"vs/platform/node/product": path.resolve(vscodeFills, "product.ts"),
"vs/platform/node/package": path.resolve(vscodeFills, "package.ts"),
"vs/base/node/paths": path.resolve(vscodeFills, "paths.ts"),