Archived
1
0

not finished

This commit is contained in:
Asher
2019-01-07 18:46:19 -06:00
committed by Kyle Carberry
parent 776bb227e6
commit 9cd81f73fa
79 changed files with 11015 additions and 0 deletions

View File

@ -0,0 +1,4 @@
{
"name": "@coder/vscode",
"description": "VS Code implementation of the browser-based IDE client."
}

View 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);
}
}

View 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;
}

View 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));
}
}

View 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);
});

View 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}");`;
};

View File

@ -0,0 +1,8 @@
module.exports = {
getCurrentKeyboardLayout: (): null => {
return null;
},
getKeyMap: (): undefined[] => {
return [];
},
};

View 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;

View 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;
}
}

View 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;
}
},
});
}

View 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");
}
}
}

View 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);
}
}

View 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;
};