Просмотр исходного кода

Prototype support for deprecated members

Sebastien Lebreton 5 лет назад
Родитель
Сommit
e092e423a0
2 измененных файлов с 217 добавлено и 30 удалено
  1. 98 0
      Playground/js/definitionWorker.js
  2. 119 30
      Playground/js/monacoCreator.js

+ 98 - 0
Playground/js/definitionWorker.js

@@ -0,0 +1,98 @@
+// monaco is using 'define' for module dependencies and service lookup.
+// hopefully typescript is self-contained
+var ts = null;
+var define = (id, dependencies, callback) => ts = callback();
+
+importScripts("../node_modules/monaco-editor/dev/vs/language/typescript/lib/typescriptServices.js");
+
+// store deprecated names
+var deprecatedCandidates = [];
+
+function canHaveJsDoc(node) {
+    const kind = node.kind;
+    switch (kind) {
+        case ts.SyntaxKind.Parameter:
+        case ts.SyntaxKind.CallSignature:
+        case ts.SyntaxKind.ConstructSignature:
+        case ts.SyntaxKind.MethodSignature:
+        case ts.SyntaxKind.PropertySignature:
+        case ts.SyntaxKind.ArrowFunction:
+        case ts.SyntaxKind.ParenthesizedExpression:
+        case ts.SyntaxKind.SpreadAssignment:
+        case ts.SyntaxKind.ShorthandPropertyAssignment:
+        case ts.SyntaxKind.PropertyAssignment:
+        case ts.SyntaxKind.FunctionExpression:
+        case ts.SyntaxKind.FunctionDeclaration:
+        case ts.SyntaxKind.LabeledStatement:
+        case ts.SyntaxKind.ExpressionStatement:
+        case ts.SyntaxKind.VariableStatement:
+        case ts.SyntaxKind.Constructor:
+        case ts.SyntaxKind.MethodDeclaration:
+        case ts.SyntaxKind.PropertyDeclaration:
+        case ts.SyntaxKind.GetAccessor:
+        case ts.SyntaxKind.SetAccessor:
+        case ts.SyntaxKind.ClassDeclaration:
+        case ts.SyntaxKind.ClassExpression:
+        case ts.SyntaxKind.InterfaceDeclaration:
+        case ts.SyntaxKind.TypeAliasDeclaration:
+        case ts.SyntaxKind.EnumMember:
+        case ts.SyntaxKind.EnumDeclaration:
+        case ts.SyntaxKind.ModuleDeclaration:
+        case ts.SyntaxKind.ImportEqualsDeclaration:
+        case ts.SyntaxKind.IndexSignature:
+        case ts.SyntaxKind.FunctionType:
+        case ts.SyntaxKind.ConstructorType:
+        case ts.SyntaxKind.JSDocFunctionType:
+        case ts.SyntaxKind.EndOfFileToken:
+        case ts.SyntaxKind.ExportDeclaration:
+            return true;
+        default:
+            return false;
+    }
+}
+
+function onFindDeprecatedCandidate(node) {
+    const name = relatedName(node);
+    if (name)
+        deprecatedCandidates.push(name);
+}
+
+function relatedName(node) {
+    if (canHaveJsDoc(node) && node.name)
+        return node.name.escapedText;
+
+    if (node.parent)
+        return relatedName(parent);
+
+    return undefined;
+}
+
+function visit(node) {
+
+    if (node.jsDoc) {
+        for (const jsDocEntry of node.jsDoc) {
+            if (jsDocEntry.tags) {
+                for (const tag of jsDocEntry.tags) {
+                    if (tag.tagName && tag.tagName.escapedText == 'deprecated')
+                        onFindDeprecatedCandidate(node);
+                }
+            }
+        }
+    }
+
+    ts.forEachChild(node, visit);
+}
+
+function processDefinition(code) {
+    if (deprecatedCandidates.length == 0) {
+        var sourceFile = ts.createSourceFile('babylon.js', code, ts.ScriptTarget.ESNext, true);
+        ts.forEachChild(sourceFile, visit);
+    }
+
+    self.postMessage({ result: deprecatedCandidates });
+}
+
+self.addEventListener('message', event => {
+    const { code } = event.data;
+    processDefinition(code);
+});

+ 119 - 30
Playground/js/monacoCreator.js

@@ -4,12 +4,14 @@
 class MonacoCreator {
     constructor(parent) {
         this.parent = parent;
-        
+
         this.jsEditor = null;
         this.diffEditor = null;
         this.diffNavigator = null;
         this.monacoMode = "javascript";
         this.blockEditorChange = false;
+        this.definitionWorker = null;
+        this.deprecatedCandidates = [];
 
         this.compilerTriggerTimeoutID = null;
     }
@@ -21,7 +23,7 @@ class MonacoCreator {
     };
 
     getCode() {
-        if(this.jsEditor) return this.jsEditor.getValue();
+        if (this.jsEditor) return this.jsEditor.getValue();
         else return "";
     };
     setCode(value) {
@@ -56,6 +58,8 @@ class MonacoCreator {
             return;
 
         const libContent = await response.text();
+        this.setupDefinitionWorker(libContent);
+
         require.config({ paths: { 'vs': 'node_modules/monaco-editor/dev/vs' } });
 
         require(['vs/editor/editor.main'], () => {
@@ -70,24 +74,108 @@ class MonacoCreator {
         });
     };
 
-    hookMonacoCompletionProvider(provider) {
-        const hooked = provider.prototype.provideCompletionItems;
+    setupDefinitionWorker(libContent) {
+        this.definitionWorker = new Worker('js/definitionWorker.js');
+        this.definitionWorker.addEventListener('message', ({ data }) => {
+            this.deprecatedCandidates = data.result;
+            this.analyzeCode();
+        });
+        this.definitionWorker.postMessage({ code: libContent });
+    }
+
+    isDeprecatedEntry(details) {
+        return details
+            && details.tags
+            && details.tags.find(this.isDeprecatedTag);
+    }
+
+    isDeprecatedTag(tag) {
+        return tag
+            && tag.name == "deprecated";
+    }
+
+    async analyzeCode() {
+        // if the definition worker is very fast, this can be called out of context
+        if (!this.jsEditor)
+            return;
+
+        const model = this.jsEditor.getModel();
+        if (!model)
+            return;
+
+        const uri = model.uri;
+
+        let worker = null;
+        if (this.parent.settingsPG.ScriptLanguage == "JS")
+            worker = await monaco.languages.typescript.getJavaScriptWorker();
+        else
+            worker = await monaco.languages.typescript.getTypeScriptWorker();
 
-        const suggestionFilter = function(suggestion) {
-            return !suggestion.label.startsWith("_");
+        const languageService = await worker(uri);
+        const source = 'babylonjs';
+
+        monaco.editor.setModelMarkers(model, source, []);
+        const markers = [];
+
+        for (const candidate of this.deprecatedCandidates) {
+            const matches = model.findMatches(candidate, null, false, true, null, false);
+            for (const match of matches) {
+                const position = { lineNumber: match.range.startLineNumber, column: match.range.startColumn };
+                const wordInfo = model.getWordAtPosition(position);
+                const offset = model.getOffsetAt(position);
+
+                // the following is time consuming on all suggestions, that's why we precompute deprecated candidate names in the definition worker to filter calls
+                const details = await languageService.getCompletionEntryDetails(uri.toString(), offset, wordInfo.word);
+                if (this.isDeprecatedEntry(details)) {
+                    const deprecatedInfo = details.tags.find(this.isDeprecatedTag);
+                    markers.push({
+                        startLineNumber: match.range.startLineNumber,
+                        endLineNumber: match.range.endLineNumber,
+                        startColumn: match.range.startColumn,
+                        endColumn: match.range.endColumn,
+                        message: deprecatedInfo.text,
+                        severity: monaco.MarkerSeverity.Warning,
+                        source: source,
+                    });
+                }
+            }
         }
 
-        provider.prototype.provideCompletionItems = async function(model, position, context, token) {
+        monaco.editor.setModelMarkers(model, source, markers);
+    }
+
+    hookMonacoCompletionProvider(provider) {
+        const provideCompletionItems = provider.prototype.provideCompletionItems;
+        const owner = this;
+
+        provider.prototype.provideCompletionItems = async function (model, position, context, token) {
             // reuse 'this' to preserve context through call (using apply)
-            var result = await hooked.apply(this, [model, position, context, token]);
-            
+            const result = await provideCompletionItems.apply(this, [model, position, context, token]);
+
             if (!result || !result.suggestions)
                 return result;
 
-            const suggestions = result.suggestions.filter(suggestionFilter);
-            const incomplete = result.incomplete && result.incomplete == true;
+            const suggestions = result.suggestions.filter(item => !item.label.startsWith("_"));
+
+            for (const suggestion of suggestions) {
+                if (owner.deprecatedCandidates.includes(suggestion.label)) {
+
+                    // the following is time consuming on all suggestions, that's why we precompute deprecated candidate names in the definition worker to filter calls
+                    const uri = suggestion.uri;
+                    const worker = await this._worker(uri);
+                    const model = monaco.editor.getModel(uri);
+                    const details = await worker.getCompletionEntryDetails(uri.toString(), model.getOffsetAt(position), suggestion.label)
 
-            return { 
+                    if (owner.isDeprecatedEntry(details)) {
+                        suggestion.tags = [monaco.languages.CompletionItemTag.Deprecated];
+                    }
+                }
+            }
+
+            // preserve incomplete flag or force it when the definition is not yet analyzed
+            const incomplete = (result.incomplete && result.incomplete == true) || owner.deprecatedCandidates.length == 0;
+
+            return {
                 suggestions: suggestions,
                 incomplete: incomplete
             };
@@ -127,18 +215,18 @@ class MonacoCreator {
         monaco.languages.registerColorProvider(this.monacoMode, {
             provideColorPresentations: (model, colorInfo) => {
                 const color = colorInfo.color;
-                
+
                 const precision = 100.0;
                 const converter = (n) => Math.round(n * precision) / precision;
-                
+
                 let label;
                 if (color.alpha === undefined || color.alpha === 1.0) {
                     label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)})`;
                 } else {
                     label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)}, ${converter(color.alpha)})`;
                 }
-        
-                return [ { label: label } ];
+
+                return [{ label: label }];
             },
 
             provideDocumentColors: (model) => {
@@ -150,13 +238,13 @@ class MonacoCreator {
                 const converter = (g) => g === undefined ? undefined : Number(g);
 
                 return matches.map(match => ({
-                    color: { 
-                        red: converter(match.matches[1]), 
-                        green: converter(match.matches[2]), 
+                    color: {
+                        red: converter(match.matches[1]),
+                        green: converter(match.matches[2]),
                         blue: converter(match.matches[3]),
                         alpha: converter(match.matches[4])
                     },
-                    range:{
+                    range: {
                         startLineNumber: match.range.startLineNumber,
                         startColumn: match.range.startColumn + match.matches[0].indexOf("("),
                         endLineNumber: match.range.startLineNumber,
@@ -200,8 +288,9 @@ class MonacoCreator {
         this.jsEditor = monaco.editor.create(document.getElementById('jsEditor'), editorOptions);
 
         this.jsEditor.setValue(oldCode);
-        this.jsEditor.onKeyUp(function () {
+        this.jsEditor.onDidChangeModelContent(function () {
             this.parent.utils.markDirty();
+            this.analyzeCode();
         }.bind(this));
     };
 
@@ -232,15 +321,15 @@ class MonacoCreator {
             followsCaret: true,
             ignoreCharChanges: true
         });
-        
+
         const menuPG = this.parent.menuPG;
         const main = this.parent.main;
         const monacoCreator = this;
 
-        this.diffEditor.addCommand(monaco.KeyCode.Escape, function() { main.toggleDiffEditor(monacoCreator, menuPG); });
+        this.diffEditor.addCommand(monaco.KeyCode.Escape, function () { main.toggleDiffEditor(monacoCreator, menuPG); });
         // Adding default VSCode bindinds for previous/next difference
-        this.diffEditor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.F5, function() { main.navigateToNext(); });
-        this.diffEditor.addCommand(monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.F5, function() { main.navigateToPrevious(); });
+        this.diffEditor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.F5, function () { main.navigateToNext(); });
+        this.diffEditor.addCommand(monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.F5, function () { main.navigateToPrevious(); });
 
         this.diffEditor.focus();
     }
@@ -253,7 +342,7 @@ class MonacoCreator {
         let model = this.diffEditor.getModel();
         let leftModel = model.original;
         let rightModel = model.modified;
-        
+
         leftModel.dispose();
         rightModel.dispose();
 
@@ -267,14 +356,14 @@ class MonacoCreator {
     /**
      * Format the code in the editor
      */
-    formatCode () {
+    formatCode() {
         this.jsEditor.getAction('editor.action.formatDocument').run();
     };
 
     /**
      * Toggle the minimap
      */
-    toggleMinimap () {
+    toggleMinimap() {
         var minimapToggle = document.getElementById("minimapToggle1280");
         if (minimapToggle.classList.contains('checked')) {
             this.jsEditor.updateOptions({ minimap: { enabled: false } });
@@ -306,11 +395,11 @@ class MonacoCreator {
             const result = await languageService.getEmitOutput(uriStr);
             const diagnostics = await Promise.all([languageService.getSyntacticDiagnostics(uriStr), languageService.getSemanticDiagnostics(uriStr)]);
 
-            diagnostics.forEach(function(diagset) {
+            diagnostics.forEach(function (diagset) {
                 if (diagset.length) {
                     const diagnostic = diagset[0];
                     const position = model.getPositionAt(diagnostic.start);
-                    
+
                     const error = new EvalError(diagnostic.messageText);
                     error.lineNumber = position.lineNumber;
                     error.columnNumber = position.column;