Browse Source

First cut at doing compiling TypeScript in the editor. This will only work if there is a JSResourceEditor open currently since the compilation is done inside the web view editor.

Shaddock Heath 9 years ago
parent
commit
6265817e37

+ 88 - 8
Script/AtomicEditor/hostExtensions/languageExtensions/TypscriptLanguageExtension.ts

@@ -68,16 +68,19 @@ export default class TypescriptLanguageExtension implements Editor.HostExtension
             // First we need to load in a copy of the lib.core.d.ts that is necessary for the hosted typescript compiler
             projectFiles.push(Atomic.addTrailingSlash(Atomic.addTrailingSlash(ToolCore.toolEnvironment.toolDataDir) + "TypeScriptSupport") + "lib.core.d.ts");
 
-            // Load up a copy of the duktape.d.ts
-            projectFiles.push(Atomic.addTrailingSlash(Atomic.addTrailingSlash(ToolCore.toolEnvironment.toolDataDir) + "TypeScriptSupport") + "duktape.d.ts");
-
-
-            // Look in a 'typings' directory for any typescript definition files
-            const typingsDir = Atomic.addTrailingSlash(ToolCore.toolSystem.project.projectPath) + "typings";
-            Atomic.fileSystem.scanDir(typingsDir, "*.d.ts", Atomic.SCAN_FILES, true).forEach(filename => {
-                projectFiles.push(Atomic.addTrailingSlash(typingsDir) + filename);
+            // Then see if we have a copy of Atomic.d.ts in the project directory.  If we don't then we should load it up from the tool environment
+            let found = false;
+            projectFiles.forEach((file) => {
+                if (file.indexOf("Atomic.d.ts") != -1) {
+                    found = true;
+                }
             });
 
+            if (!found) {
+                // Load up the Atomic.d.ts from the tool core
+                console.log("Need to load up hosted Atomic.d.ts!");
+            }
+
             let files = projectFiles.map((f: string) => {
                 if (f.indexOf(ToolCore.toolSystem.project.resourcePath) != -1) {
                     // if we are in the resources directory, just pass back the path from resources down
@@ -111,6 +114,7 @@ export default class TypescriptLanguageExtension implements Editor.HostExtension
         // We care about both resource events as well as project events
         serviceLocator.resourceServices.register(this);
         serviceLocator.projectServices.register(this);
+        serviceLocator.uiServices.register(this);
         this.serviceRegistry = serviceLocator;
     }
 
@@ -197,5 +201,81 @@ export default class TypescriptLanguageExtension implements Editor.HostExtension
         // got a load, we need to reset the language service
         console.log(`${this.name}: received a project loaded event for project at ${ev.path}`);
         this.loadProjectFiles();
+        this.rebuildMenu();
+    }
+
+
+    /**
+     * Rebuilds the plugin menu.  This is needed to toggle the CompileOnSave true or false
+     */
+    rebuildMenu() {
+        if (this.isTypescriptProject) {
+            this.serviceRegistry.uiServices.removePluginMenuItemSource("TypeScript");
+            const isCompileOnSave = this.serviceRegistry.projectServices.getUserPreference(this.name, "CompileOnSave", false);
+            let subMenu = {};
+            if (isCompileOnSave) {
+                subMenu["Compile on Save: On"] = [`${this.name}.compileonsave`];
+            } else {
+                subMenu["Compile on Save: Off"] = [`${this.name}.compileonsave`];
+            }
+            subMenu["Compile Project"] = [`${this.name}.compileproject`];
+            this.serviceRegistry.uiServices.createPluginMenuItemSource("TypeScript", subMenu);
+        }
+    }
+
+    /*** UIService implementation ***/
+
+    /**
+     * Called when a plugin menu item is clicked
+     * @param  {string} refId
+     * @return {boolean}
+     */
+    menuItemClicked(refId: string): boolean {
+        let [extension, action] = refId.split(".");
+        if (extension == this.name) {
+            switch (action) {
+                case "compileonsave":
+                    // Toggle
+                    const isCompileOnSave = this.serviceRegistry.projectServices.getUserPreference(this.name, "CompileOnSave", false);
+                    this.serviceRegistry.projectServices.setUserPreference(this.name, "CompileOnSave", !isCompileOnSave);
+                    this.rebuildMenu();
+                    return true;
+                case "compileproject":
+                    const editor = this.serviceRegistry.uiServices.getCurrentResourceEditor();
+                    if (editor && editor.typeName == "JSResourceEditor") {
+                        const jsEditor = <Editor.JSResourceEditor>editor;
+                        jsEditor.webView.webClient.executeJavaScript(`TypeScript_DoFullCompile();`);
+                    }
+                    return true;
+            }
+        }
+    }
+
+    /**
+     * Handle messages that are submitted via Atomic.Query from within a web view editor.
+     * @param message The message type that was submitted to be used to determine what the data contains if present
+     * @param data any additional data that needs to be submitted with the message
+     */
+    handleWebMessage(messageType: string, data: any) {
+        switch (messageType) {
+            case "TypeScript.DisplayCompileResults":
+                this.displayCompileResults(data.annotations);
+                break;
+        }
+    }
+
+    /**
+     * Display the results of the compilation step
+     * @param  {any[]} annotations
+     */
+    displayCompileResults(annotations: any[]) {
+        let messageArray = annotations.map((result) => {
+            return `${result.text} at line ${result.row} col ${result.column} in ${result.file}`;
+        });
+
+        if (messageArray.length == 0) {
+            messageArray.push("Success");
+        }
+        this.serviceRegistry.uiServices.showModalError("TypeScript Compilation Results", messageArray.join("\n"));
     }
 }

+ 57 - 9
Script/AtomicWebViewEditor/clientExtensions/languageExtensions/typescript/TypescriptLanguageExtension.ts

@@ -118,6 +118,10 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
      */
     codeLoaded(ev: Editor.EditorEvents.CodeLoadedEvent) {
         if (this.isValidFiletype(ev.filename)) {
+
+            // Hook in the routine to allow the host to perform a full compile
+            this.serviceLocator.clientServices.getHostInterop().addCustomHostRoutine("TypeScript_DoFullCompile", this.doFullCompile.bind(this));
+
             this.filename = ev.filename;
 
             let editor = ev.editor;
@@ -148,9 +152,24 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
             case WorkerProcessTypes.AnnotationsUpdated:
                 this.setAnnotations(e.data);
                 break;
+            case WorkerProcessTypes.SaveFile:
+                this.saveFile(e.data);
+                break;
+            case WorkerProcessTypes.DisplayFullCompileResults:
+                this.displayFullCompileResults(e.data);
+                break;
         }
     }
 
+    /**
+     * Saves a compiled file sent across from the worker service
+     * @param  {WorkerProcessTypes.SaveMessageData} event
+     */
+    saveFile(event: WorkerProcessTypes.SaveMessageData) {
+        console.log("Save File:" + event.filename);
+        this.serviceLocator.clientServices.getHostInterop().saveFile(event.filename, event.code);
+    }
+
     /**
      * Set annotations based upon issues reported by the typescript language service
      * @param  {WorkerProcessTypes.GetAnnotationsResponseMessageData} event
@@ -207,8 +226,8 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
                 // a result back
                 extension.workerRequest(WorkerProcessTypes.DocTooltipResponse, message)
                     .then((e: WorkerProcessTypes.GetDocTooltipResponseMessageData) => {
-                    extension.editor.completer.showDocTooltip(e);
-                });
+                        extension.editor.completer.showDocTooltip(e);
+                    });
             },
 
             getCompletions: function(editor, session, pos, prefix, callback) {
@@ -222,8 +241,8 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
 
                 extension.workerRequest(WorkerProcessTypes.CompletionResponse, message)
                     .then((e: WorkerProcessTypes.GetCompletionsResponseMessageData) => {
-                    callback(null, e.completions);
-                });
+                        callback(null, e.completions);
+                    });
             }
         };
         return wordCompleter;
@@ -235,7 +254,7 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
      */
     save(ev: Editor.EditorEvents.CodeSavedEvent) {
         if (this.isValidFiletype(ev.filename)) {
-            console.log(`${this.name}: received a save resource event for ${ev.filename}`);
+            //console.log(`${this.name}: received a save resource event for ${ev.filename}`);
 
             const message: WorkerProcessTypes.SaveMessageData = {
                 command: ClientExtensionEventNames.CodeSavedEvent,
@@ -255,7 +274,7 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
      */
     delete(ev: Editor.EditorEvents.DeleteResourceEvent) {
         if (this.isValidFiletype(ev.path)) {
-            console.log(`${this.name}: received a delete resource event for ${ev.path}`);
+            //console.log(`${this.name}: received a delete resource event for ${ev.path}`);
 
             // notify the typescript language service that the file has been deleted
             const message: WorkerProcessTypes.DeleteMessageData = {
@@ -273,7 +292,7 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
      */
     rename(ev: Editor.EditorEvents.RenameResourceEvent) {
         if (this.isValidFiletype(ev.path)) {
-            console.log(`${this.name}: received a rename resource event for ${ev.path} -> ${ev.newPath}`);
+            //console.log(`${this.name}: received a rename resource event for ${ev.path} -> ${ev.newPath}`);
 
             // notify the typescript language service that the file has been renamed
             const message: WorkerProcessTypes.RenameMessageData = {
@@ -291,7 +310,36 @@ export default class TypescriptLanguageExtension implements Editor.ClientExtensi
      * @return {[type]}
      */
     preferencesChanged() {
-        // Stub function for now
-        this.serviceLocator.clientServices.getUserPreference("TypescriptLanguageExtension", "CompileOnSave", true);
+        let compileOnSave = this.serviceLocator.clientServices.getUserPreference("HostTypeScriptLanguageExtension", "CompileOnSave", true);
+        const message: WorkerProcessTypes.SetPreferencesMessageData = {
+            command: WorkerProcessTypes.SetPreferences,
+            preferences: {
+                compileOnSave: compileOnSave
+            }
+        };
+
+        this.worker.port.postMessage(message);
+    }
+
+    /**
+     * Tell the language service to perform a full compile
+     */
+    doFullCompile() {
+        const message: WorkerProcessTypes.WorkerProcessMessageData = {
+            command: WorkerProcessTypes.DoFullCompile
+        };
+        this.worker.port.postMessage(message);
+    }
+
+
+    /**
+     * Displays the results from a full compile
+     * @param  {WorkerProcessTypes.FullCompileResultsMessageData} results
+     */
+    displayFullCompileResults(results: WorkerProcessTypes.FullCompileResultsMessageData) {
+        let messageArray = results.annotations.map((result) => {
+            return `${result.text} at line ${result.row} col ${result.column} in ${result.file}`;
+        });
+        window.atomicQueryPromise("TypeScript.DisplayCompileResults", results);
     }
 }

+ 92 - 11
Script/AtomicWebViewEditor/clientExtensions/languageExtensions/typescript/workerprocess/TypescriptLanguageServiceWebWorker.ts

@@ -35,6 +35,30 @@ interface TSConfigFile {
     files: Array<string>;
 }
 
+/**
+ * Promise version of atomic query
+ * @param  {string} message the query to use to pass to atomicQuery.  If there is no payload, this will be passed directly, otherwise it will be passed in a data object
+ * @param  {any} payload optional data to send
+ * @return {Promise}
+ */
+function atomicQueryPromise(message: any): Promise<{}> {
+    return new Promise(function(resolve, reject) {
+        let queryMessage = message;
+
+        // if message is coming in as an object then let's stringify it
+        if (typeof (message) != "string") {
+            queryMessage = JSON.stringify(message);
+        }
+
+        window.atomicQuery({
+            request: queryMessage,
+            persistent: false,
+            onSuccess: resolve,
+            onFailure: (error_code, error_message) => reject({ error_code: error_code, error_message: error_message })
+        });
+    });
+}
+
 /**
  * Queries the host for a particular resource and returns it in a promise
  * @param  {string} codeUrl
@@ -68,6 +92,17 @@ function getFileResource(filename: string): Promise<{}> {
 class WebFileSystem implements FileSystemInterface {
 
     private fileCache = {};
+    private communicationPort: MessagePort;
+
+    /**
+     * Sets the port used to communicate with the primary window.
+     * This is needed since AtomicQuery can't be called from a webworker
+     * @param  {MessagePort} port
+     */
+    setCommunicationPort(port: MessagePort) {
+        this.communicationPort = port;
+    }
+
     /**
      * Deterimine if the particular file exists in the resources
      * @param  {string} filename
@@ -103,15 +138,16 @@ class WebFileSystem implements FileSystemInterface {
      * @param  {string} contents
      */
     writeFile(filename: string, contents: string) {
-        //TODO:
-        /*let script = new Atomic.File(filename, Atomic.FILE_WRITE);
-        try {
-            script.writeString(contents);
-            script.flush();
-        } finally {
-            script.close();
-        }
-        */
+        const fileExt = filename.indexOf(".") != -1 ? filename.split(".").pop() : "";
+        let message: WorkerProcessTypes.SaveMessageData = {
+            command: WorkerProcessTypes.SaveFile,
+            filename: filename,
+            code: contents,
+            fileExt: fileExt,
+            editor: null
+        };
+
+        this.communicationPort.postMessage(message);
     }
 
     /**
@@ -141,6 +177,10 @@ export default class TypescriptLanguageServiceWebWorker {
 
     projectLoaded = false;
 
+    options = {
+        compileOnSave: false
+    };
+
     constructor() {
         this.fs = new WebFileSystem();
         this.languageService = new TypescriptLanguageService(this.fs);
@@ -180,6 +220,12 @@ export default class TypescriptLanguageServiceWebWorker {
                 case WorkerProcessTypes.GetAnnotations:
                     this.handleGetAnnotations(port, e.data);
                     break;
+                case WorkerProcessTypes.SetPreferences:
+                    this.setPreferences(port, e.data);
+                    break;
+                case WorkerProcessTypes.DoFullCompile:
+                    this.doFullCompile(port);
+                    break;
             }
 
         }, false);
@@ -245,7 +291,7 @@ export default class TypescriptLanguageServiceWebWorker {
         sender: string,
         filename: string
     }) {
-        port.postMessage({ command: WorkerProcessTypes.Message, message: "Hello " + eventData.sender + " (port #" + this.connections + ")" });
+        // port.postMessage({ command: WorkerProcessTypes.Message, message: "Hello " + eventData.sender + " (port #" + this.connections + ")" });
         this.loadProjectFiles().then(() => {
             let diagnostics = this.languageService.compile([eventData.filename]);
             this.handleGetAnnotations(port, eventData);
@@ -262,7 +308,6 @@ export default class TypescriptLanguageServiceWebWorker {
         if (this.connections <= 0) {
             this.reset();
         }
-        console.log("Got a close");
     }
 
     /**
@@ -341,6 +386,38 @@ export default class TypescriptLanguageServiceWebWorker {
     handleSave(port: MessagePort, eventData: WorkerProcessTypes.SaveMessageData) {
         this.languageService.updateProjectFile(eventData.filename, eventData.code);
         this.handleGetAnnotations(port, eventData);
+
+        if (this.options.compileOnSave) {
+            this.fs.setCommunicationPort(port);
+            let results = this.languageService.compile([eventData.filename]);
+        }
+    }
+
+    /**
+     * Perform a full compile of the typescript
+     * @param  {MessagePort} port
+     */
+    doFullCompile(port: MessagePort) {
+        this.fs.setCommunicationPort(port);
+        let errors = this.languageService.compile([]);
+
+        let results = errors.map(diagnostic => {
+            let lineChar = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
+            let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
+            return {
+                file: diagnostic.file.fileName,
+                row: lineChar.line,
+                column: lineChar.character,
+                text: message,
+                type: diagnostic.category == 1 ? "error" : "warning"
+            };
+        });
+
+        let message: WorkerProcessTypes.FullCompileResultsMessageData = {
+            command: WorkerProcessTypes.DisplayFullCompileResults,
+            annotations: results
+        };
+        port.postMessage(message);
     }
 
     /**
@@ -360,4 +437,8 @@ export default class TypescriptLanguageServiceWebWorker {
     handleRename(port: MessagePort, eventData: WorkerProcessTypes.RenameMessageData) {
         this.languageService.renameProjectFile(eventData.path, eventData.newPath);
     }
+
+    setPreferences(port: MessagePort, eventData: WorkerProcessTypes.SetPreferencesMessageData) {
+        this.options.compileOnSave = eventData.preferences.compileOnSave;
+    }
 }

+ 13 - 0
Script/AtomicWebViewEditor/clientExtensions/languageExtensions/typescript/workerprocess/workerProcessTypes.ts

@@ -32,6 +32,13 @@ export interface SaveMessageData extends WorkerProcessMessageData, Editor.Editor
 export interface DeleteMessageData extends WorkerProcessMessageData, Editor.EditorEvents.DeleteResourceEvent {}
 export interface RenameMessageData extends WorkerProcessMessageData, Editor.EditorEvents.RenameResourceEvent {}
 
+export const SetPreferences = "SET_PREFERENCES";
+export interface SetPreferencesMessageData extends WorkerProcessMessageData {
+    preferences: {
+        compileOnSave?: boolean;
+    };
+}
+
 export const GetCompletions = "COMPLETIONS";
 export const CompletionResponse = "COMPLETION_RESPONSE";
 export interface WordCompletion {
@@ -76,6 +83,12 @@ export interface GetAnnotationsResponseMessageData extends WorkerProcessMessageD
     annotations: any[];
 }
 
+export const DoFullCompile  = "DO_FULL_COMPILE";
+export const DisplayFullCompileResults = "DISPLAY_FULL_COMPILE_RESULTS";
+export interface FullCompileResultsMessageData extends GetAnnotationsResponseMessageData {}
+
+export const SaveFile = "SAVE_FILE";
+
 export const Connect = "HELO";
 export const Disconnect = "CLOSE";
 export const Message = "MESSAGE";