diff options
Diffstat (limited to 'extension')
28 files changed, 666 insertions, 423 deletions
diff --git a/extension/.vscodeignore b/extension/.vscodeignore index f65d24a9..c1a09172 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -9,4 +9,17 @@ vsc-extension-quickstart.md  **/.eslintrc.json  **/*.map  **/*.ts -scripts/env/**
\ No newline at end of file +scripts/env/** +scripts/.env +**/.env +**/env +**/node_modules + +react-app/node_modules +react-app/public +react-app/src + +**/.pytest_cache +**/__pycache__ + +scripts/data
\ No newline at end of file diff --git a/extension/logging.yaml b/extension/logging.yaml new file mode 100644 index 00000000..391041ef --- /dev/null +++ b/extension/logging.yaml @@ -0,0 +1,30 @@ +version: 1 +disable_existing_loggers: False +formatters: +  default: +    (): 'uvicorn.logging.DefaultFormatter' +    fmt: '%(asctime)s %(levelprefix)-9s %(name)s -: %(message)s' +  access: +    (): 'uvicorn.logging.AccessFormatter' +    fmt: '%(asctime)s %(levelprefix)-9s %(name)s -: %(client_addr)s - "%(request_line)s" %(status_code)s' +handlers: +  default: +    class: logging.StreamHandler +    formatter: default +    stream: ext://sys.stderr +  access: +    class: logging.StreamHandler +    formatter: access +    stream: ext://sys.stdout +loggers: +  uvicorn: +    level: INFO +    handlers: +      - default +  uvicorn.error: +    level: INFO +  uvicorn.access: +    level: INFO +    propagate: False +    handlers: +      - access
\ No newline at end of file diff --git a/extension/package-lock.json b/extension/package-lock.json index 822b908e..ef3a619b 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@  {    "name": "continue", -  "version": "0.0.4", +  "version": "0.0.10",    "lockfileVersion": 2,    "requires": true,    "packages": {      "": {        "name": "continue", -      "version": "0.0.4", +      "version": "0.0.10",        "license": "Apache-2.0",        "dependencies": {          "@electron/rebuild": "^3.2.10", @@ -28,6 +28,7 @@          "@types/node": "16.x",          "@types/node-fetch": "^2.6.2",          "@types/vscode": "^1.74.0", +        "@types/ws": "^8.5.4",          "@typescript-eslint/eslint-plugin": "^5.45.0",          "@typescript-eslint/parser": "^5.45.0",          "@vscode/test-electron": "^2.2.0", @@ -2027,6 +2028,15 @@        "integrity": "sha512-LyeCIU3jb9d38w0MXFwta9r0Jx23ugujkAxdwLTNCyspdZTKUc43t7ppPbCiPoQ/Ivd/pnDFZrb4hWd45wrsgA==",        "dev": true      }, +    "node_modules/@types/ws": { +      "version": "8.5.4", +      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", +      "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", +      "dev": true, +      "dependencies": { +        "@types/node": "*" +      } +    },      "node_modules/@typescript-eslint/eslint-plugin": {        "version": "5.48.2",        "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.2.tgz", @@ -9246,6 +9256,15 @@        "integrity": "sha512-LyeCIU3jb9d38w0MXFwta9r0Jx23ugujkAxdwLTNCyspdZTKUc43t7ppPbCiPoQ/Ivd/pnDFZrb4hWd45wrsgA==",        "dev": true      }, +    "@types/ws": { +      "version": "8.5.4", +      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", +      "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", +      "dev": true, +      "requires": { +        "@types/node": "*" +      } +    },      "@typescript-eslint/eslint-plugin": {        "version": "5.48.2",        "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.2.tgz", diff --git a/extension/package.json b/extension/package.json index f5771ee6..6658f77a 100644 --- a/extension/package.json +++ b/extension/package.json @@ -14,7 +14,7 @@    "displayName": "Continue",    "pricing": "Free",    "description": "Refine code 10x faster", -  "version": "0.0.4", +  "version": "0.0.10",    "publisher": "Continue",    "engines": {      "vscode": "^1.74.0" @@ -148,7 +148,7 @@    },    "scripts": {      "vscode:prepublish": "npm run esbuild-base -- --minify", -    "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node", +    "esbuild-base": "rm -rf ./out && esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",      "esbuild": "rm -rf ./out && npm run esbuild-base -- --sourcemap",      "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch",      "test-compile": "tsc -p ./", @@ -160,9 +160,9 @@      "pretest": "npm run compile && npm run lint",      "lint": "eslint src --ext ts",      "test": "node ./out/test/runTest.js", -    "package": "cp ./config/prod_config.json ./config/config.json && mkdir -p ./build && vsce package --out ./build && chmod 777 ./build/continue-0.0.2.vsix && cp ./config/dev_config.json ./config/config.json", +    "package": "cp ./config/prod_config.json ./config/config.json && mkdir -p ./build && vsce package --out ./build && cp ./config/dev_config.json ./config/config.json",      "full-package": "cd ../continuedev && poetry build && cp ./dist/continuedev-0.1.0-py3-none-any.whl ../extension/scripts/continuedev-0.1.0-py3-none-any.whl && cd ../extension && npm run typegen && npm run clientgen && cd react-app && npm run build && cd .. && npm run package", -    "install-extension": "code --install-extension ./build/continue-0.0.1.vsix", +    "install-extension": "code --install-extension ./build/continue-0.0.8.vsix",      "uninstall": "code --uninstall-extension .continue",      "reinstall": "rm -rf ./build && npm run package && npm run uninstall && npm run install-extension"    }, @@ -173,6 +173,7 @@      "@types/node": "16.x",      "@types/node-fetch": "^2.6.2",      "@types/vscode": "^1.74.0", +    "@types/ws": "^8.5.4",      "@typescript-eslint/eslint-plugin": "^5.45.0",      "@typescript-eslint/parser": "^5.45.0",      "@vscode/test-electron": "^2.2.0", diff --git a/extension/react-app/src/components/CodeBlock.tsx b/extension/react-app/src/components/CodeBlock.tsx index 4c10ab23..e0336554 100644 --- a/extension/react-app/src/components/CodeBlock.tsx +++ b/extension/react-app/src/components/CodeBlock.tsx @@ -6,7 +6,8 @@ import { defaultBorderRadius, vscBackground } from ".";  import { Clipboard } from "@styled-icons/heroicons-outline";  const StyledPre = styled.pre` -  overflow: scroll; +  overflow-y: scroll; +  word-wrap: normal;    border: 1px solid gray;    border-radius: ${defaultBorderRadius};    background-color: ${vscBackground}; diff --git a/extension/react-app/src/components/DebugPanel.tsx b/extension/react-app/src/components/DebugPanel.tsx index ed00571b..9dacc624 100644 --- a/extension/react-app/src/components/DebugPanel.tsx +++ b/extension/react-app/src/components/DebugPanel.tsx @@ -36,7 +36,8 @@ const GradientContainer = styled.div`  const MainDiv = styled.div`    height: 100%;    border-radius: ${defaultBorderRadius}; -  overflow: scroll; +  overflow-y: scroll; +  scrollbar-gutter: stable both-edges;    scrollbar-base-color: transparent;    /* background: ${vscBackground}; */    background-color: #1e1e1ede; @@ -107,6 +108,7 @@ function DebugPanel(props: DebugPanelProps) {                  className={                    tab.title === "Chat" ? "overflow-hidden" : "overflow-scroll"                  } +                style={{ scrollbarGutter: "stable both-edges" }}                >                  {tab.element}                </div> diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index 03649b66..5e979b34 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -36,6 +36,8 @@ const MainDiv = styled.div<{ stepDepth: number; inFuture: boolean }>`    animation: ${appear} 0.3s ease-in-out;    /* padding-left: ${(props) => props.stepDepth * 20}px; */    overflow: hidden; +  margin-left: 0px; +  margin-right: 0px;  `;  const StepContainerDiv = styled.div<{ open: boolean }>` @@ -78,6 +80,13 @@ function StepContainer(props: StepContainerProps) {    const [open, setOpen] = useState(false);    const [isHovered, setIsHovered] = useState(false);    const naturalLanguageInputRef = useRef<HTMLTextAreaElement>(null); +  const userInputRef = useRef<HTMLInputElement>(null); + +  useEffect(() => { +    if (userInputRef?.current) { +      userInputRef.current.focus(); +    } +  }, [userInputRef]);    useEffect(() => {      if (isHovered) { @@ -134,6 +143,7 @@ function StepContainer(props: StepContainerProps) {            {props.historyNode.step.name === "Waiting for user input" && (              <input +              ref={userInputRef}                className="m-auto p-2 rounded-md border-1 border-solid text-white w-3/4 border-gray-200 bg-vsc-background"                onKeyDown={(e) => {                  if (e.key === "Enter") { @@ -144,6 +154,9 @@ function StepContainer(props: StepContainerProps) {                onSubmit={(ev) => {                  props.onUserInput(ev.currentTarget.value);                }} +              onClick={(e) => { +                e.stopPropagation(); +              }}              />            )}            {props.historyNode.step.name === "Waiting for user confirmation" && ( @@ -165,24 +178,6 @@ function StepContainer(props: StepContainerProps) {                />              </>            )} - -          {open && ( -            <> -              {/* {props.historyNode.observation && ( -                <SubContainer title="Error"> -                  <CodeBlock>Error Here</CodeBlock> -                </SubContainer> -              )} */} -              {/* {props.iterationContext.suggestedChanges.map((sc) => { -              return ( -                <SubContainer title="Suggested Change"> -                  {sc.filepath} -                  <CodeBlock>{sc.replacement}</CodeBlock> -                </SubContainer> -              ); -            })} */} -            </> -          )}          </StepContainerDiv>        </GradientBorder> diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts index e37c97f3..7ba60467 100644 --- a/extension/react-app/src/components/index.ts +++ b/extension/react-app/src/components/index.ts @@ -45,7 +45,7 @@ export const Pre = styled.pre`    border-radius: ${defaultBorderRadius};    padding: 8px;    max-height: 150px; -  overflow: scroll; +  overflow-y: scroll;    margin: 0;    background-color: ${secondaryDark};    border: none; diff --git a/extension/react-app/src/hooks/ContinueNotebookClientProtocol.ts b/extension/react-app/src/hooks/ContinueNotebookClientProtocol.ts new file mode 100644 index 00000000..75fd7373 --- /dev/null +++ b/extension/react-app/src/hooks/ContinueNotebookClientProtocol.ts @@ -0,0 +1,13 @@ +abstract class AbstractContinueNotebookClientProtocol { +  abstract sendMainInput(input: string): void; + +  abstract reverseToIndex(index: number): void; + +  abstract sendRefinementInput(input: string, index: number): void; + +  abstract sendStepUserInput(input: string, index: number): void; + +  abstract onStateUpdate(state: any): void; +} + +export default AbstractContinueNotebookClientProtocol; diff --git a/extension/react-app/src/hooks/messenger.ts b/extension/react-app/src/hooks/messenger.ts new file mode 100644 index 00000000..e2a0bab8 --- /dev/null +++ b/extension/react-app/src/hooks/messenger.ts @@ -0,0 +1,108 @@ +// console.log("Websocket import"); +// const WebSocket = require("ws"); + +export abstract class Messenger { +  abstract send(messageType: string, data: object): void; + +  abstract onMessageType( +    messageType: string, +    callback: (data: object) => void +  ): void; + +  abstract onMessage(callback: (messageType: string, data: any) => void): void; + +  abstract onOpen(callback: () => void): void; + +  abstract onClose(callback: () => void): void; + +  abstract sendAndReceive(messageType: string, data: any): Promise<any>; +} + +export class WebsocketMessenger extends Messenger { +  websocket: WebSocket; +  private onMessageListeners: { +    [messageType: string]: ((data: object) => void)[]; +  } = {}; +  private onOpenListeners: (() => void)[] = []; +  private onCloseListeners: (() => void)[] = []; +  private serverUrl: string; + +  _newWebsocket(): WebSocket { +    // // Dynamic import, because WebSocket is builtin with browser, but not with node. And can't use require in browser. +    // if (typeof process === "object") { +    //   console.log("Using node"); +    //   // process is only available in Node +    //   var WebSocket = require("ws"); +    // } + +    const newWebsocket = new WebSocket(this.serverUrl); +    for (const listener of this.onOpenListeners) { +      this.onOpen(listener); +    } +    for (const listener of this.onCloseListeners) { +      this.onClose(listener); +    } +    for (const messageType in this.onMessageListeners) { +      for (const listener of this.onMessageListeners[messageType]) { +        this.onMessageType(messageType, listener); +      } +    } +    return newWebsocket; +  } + +  constructor(serverUrl: string) { +    super(); +    this.serverUrl = serverUrl; +    this.websocket = this._newWebsocket(); +  } + +  send(messageType: string, data: object) { +    const payload = JSON.stringify({ messageType, data }); +    if (this.websocket.readyState === this.websocket.OPEN) { +      this.websocket.send(payload); +    } else { +      if (this.websocket.readyState !== this.websocket.CONNECTING) { +        this.websocket = this._newWebsocket(); +      } +      this.websocket.addEventListener("open", () => { +        this.websocket.send(payload); +      }); +    } +  } + +  sendAndReceive(messageType: string, data: any): Promise<any> { +    return new Promise((resolve, reject) => { +      const eventListener = (data: any) => { +        // THIS ISN"T GETTING CALLED +        resolve(data); +        this.websocket.removeEventListener("message", eventListener); +      }; +      this.onMessageType(messageType, eventListener); +      this.send(messageType, data); +    }); +  } + +  onMessageType(messageType: string, callback: (data: any) => void): void { +    this.websocket.addEventListener("message", (event: any) => { +      const msg = JSON.parse(event.data); +      if (msg.messageType === messageType) { +        callback(msg.data); +      } +    }); +  } + +  onMessage(callback: (messageType: string, data: any) => void): void { +    this.websocket.addEventListener("message", (event) => { +      const msg = JSON.parse(event.data); +      callback(msg.messageType, msg.data); +    }); +  } + +  onOpen(callback: () => void): void { +    this.websocket.addEventListener("open", callback); +  } + +  onClose(callback: () => void): void { +    this.websocket.addEventListener("close", callback); +  } +} diff --git a/extension/react-app/src/hooks/useContinueNotebookProtocol.ts b/extension/react-app/src/hooks/useContinueNotebookProtocol.ts new file mode 100644 index 00000000..b785cc84 --- /dev/null +++ b/extension/react-app/src/hooks/useContinueNotebookProtocol.ts @@ -0,0 +1,49 @@ +import AbstractContinueNotebookClientProtocol from "./ContinueNotebookClientProtocol"; +// import { Messenger, WebsocketMessenger } from "../../../src/util/messenger"; +import { Messenger, WebsocketMessenger } from "./messenger"; +import { VscodeMessenger } from "./vscodeMessenger"; + +class ContinueNotebookClientProtocol extends AbstractContinueNotebookClientProtocol { +  messenger: Messenger; +  // Server URL must contain the session ID param +  serverUrlWithSessionId: string; + +  constructor( +    serverUrlWithSessionId: string, +    useVscodeMessagePassing: boolean +  ) { +    super(); +    this.serverUrlWithSessionId = serverUrlWithSessionId; +    if (useVscodeMessagePassing) { +      this.messenger = new VscodeMessenger(serverUrlWithSessionId); +    } else { +      this.messenger = new WebsocketMessenger(serverUrlWithSessionId); +    } +  } + +  sendMainInput(input: string) { +    this.messenger.send("main_input", { input }); +  } + +  reverseToIndex(index: number) { +    this.messenger.send("reverse_to_index", { index }); +  } + +  sendRefinementInput(input: string, index: number) { +    this.messenger.send("refinement_input", { input, index }); +  } + +  sendStepUserInput(input: string, index: number) { +    this.messenger.send("step_user_input", { input, index }); +  } + +  onStateUpdate(callback: (state: any) => void) { +    this.messenger.onMessageType("state_update", (data: any) => { +      if (data.state) { +        callback(data.state); +      } +    }); +  } +} + +export default ContinueNotebookClientProtocol; diff --git a/extension/react-app/src/hooks/useWebsocket.ts b/extension/react-app/src/hooks/useWebsocket.ts index 6e8e68fa..016fa17d 100644 --- a/extension/react-app/src/hooks/useWebsocket.ts +++ b/extension/react-app/src/hooks/useWebsocket.ts @@ -1,158 +1,39 @@  import React, { useEffect, useState } from "react";  import { RootStore } from "../redux/store";  import { useSelector } from "react-redux"; +import ContinueNotebookClientProtocol from "./useContinueNotebookProtocol";  import { postVscMessage } from "../vscode"; -abstract class Messenger { -  abstract send(data: string): void; -} - -class VscodeMessenger extends Messenger { -  url: string; - -  constructor( -    url: string, -    onMessage: (message: { data: any }) => void, -    onOpen: (messenger: Messenger) => void, -    onClose: (messenger: Messenger) => void -  ) { -    super(); -    this.url = url; -    window.addEventListener("message", (event: any) => { -      switch (event.data.type) { -        case "websocketForwardingMessage": -          onMessage(event.data); -          break; -        case "websocketForwardingOpen": -          onOpen(this); -          break; -        case "websocketForwardingClose": -          onClose(this); -          break; -      } -    }); - -    postVscMessage("websocketForwardingOpen", { url: this.url }); -  } - -  send(data: string) { -    postVscMessage("websocketForwardingMessage", { -      message: data, -      url: this.url, -    }); -  } -} - -class WebsocketMessenger extends Messenger { -  websocket: WebSocket; -  constructor( -    websocket: WebSocket, -    onMessage: (message: { data: any }) => void, -    onOpen: (messenger: Messenger) => void, -    onClose: (messenger: Messenger) => void -  ) { -    super(); -    this.websocket = websocket; - -    websocket.addEventListener("close", () => { -      onClose(this); -    }); - -    websocket.addEventListener("open", () => { -      onOpen(this); -    }); - -    websocket.addEventListener("message", (event) => { -      onMessage(event.data); -    }); -  } - -  static async connect( -    url: string, -    sessionId: string, -    onMessage: (message: { data: any }) => void, -    onOpen: (messenger: Messenger) => void, -    onClose: (messenger: Messenger) => void -  ): Promise<WebsocketMessenger> { -    const ws = new WebSocket(url); - -    return new Promise((resolve, reject) => { -      ws.addEventListener("open", () => { -        resolve(new WebsocketMessenger(ws, onMessage, onOpen, onClose)); -      }); -    }); -  } - -  send(data: string) { -    this.websocket.send(JSON.stringify(data)); -  } -} - -function useContinueWebsocket( -  serverUrl: string, -  onMessage: (message: { data: any }) => void, -  useVscodeMessagePassing: boolean = true -) { +function useContinueNotebookProtocol(useVscodeMessagePassing: boolean = true) {    const sessionId = useSelector((state: RootStore) => state.config.sessionId); -  const [websocket, setWebsocket] = useState<Messenger | undefined>(undefined); +  const serverHttpUrl = useSelector((state: RootStore) => state.config.apiUrl); +  const [client, setClient] = useState< +    ContinueNotebookClientProtocol | undefined +  >(undefined); -  async function connect() { -    while (!sessionId) { -      await new Promise((resolve) => setTimeout(resolve, 300)); +  useEffect(() => { +    if (!sessionId || !serverHttpUrl) { +      if (useVscodeMessagePassing) { +        postVscMessage("onLoad", {}); +      } +      setClient(undefined); +      return;      } -    console.log("Creating websocket", sessionId); -    console.log("Using vscode message passing", useVscodeMessagePassing); - -    const onClose = (messenger: Messenger) => { -      console.log("Websocket closed"); -      setWebsocket(undefined); -    }; - -    const onOpen = (messenger: Messenger) => { -      console.log("Websocket opened"); -      messenger.send(JSON.stringify({ sessionId })); -    }; - -    const url = -      serverUrl.replace("http", "ws") + +    const serverUrlWithSessionId = +      serverHttpUrl.replace("http", "ws") +        "/notebook/ws?session_id=" +        encodeURIComponent(sessionId); -    const messenger: Messenger = useVscodeMessagePassing -      ? new VscodeMessenger(url, onMessage, onOpen, onClose) -      : await WebsocketMessenger.connect( -          url, -          sessionId, -          onMessage, -          onOpen, -          onClose -        ); - -    setWebsocket(messenger); - -    return messenger; -  } - -  async function getConnection() { -    if (!websocket) { -      return await connect(); -    } -    return websocket; -  } - -  async function send(message: object) { -    let ws = await getConnection(); -    ws.send(JSON.stringify(message)); -  } - -  useEffect(() => { -    if (!sessionId) { -      return; -    } -    connect(); -  }, [sessionId]); - -  return { send }; +    console.log("Creating websocket", serverUrlWithSessionId); +    console.log("Using vscode message passing", useVscodeMessagePassing); +    const newClient = new ContinueNotebookClientProtocol( +      serverUrlWithSessionId, +      useVscodeMessagePassing +    ); +    setClient(newClient); +  }, [sessionId, serverHttpUrl]); + +  return client;  } -export default useContinueWebsocket; +export default useContinueNotebookProtocol; diff --git a/extension/react-app/src/hooks/vscodeMessenger.ts b/extension/react-app/src/hooks/vscodeMessenger.ts new file mode 100644 index 00000000..ba19586b --- /dev/null +++ b/extension/react-app/src/hooks/vscodeMessenger.ts @@ -0,0 +1,71 @@ +import { postVscMessage } from "../vscode"; +// import { Messenger } from "../../../src/util/messenger"; +import { Messenger } from "./messenger"; + +export class VscodeMessenger extends Messenger { +  serverUrl: string; + +  constructor(serverUrl: string) { +    super(); +    this.serverUrl = serverUrl; +    postVscMessage("websocketForwardingOpen", { url: this.serverUrl }); +  } + +  send(messageType: string, data: object) { +    postVscMessage("websocketForwardingMessage", { +      message: { messageType, data }, +      url: this.serverUrl, +    }); +  } + +  onMessageType(messageType: string, callback: (data: object) => void): void { +    window.addEventListener("message", (event: any) => { +      if (event.data.type === "websocketForwardingMessage") { +        const data = JSON.parse(event.data.data); +        if (data.messageType === messageType) { +          callback(data.data); +        } +      } +    }); +  } + +  onMessage(callback: (messageType: string, data: any) => void): void { +    window.addEventListener("message", (event: any) => { +      if (event.data.type === "websocketForwardingMessage") { +        const data = JSON.parse(event.data.data); +        callback(data.messageType, data.data); +      } +    }); +  } + +  sendAndReceive(messageType: string, data: any): Promise<any> { +    return new Promise((resolve) => { +      const handler = (event: any) => { +        if (event.data.type === "websocketForwardingMessage") { +          const data = JSON.parse(event.data.data); +          if (data.messageType === messageType) { +            window.removeEventListener("message", handler); +            resolve(data.data); +          } +        } +      }; +      window.addEventListener("message", handler); +      this.send(messageType, data); +    }); +  } + +  onOpen(callback: () => void): void { +    window.addEventListener("message", (event: any) => { +      if (event.data.type === "websocketForwardingOpen") { +        callback(); +      } +    }); +  } +  onClose(callback: () => void): void { +    window.addEventListener("message", (event: any) => { +      if (event.data.type === "websocketForwardingClose") { +        callback(); +      } +    }); +  } +} diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css index dd38eec3..20599d30 100644 --- a/extension/react-app/src/index.css +++ b/extension/react-app/src/index.css @@ -21,7 +21,7 @@  html,  body,  #root { -  height: calc(100% - 7px); +  height: calc(100%);  }  body { diff --git a/extension/react-app/src/tabs/chat/MessageDiv.tsx b/extension/react-app/src/tabs/chat/MessageDiv.tsx index ab632220..ad81f5e9 100644 --- a/extension/react-app/src/tabs/chat/MessageDiv.tsx +++ b/extension/react-app/src/tabs/chat/MessageDiv.tsx @@ -20,7 +20,8 @@ const Container = styled.div`    margin: 3px;    width: fit-content;    max-width: 75%; -  overflow: scroll; +  overflow-y: scroll; +  scrollbar-gutter: stable both-edges;    word-wrap: break-word;    -ms-word-wrap: break-word;    height: fit-content; diff --git a/extension/react-app/src/tabs/notebook.tsx b/extension/react-app/src/tabs/notebook.tsx index a9c69c5b..02c9ff31 100644 --- a/extension/react-app/src/tabs/notebook.tsx +++ b/extension/react-app/src/tabs/notebook.tsx @@ -14,6 +14,7 @@ import StepContainer from "../components/StepContainer";  import { useSelector } from "react-redux";  import { RootStore } from "../redux/store";  import useContinueWebsocket from "../hooks/useWebsocket"; +import useContinueNotebookProtocol from "../hooks/useWebsocket";  let TopNotebookDiv = styled.div`    display: grid; @@ -33,8 +34,6 @@ interface NotebookProps {  }  function Notebook(props: NotebookProps) { -  const serverUrl = useSelector((state: RootStore) => state.config.apiUrl); -    const [waitingForSteps, setWaitingForSteps] = useState(false);    const [userInputQueue, setUserInputQueue] = useState<string[]>([]);    const [history, setHistory] = useState<History | undefined>(); @@ -157,30 +156,17 @@ function Notebook(props: NotebookProps) {    // } as any    // ); -  const { send: websocketSend } = useContinueWebsocket(serverUrl, (msg) => { -    let data = JSON.parse(msg.data); -    if (data.messageType === "state") { -      setWaitingForSteps(data.state.active); -      setHistory(data.state.history); -      setUserInputQueue(data.state.user_input_queue); -    } -  }); +  const client = useContinueNotebookProtocol(); -  // useEffect(() => { -  //   (async () => { -  //     if (sessionId && props.firstObservation) { -  //       let resp = await fetch(serverUrl + "/observation", { -  //         method: "POST", -  //         headers: new Headers({ -  //           "x-continue-session-id": sessionId, -  //         }), -  //         body: JSON.stringify({ -  //           observation: props.firstObservation, -  //         }), -  //       }); -  //     } -  //   })(); -  // }, [props.firstObservation]); +  useEffect(() => { +    console.log("CLIENT ON STATE UPDATE: ", client, client?.onStateUpdate); +    client?.onStateUpdate((state) => { +      console.log("Received state update: ", state); +      setWaitingForSteps(state.active); +      setHistory(state.history); +      setUserInputQueue(state.user_input_queue); +    }); +  }, [client]);    const mainTextInputRef = useRef<HTMLTextAreaElement>(null); @@ -201,14 +187,12 @@ function Notebook(props: NotebookProps) {    const onMainTextInput = () => {      if (mainTextInputRef.current) { -      let value = mainTextInputRef.current.value; +      if (!client) return; +      let input = mainTextInputRef.current.value;        setWaitingForSteps(true); -      websocketSend({ -        messageType: "main_input", -        value: value, -      }); +      client.sendMainInput(input);        setUserInputQueue((queue) => { -        return [...queue, value]; +        return [...queue, input];        });        mainTextInputRef.current.value = "";        mainTextInputRef.current.style.height = ""; @@ -216,17 +200,20 @@ function Notebook(props: NotebookProps) {    };    const onStepUserInput = (input: string, index: number) => { +    if (!client) return;      console.log("Sending step user input", input, index); -    websocketSend({ -      messageType: "step_user_input", -      value: input, -      index, -    }); +    client.sendStepUserInput(input, index);    };    // const iterations = useSelector(selectIterations);    return (      <TopNotebookDiv> +      {typeof client === "undefined" && ( +        <> +          <Loader></Loader> +          <p>Server disconnected</p> +        </> +      )}        {history?.timeline.map((node: HistoryNode, index: number) => {          return (            <StepContainer @@ -237,17 +224,10 @@ function Notebook(props: NotebookProps) {              inFuture={index > history?.current_index}              historyNode={node}              onRefinement={(input: string) => { -              websocketSend({ -                messageType: "refinement_input", -                value: input, -                index, -              }); +              client?.sendRefinementInput(input, index);              }}              onReverse={() => { -              websocketSend({ -                messageType: "reverse", -                index, -              }); +              client?.reverseToIndex(index);              }}            />          ); diff --git a/extension/react-app/src/vscode/index.ts b/extension/react-app/src/vscode/index.ts index 7e373cd9..0785aa4d 100644 --- a/extension/react-app/src/vscode/index.ts +++ b/extension/react-app/src/vscode/index.ts @@ -5,6 +5,7 @@ declare const vscode: any;  export function postVscMessage(type: string, data: any) {    if (typeof vscode === "undefined") { +    console.log("Unable to send message: vscode is undefined");      return;    }    vscode.postMessage({ diff --git a/extension/react-app/tsconfig.json b/extension/react-app/tsconfig.json index 3d0a51a8..940a9359 100644 --- a/extension/react-app/tsconfig.json +++ b/extension/react-app/tsconfig.json @@ -16,6 +16,6 @@      "noEmit": true,      "jsx": "react-jsx"    }, -  "include": ["src"], +  "include": ["src", "../src/util/messenger.ts"],    "references": [{ "path": "./tsconfig.node.json" }]  } diff --git a/extension/scripts/continuedev-0.1.0-py3-none-any.whl b/extension/scripts/continuedev-0.1.0-py3-none-any.whl Binary files differindex d1483db9..6cda6f2d 100644 --- a/extension/scripts/continuedev-0.1.0-py3-none-any.whl +++ b/extension/scripts/continuedev-0.1.0-py3-none-any.whl diff --git a/extension/scripts/install_from_source.py b/extension/scripts/install_from_source.py index 28f382b0..bbb86797 100644 --- a/extension/scripts/install_from_source.py +++ b/extension/scripts/install_from_source.py @@ -29,7 +29,7 @@ def main():          print("Poetry is required for Continue but is not installed on your machine. See https://python-poetry.org/docs/#installation to download the latest version, then try again.")          return -    resp = run("cd ../../continuedev; poetry run typegen") +    resp = run("cd ../../continuedev; poetry install; poetry run typegen")      resp = run(          "cd ..; npm i; cd react-app; npm i; cd ..; npm run full-package") diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts index a0aa560b..f8f3c65a 100644 --- a/extension/src/activation/activate.ts +++ b/extension/src/activation/activate.ts @@ -10,7 +10,7 @@ import { getContinueServerUrl } from "../bridge";  export let extensionContext: vscode.ExtensionContext | undefined = undefined; -export let ideProtocolClient: IdeProtocolClient | undefined = undefined; +export let ideProtocolClient: IdeProtocolClient;  export function activateExtension(    context: vscode.ExtensionContext, @@ -24,7 +24,7 @@ export function activateExtension(    let serverUrl = getContinueServerUrl();    ideProtocolClient = new IdeProtocolClient( -    serverUrl.replace("http", "ws") + "/ide/ws", +    `${serverUrl.replace("http", "ws")}/ide/ws`,      context    ); @@ -62,9 +62,9 @@ export function activateExtension(        ideProtocolClient?.openNotebook();      });    } else { -    // ideProtocolClient?.openNotebook().then(() => { -    //   // openCapturedTerminal(); -    // }); +    ideProtocolClient.openNotebook().then(() => { +      // openCapturedTerminal(); +    });    }    extensionContext = context; diff --git a/extension/src/activation/environmentSetup.ts b/extension/src/activation/environmentSetup.ts index 93a471ff..170426e1 100644 --- a/extension/src/activation/environmentSetup.ts +++ b/extension/src/activation/environmentSetup.ts @@ -10,6 +10,7 @@ import { getContinueServerUrl } from "../bridge";  import fetch from "node-fetch";  async function runCommand(cmd: string): Promise<[string, string | undefined]> { +  console.log("Running command: ", cmd);    var stdout: any = "";    var stderr: any = "";    try { @@ -28,18 +29,7 @@ async function runCommand(cmd: string): Promise<[string, string | undefined]> {    return [stdout, stderr];  } -async function getPythonCmdAssumingInstalled() { -  const [, stderr] = await runCommand("python3 --version"); -  if (stderr) { -    return "python"; -  } -  return "python3"; -} - -async function setupPythonEnv() { -  console.log("Setting up python env for Continue extension..."); -  // First check that python3 is installed - +async function getPythonPipCommands() {    var [stdout, stderr] = await runCommand("python3 --version");    let pythonCmd = "python3";    if (stderr) { @@ -58,28 +48,81 @@ async function setupPythonEnv() {      }    }    let pipCmd = pythonCmd.endsWith("3") ? "pip3" : "pip"; +  return [pythonCmd, pipCmd]; +} +function getActivateUpgradeCommands(pythonCmd: string, pipCmd: string) {    let activateCmd = ". env/bin/activate";    let pipUpgradeCmd = `${pipCmd} install --upgrade pip`;    if (process.platform == "win32") {      activateCmd = ".\\env\\Scripts\\activate";      pipUpgradeCmd = `${pythonCmd} -m pip install --upgrade pip`;    } +  return [activateCmd, pipUpgradeCmd]; +} -  let command = `cd ${path.join( +function checkEnvExists() { +  const envBinPath = path.join(      getExtensionUri().fsPath, -    "scripts" -  )} && ${pythonCmd} -m venv env && ${activateCmd} && ${pipUpgradeCmd} && ${pipCmd} install -r requirements.txt`; -  var [stdout, stderr] = await runCommand(command); -  if (stderr) { -    throw new Error(stderr); +    "scripts", +    "env", +    process.platform == "win32" ? "Scripts" : "bin" +  ); +  return ( +    fs.existsSync(path.join(envBinPath, "activate")) && +    fs.existsSync(path.join(envBinPath, "pip")) +  ); +} + +async function setupPythonEnv() { +  console.log("Setting up python env for Continue extension..."); + +  if (checkEnvExists()) return; + +  // Assemble the command to create the env +  const [pythonCmd, pipCmd] = await getPythonPipCommands(); +  const [activateCmd, pipUpgradeCmd] = getActivateUpgradeCommands( +    pythonCmd, +    pipCmd +  ); +  const createEnvCommand = [ +    `cd ${path.join(getExtensionUri().fsPath, "scripts")}`, +    `${pythonCmd} -m venv env`, +  ].join(" && "); + +  // Repeat until it is successfully created (sometimes it fails to generate the bin, need to try again) +  while (true) { +    const [, stderr] = await runCommand(createEnvCommand); +    if (stderr) { +      throw new Error(stderr); +    } +    if (checkEnvExists()) { +      break; +    } else { +      // Remove the env and try again +      const removeCommand = `rm -rf ${path.join( +        getExtensionUri().fsPath, +        "scripts", +        "env" +      )}`; +      await runCommand(removeCommand); +    }    }    console.log(      "Successfully set up python env at ",      getExtensionUri().fsPath + "/scripts/env"    ); -  await startContinuePythonServer(); +  const installRequirementsCommand = [ +    `cd ${path.join(getExtensionUri().fsPath, "scripts")}`, +    activateCmd, +    pipUpgradeCmd, +    `${pipCmd} install -r requirements.txt`, +  ].join(" && "); +  const [, stderr] = await runCommand(installRequirementsCommand); +  if (stderr) { +    throw new Error(stderr); +  }  }  function readEnvFile(path: string) { @@ -116,29 +159,19 @@ function writeEnvFile(path: string, key: string, value: string) {  }  export async function startContinuePythonServer() { +  await setupPythonEnv(); +    // Check vscode settings    let serverUrl = getContinueServerUrl();    if (serverUrl !== "http://localhost:8000") {      return;    } -  let envFile = path.join(getExtensionUri().fsPath, "scripts", ".env"); -  let openai_api_key: string | undefined = -    readEnvFile(envFile)["OPENAI_API_KEY"]; -  while (typeof openai_api_key === "undefined" || openai_api_key === "") { -    openai_api_key = await vscode.window.showInputBox({ -      prompt: "Enter your OpenAI API key", -      placeHolder: "Enter your OpenAI API key", -    }); -    // Write to .env file -  } -  writeEnvFile(envFile, "OPENAI_API_KEY", openai_api_key); -    console.log("Starting Continue python server...");    // Check if already running by calling /health    try { -    let response = await fetch(serverUrl + "/health"); +    const response = await fetch(serverUrl + "/health");      if (response.status === 200) {        console.log("Continue python server already running");        return; @@ -156,27 +189,30 @@ export async function startContinuePythonServer() {      getExtensionUri().fsPath,      "scripts"    )} && ${activateCmd} && cd .. && ${pythonCmd} -m scripts.run_continue_server`; -  try { -    // exec(command); -    let child = spawn(command, { -      shell: true, -      detached: true, -    }); -    child.stdout.on("data", (data: any) => { -      console.log(`stdout: ${data}`); -    }); -    child.stderr.on("data", (data: any) => { -      console.log(`stderr: ${data}`); -    }); -    child.on("error", (error: any) => { -      console.log(`error: ${error.message}`); -    }); -  } catch (e) { -    console.log("Failed to start Continue python server", e); -  } -  // Sleep for 3 seconds to give the server time to start -  await new Promise((resolve) => setTimeout(resolve, 3000)); -  console.log("Successfully started Continue python server"); + +  return new Promise((resolve, reject) => { +    try { +      const child = spawn(command, { +        shell: true, +      }); +      child.stdout.on("data", (data: any) => { +        console.log(`stdout: ${data}`); +      }); +      child.stderr.on("data", (data: any) => { +        console.log(`stderr: ${data}`); +        if (data.includes("Uvicorn running on")) { +          console.log("Successfully started Continue python server"); +          resolve(null); +        } +      }); +      child.on("error", (error: any) => { +        console.log(`error: ${error.message}`); +      }); +    } catch (e) { +      console.log("Failed to start Continue python server", e); +      reject(); +    } +  });  }  async function installNodeModules() { @@ -194,11 +230,6 @@ export function isPythonEnvSetup(): boolean {    return fs.existsSync(path.join(pathToEnvCfg));  } -export async function setupExtensionEnvironment() { -  console.log("Setting up environment for Continue extension..."); -  await Promise.all([setupPythonEnv()]); -} -  export async function downloadPython3() {    // Download python3 and return the command to run it (python or python3)    let os = process.platform; diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 18f08e31..aeeb4b4f 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -62,11 +62,11 @@ const commandsMap: { [command: string]: (...args: any) => any } = {    "continue.acceptSuggestion": acceptSuggestionCommand,    "continue.rejectSuggestion": rejectSuggestionCommand,    "continue.openDebugPanel": () => { -    ideProtocolClient?.openNotebook(); +    ideProtocolClient.openNotebook();    },    "continue.focusContinueInput": async () => {      if (!debugPanelWebview) { -      await ideProtocolClient?.openNotebook(); +      await ideProtocolClient.openNotebook();      }      debugPanelWebview?.postMessage({        type: "focusContinueInput", diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index 35eb668d..477d1420 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -10,30 +10,28 @@ import {  } from "./suggestions";  import { debugPanelWebview, setupDebugPanel } from "./debugPanel";  import { FileEditWithFullContents } from "../schema/FileEditWithFullContents"; -const util = require("util"); -const exec = util.promisify(require("child_process").exec); -const WebSocket = require("ws");  import fs = require("fs"); +import { WebsocketMessenger } from "./util/messenger";  class IdeProtocolClient { -  private _ws: WebSocket | null = null; -  private _panels: Map<string, vscode.WebviewPanel> = new Map(); -  private readonly _serverUrl: string; -  private readonly _context: vscode.ExtensionContext; +  private messenger: WebsocketMessenger | null = null; +  private panels: Map<string, vscode.WebviewPanel> = new Map(); +  private readonly context: vscode.ExtensionContext;    private _makingEdit = 0;    constructor(serverUrl: string, context: vscode.ExtensionContext) { -    this._context = context; -    this._serverUrl = serverUrl; -    let ws = new WebSocket(serverUrl); -    this._ws = ws; -    ws.onclose = () => { -      this._ws = null; -    }; -    ws.on("message", (data: any) => { -      this.handleMessage(JSON.parse(data)); +    this.context = context; + +    let messenger = new WebsocketMessenger(serverUrl); +    this.messenger = messenger; +    messenger.onClose(() => { +      this.messenger = null; +    }); +    messenger.onMessage((messageType, data) => { +      this.handleMessage(messageType, data);      }); +      // Setup listeners for any file changes in open editors      vscode.workspace.onDidChangeTextDocument((event) => {        if (this._makingEdit === 0) { @@ -58,125 +56,52 @@ class IdeProtocolClient {              };            }          ); -        this.send("fileEdits", { fileEdits }); +        this.messenger?.send("fileEdits", { fileEdits });        } else {          this._makingEdit--;        }      });    } -  async isConnected() { -    if (this._ws === null || this._ws.readyState !== WebSocket.OPEN) { -      let ws = new WebSocket(this._serverUrl); -      ws.onclose = () => { -        this._ws = null; -      }; -      ws.on("message", (data: any) => { -        this.handleMessage(JSON.parse(data)); -      }); -      this._ws = ws; - -      return new Promise((resolve, reject) => { -        ws.addEventListener("open", () => { -          resolve(null); -        }); -      }); -    } -  } - -  async startCore() { -    var { stdout, stderr } = await exec( -      "cd /Users/natesesti/Desktop/continue/continue && poetry shell" -    ); -    if (stderr) { -      throw new Error(stderr); -    } -    var { stdout, stderr } = await exec( -      "cd .. && uvicorn continue.src.server.main:app --reload --reload-dir continue" -    ); -    if (stderr) { -      throw new Error(stderr); -    } -    var { stdout, stderr } = await exec("python3 -m continue.src.libs.ide"); -    if (stderr) { -      throw new Error(stderr); -    } -  } - -  async send(messageType: string, data: object) { -    await this.isConnected(); -    let msg = JSON.stringify({ messageType, ...data }); -    this._ws!.send(msg); -    console.log("Sent message", msg); -  } - -  async receiveMessage(messageType: string): Promise<any> { -    await this.isConnected(); -    console.log("Connected to websocket"); -    return await new Promise((resolve, reject) => { -      if (!this._ws) { -        reject("Not connected to websocket"); -      } -      this._ws!.onmessage = (event: any) => { -        let message = JSON.parse(event.data); -        console.log("RECEIVED MESSAGE", message); -        if (message.messageType === messageType) { -          resolve(message); -        } -      }; -    }); -  } - -  async sendAndReceive(message: any, messageType: string): Promise<any> { -    try { -      await this.send(messageType, message); -      let msg = await this.receiveMessage(messageType); -      console.log("Received message", msg); -      return msg; -    } catch (e) { -      console.log("Error sending message", e); -    } -  } - -  async handleMessage(message: any) { -    switch (message.messageType) { +  async handleMessage(messageType: string, data: any) { +    switch (messageType) {        case "highlightedCode": -        this.send("highlightedCode", { +        this.messenger?.send("highlightedCode", {            highlightedCode: this.getHighlightedCode(),          });          break;        case "workspaceDirectory": -        this.send("workspaceDirectory", { +        this.messenger?.send("workspaceDirectory", {            workspaceDirectory: this.getWorkspaceDirectory(),          });        case "openFiles": -        this.send("openFiles", { +        this.messenger?.send("openFiles", {            openFiles: this.getOpenFiles(),          });          break;        case "readFile": -        this.send("readFile", { -          contents: this.readFile(message.filepath), +        this.messenger?.send("readFile", { +          contents: this.readFile(data.filepath),          });          break;        case "editFile": -        let fileEdit = await this.editFile(message.edit); -        this.send("editFile", { +        const fileEdit = await this.editFile(data.edit); +        this.messenger?.send("editFile", {            fileEdit,          });          break;        case "saveFile": -        this.saveFile(message.filepath); +        this.saveFile(data.filepath);          break;        case "setFileOpen": -        this.openFile(message.filepath); +        this.openFile(data.filepath);          // TODO: Close file          break;        case "openNotebook":        case "connected":          break;        default: -        throw Error("Unknown message type:" + message.messageType); +        throw Error("Unknown message type:" + messageType);      }    }    getWorkspaceDirectory() { @@ -209,17 +134,20 @@ class IdeProtocolClient {    // Initiate Request    closeNotebook(sessionId: string) { -    this._panels.get(sessionId)?.dispose(); -    this._panels.delete(sessionId); +    this.panels.get(sessionId)?.dispose(); +    this.panels.delete(sessionId);    }    async openNotebook() {      console.log("OPENING NOTEBOOK"); -    let resp = await this.sendAndReceive({}, "openNotebook"); -    let sessionId = resp.sessionId; +    if (this.messenger === null) { +      console.log("MESSENGER IS NULL"); +    } +    const resp = await this.messenger?.sendAndReceive("openNotebook", {}); +    const sessionId = resp.sessionId;      console.log("SESSION ID", sessionId); -    let column = getRightViewColumn(); +    const column = getRightViewColumn();      const panel = vscode.window.createWebviewPanel(        "continue.debugPanelView",        "Continue", @@ -231,9 +159,9 @@ class IdeProtocolClient {      );      // And set its HTML content -    panel.webview.html = setupDebugPanel(panel, this._context, sessionId); +    panel.webview.html = setupDebugPanel(panel, this.context, sessionId); -    this._panels.set(sessionId, panel); +    this.panels.set(sessionId, panel);    }    acceptRejectSuggestion(accept: boolean, key: SuggestionRanges) { diff --git a/extension/src/debugPanel.ts b/extension/src/debugPanel.ts index 4192595c..87c33da1 100644 --- a/extension/src/debugPanel.ts +++ b/extension/src/debugPanel.ts @@ -108,18 +108,21 @@ class WebsocketConnection {      this._onOpen = onOpen;      this._onClose = onClose; -    this._ws.onmessage = (event) => { +    this._ws.addEventListener("message", (event) => {        this._onMessage(event.data); -    }; -    this._ws.onclose = () => { +    }); +    this._ws.addEventListener("close", () => {        this._onClose(); -    }; -    this._ws.onopen = () => { +    }); +    this._ws.addEventListener("open", () => {        this._onOpen(); -    }; +    });    }    public send(message: string) { +    if (typeof message !== "string") { +      message = JSON.stringify(message); +    }      this._ws.send(message);    } @@ -230,6 +233,19 @@ export function setupDebugPanel(            apiUrl: getContinueServerUrl(),            sessionId,          }); + +        // // Listen for changes to server URL in settings +        // vscode.workspace.onDidChangeConfiguration((event) => { +        //   if (event.affectsConfiguration("continue.serverUrl")) { +        //     debugPanelWebview?.postMessage({ +        //       type: "onLoad", +        //       vscMachineId: vscode.env.machineId, +        //       apiUrl: getContinueServerUrl(), +        //       sessionId, +        //     }); +        //   } +        // }); +          break;        } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index e0b94278..88af0d19 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -4,7 +4,6 @@  import * as vscode from "vscode";  import { -  setupExtensionEnvironment,    isPythonEnvSetup,    startContinuePythonServer,  } from "./activation/environmentSetup"; @@ -26,11 +25,7 @@ export function activate(context: vscode.ExtensionContext) {        cancellable: false,      },      async () => { -      if (isPythonEnvSetup()) { -        await startContinuePythonServer(); -      } else { -        await setupExtensionEnvironment(); -      } +      await startContinuePythonServer();        dynamicImportAndActivate(context, true);      }    ); diff --git a/extension/src/test/runTest.ts b/extension/src/test/runTest.ts index 27b3ceb2..e810ed5b 100644 --- a/extension/src/test/runTest.ts +++ b/extension/src/test/runTest.ts @@ -1,23 +1,23 @@ -import * as path from 'path'; +import * as path from "path"; -import { runTests } from '@vscode/test-electron'; +import { runTests } from "@vscode/test-electron";  async function main() { -	try { -		// The folder containing the Extension Manifest package.json -		// Passed to `--extensionDevelopmentPath` -		const extensionDevelopmentPath = path.resolve(__dirname, '../../'); +  try { +    // The folder containing the Extension Manifest package.json +    // Passed to `--extensionDevelopmentPath` +    const extensionDevelopmentPath = path.resolve(__dirname, "../../"); -		// The path to test runner -		// Passed to --extensionTestsPath -		const extensionTestsPath = path.resolve(__dirname, './suite/index'); +    // The path to test runner +    // Passed to --extensionTestsPath +    const extensionTestsPath = path.resolve(__dirname, "./suite/index"); -		// Download VS Code, unzip it and run the integration test -		await runTests({ extensionDevelopmentPath, extensionTestsPath }); -	} catch (err) { -		console.error('Failed to run tests'); -		process.exit(1); -	} +    // Download VS Code, unzip it and run the integration test +    await runTests({ extensionDevelopmentPath, extensionTestsPath }); +  } catch (err) { +    console.error("Failed to run tests"); +    process.exit(1); +  }  }  main(); diff --git a/extension/src/util/messenger.ts b/extension/src/util/messenger.ts new file mode 100644 index 00000000..6f8bb29d --- /dev/null +++ b/extension/src/util/messenger.ts @@ -0,0 +1,108 @@ +console.log("Websocket import"); +const WebSocket = require("ws"); + +export abstract class Messenger { +  abstract send(messageType: string, data: object): void; + +  abstract onMessageType( +    messageType: string, +    callback: (data: object) => void +  ): void; + +  abstract onMessage(callback: (messageType: string, data: any) => void): void; + +  abstract onOpen(callback: () => void): void; + +  abstract onClose(callback: () => void): void; + +  abstract sendAndReceive(messageType: string, data: any): Promise<any>; +} + +export class WebsocketMessenger extends Messenger { +  websocket: WebSocket; +  private onMessageListeners: { +    [messageType: string]: ((data: object) => void)[]; +  } = {}; +  private onOpenListeners: (() => void)[] = []; +  private onCloseListeners: (() => void)[] = []; +  private serverUrl: string; + +  _newWebsocket(): WebSocket { +    // // Dynamic import, because WebSocket is builtin with browser, but not with node. And can't use require in browser. +    // if (typeof process === "object") { +    //   console.log("Using node"); +    //   // process is only available in Node +    //   var WebSocket = require("ws"); +    // } + +    const newWebsocket = new WebSocket(this.serverUrl); +    for (const listener of this.onOpenListeners) { +      this.onOpen(listener); +    } +    for (const listener of this.onCloseListeners) { +      this.onClose(listener); +    } +    for (const messageType in this.onMessageListeners) { +      for (const listener of this.onMessageListeners[messageType]) { +        this.onMessageType(messageType, listener); +      } +    } +    return newWebsocket; +  } + +  constructor(serverUrl: string) { +    super(); +    this.serverUrl = serverUrl; +    this.websocket = this._newWebsocket(); +  } + +  send(messageType: string, data: object) { +    const payload = JSON.stringify({ messageType, data }); +    if (this.websocket.readyState === this.websocket.OPEN) { +      this.websocket.send(payload); +    } else { +      if (this.websocket.readyState !== this.websocket.CONNECTING) { +        this.websocket = this._newWebsocket(); +      } +      this.websocket.addEventListener("open", () => { +        this.websocket.send(payload); +      }); +    } +  } + +  sendAndReceive(messageType: string, data: any): Promise<any> { +    return new Promise((resolve, reject) => { +      const eventListener = (data: any) => { +        // THIS ISN"T GETTING CALLED +        resolve(data); +        this.websocket.removeEventListener("message", eventListener); +      }; +      this.onMessageType(messageType, eventListener); +      this.send(messageType, data); +    }); +  } + +  onMessageType(messageType: string, callback: (data: any) => void): void { +    this.websocket.addEventListener("message", (event: any) => { +      const msg = JSON.parse(event.data); +      if (msg.messageType === messageType) { +        callback(msg.data); +      } +    }); +  } + +  onMessage(callback: (messageType: string, data: any) => void): void { +    this.websocket.addEventListener("message", (event) => { +      const msg = JSON.parse(event.data); +      callback(msg.messageType, msg.data); +    }); +  } + +  onOpen(callback: () => void): void { +    this.websocket.addEventListener("open", callback); +  } + +  onClose(callback: () => void): void { +    this.websocket.addEventListener("close", callback); +  } +}  | 
