From cf2c56c7061b7ed40fdd3b40a352ddb9c9b7371f Mon Sep 17 00:00:00 2001 From: alexwl Date: Tue, 2 Oct 2018 13:17:04 +0300 Subject: Initial commit --- javascript/app/components/.gitkeep | 0 javascript/app/components/bottom-panel.js | 52 +++ javascript/app/components/expression-info.js | 3 + javascript/app/components/file-tree.js | 94 ++++ javascript/app/components/haskell-module.js | 492 +++++++++++++++++++++ javascript/app/components/identifier-info.js | 62 +++ javascript/app/components/identifier-name.js | 60 +++ javascript/app/components/infinite-list.js | 50 +++ javascript/app/components/info-window.js | 144 ++++++ .../app/components/input-with-autocomplete.js | 131 ++++++ javascript/app/components/instance-info.js | 21 + javascript/app/components/paginated-list.js | 48 ++ javascript/app/components/resizable-panel.js | 72 +++ javascript/app/components/text-file.js | 67 +++ javascript/app/components/type-component.js | 29 ++ javascript/app/components/type-signature-text.js | 4 + javascript/app/components/type-signature.js | 23 + 17 files changed, 1352 insertions(+) create mode 100644 javascript/app/components/.gitkeep create mode 100644 javascript/app/components/bottom-panel.js create mode 100644 javascript/app/components/expression-info.js create mode 100644 javascript/app/components/file-tree.js create mode 100644 javascript/app/components/haskell-module.js create mode 100644 javascript/app/components/identifier-info.js create mode 100644 javascript/app/components/identifier-name.js create mode 100644 javascript/app/components/infinite-list.js create mode 100644 javascript/app/components/info-window.js create mode 100644 javascript/app/components/input-with-autocomplete.js create mode 100644 javascript/app/components/instance-info.js create mode 100644 javascript/app/components/paginated-list.js create mode 100644 javascript/app/components/resizable-panel.js create mode 100644 javascript/app/components/text-file.js create mode 100644 javascript/app/components/type-component.js create mode 100644 javascript/app/components/type-signature-text.js create mode 100644 javascript/app/components/type-signature.js (limited to 'javascript/app/components') diff --git a/javascript/app/components/.gitkeep b/javascript/app/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/javascript/app/components/bottom-panel.js b/javascript/app/components/bottom-panel.js new file mode 100644 index 0000000..5d8b5bc --- /dev/null +++ b/javascript/app/components/bottom-panel.js @@ -0,0 +1,52 @@ +import Ember from 'ember'; + +function show(component) { + const height = Math.floor(component.$containerElement.height() /2); + component.$().css({ + "display":"block", + "top" : height+"px" + }); + component.$topPanelElement.css({ + "height":height+"px" + }); +} + +function hide(component) { + const height = Math.floor(component.$containerElement.height()/2); + component.$().css({ + "display":"none", + "height":height+"px" + }); + component.$topPanelElement.css({ + "height":"100%" + }); +} + +export default Ember.Component.extend({ + classNames:["bottom-panel"], + didInsertElement : function () { + this._super(...arguments); + this.$topPanelElement = Ember.$(this.get('topPanelElementId')); + this.$containerElement = Ember.$(this.get('containerElementId')); + Ember.run.next(this,() => { + Ember.$(this.element).resizable({ + handles:"n", + maxHeight:700, + minHeight:200, + resize: (event,ui) => { + Ember.run.next(this,() => { + this.$topPanelElement.css({"height": this.$containerElement.height() - ui.size.height}); + }); + } + }); + }); + }, + visibilityObserver : Ember.observer('visible',function () { + this.get('visible') ? show(this) : hide(this); + }), + actions : { + close () { + this.set('visible',false); + } + } +}); diff --git a/javascript/app/components/expression-info.js b/javascript/app/components/expression-info.js new file mode 100644 index 0000000..d403c49 --- /dev/null +++ b/javascript/app/components/expression-info.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; +export default Ember.Component.extend({ +}); diff --git a/javascript/app/components/file-tree.js b/javascript/app/components/file-tree.js new file mode 100644 index 0000000..c86a71d --- /dev/null +++ b/javascript/app/components/file-tree.js @@ -0,0 +1,94 @@ +import Ember from 'ember'; + +const directoryTreeToJsTree = function (packageId,directoryTree) { + return directoryTree.contents.map((node) => { + const jsTreeNode = {}; + jsTreeNode.text = node.name; + jsTreeNode.data = node; + if(node.path) { + jsTreeNode.id = node.path; + jsTreeNode.a_attr = {href:"/package/" + packageId + "/show/" + node.path}; + } + if(node.tag === "Dir") { + jsTreeNode.children = directoryTreeToJsTree(packageId,node); + jsTreeNode.state = {"opened" : containsHaskellModule(node)}; + } else { + if(node.isHaskellModule) { + jsTreeNode.icon = "/assets/haskell.ico"; + jsTreeNode.isHaskellModule = true; + } else { + jsTreeNode.icon = "jstree-file"; + jsTreeNode.isHaskellModule = false; + } + } + return jsTreeNode; + }); +}; + +const containsHaskellModule = function(node) { + return node.contents.some((n) => { + if(n.tag === "File") { + return n.isHaskellModule; + } else { + return containsHaskellModule(n); + } + }); +} + +export default Ember.Component.extend({ + query: null, + didInsertElement : function () { + this._super(...arguments); + const element = this.element.getElementsByClassName('file-tree')[0]; + + const jstreeElement = Ember.$(element).jstree({ + 'core' : { + 'data' : directoryTreeToJsTree(this.get('packageId'),this.get('directoryTree')) + }, + "plugins" : [ + "search" + ], + "search": { + "case_insensitive": true, + "show_only_matches" : true, + "show_only_matches_children": true + } + }); + + jstreeElement.on("select_node.jstree",(event,data) => { + const file = data.node.data; + if(file.tag != "Dir") { + this.sendAction('openFile',file.path); + } + }); + + const jstree = jstreeElement.jstree(true); + + if(this.get('currentFile')) { + jstree.select_node(this.get('currentFile')); + const node = jstree.get_node(this.get('currentFile'),true)[0]; + if(node) { + node.scrollIntoView(); + } + } + this.jstree = jstree; + }, + currentFileObserver : Ember.observer('currentFile',function() { + Ember.run.next(() => { + this.jstree.deselect_all(); + this.jstree.select_node(this.get('currentFile')); + }); + }), + queryObserver : Ember.observer('query',function() { + if(this.get('query')) { + this.jstree.search(this.get('query')); + } else { + this.jstree.clear_search(); + } + }), + actions : { + hide() { + this.get('hide')(); + } + } +}); diff --git a/javascript/app/components/haskell-module.js b/javascript/app/components/haskell-module.js new file mode 100644 index 0000000..8883a9c --- /dev/null +++ b/javascript/app/components/haskell-module.js @@ -0,0 +1,492 @@ +import Ember from 'ember'; +import {goToDefinition} from '../utils/go-to-definition'; +import {initializeLineSelection} from '../utils/line-selection'; + +function compareLocations (p1,p2) { + if(p1.line === p2.line) { + if(p1.column === p2.column) { + return 0; + } else if(p1.column > p2.column) { + return 1; + } else { + return -1; + } + } else if(p1.line > p2.line) { + return 1; + } else { + return -1; + } +} + +function buildSrcSpan(sourceCodeLines,start,end) { + if(sourceCodeLines[start.line] && sourceCodeLines[end.line]) { + if(start.line === end.line) { + return sourceCodeLines[start.line].slice(start.column-1,end.column-1); + } else { + const firstLine = sourceCodeLines[start.line]; + let middleLines = []; + for(let i = start.line + 1; i < end.line;i ++) { + middleLines.push(sourceCodeLines[i]); + } + const lastLine = sourceCodeLines[end.line]; + const minOffset = Math.min(start.column, + (middleLines.concat([lastLine])) + .map((line) => line.search(/\S/)) + .reduce((min,value) => Math.min(min,value))); + return firstLine.slice(start.column-1,firstLine.length) + "\n" + + middleLines.map((line) => line.slice(minOffset,line.length)).join("\n") + + (middleLines.length ? "\n" : "") + lastLine.slice(minOffset,end.column-1); + } + } else { + return null; + } +} + +function modifyClass(element,on) { + if(on) { + element.classList.add('highlighted-identifier'); + } else { + element.classList.remove('highlighted-identifier'); + } +} + +function highlightIdentifiers(parentElement,identifierElement,on) { + if(identifierElement.id) { + const identifiers = Array.prototype.slice.call(parentElement.querySelectorAll("span[id='"+identifierElement.id+"']")); + identifiers.forEach((identifier) => { + modifyClass(identifier,on); + }); + } else { + modifyClass(identifierElement,on);//Literal + } +} + +//divident is a string +//divident may have any number of digits +function modulo(divident, divisor) { + return Array.from(divident).map(c => parseInt(c)) + .reduce((acc, value) => { + return (acc * 10 + value) % divisor; + },0); +} + +function isDefinedInCurrentModule(moduleName,modulePath,identifierInfo) { + return (identifierInfo.sort === "External") && + (identifierInfo.locationInfo.modulePath === modulePath + || identifierInfo.locationInfo.moduleName === moduleName) +} + +function identifierStyle(identifierElement, + identifiers, + occurrences, + path, + colorTheme, + moduleName) { + const idOcc = occurrences[identifierElement.dataset.occurrence]; + + let color = colorTheme.defaultColor; + let fontWeight; + + if(idOcc) { + if(idOcc.sort.tag === 'TypeId') { + color = colorTheme.typeColor; + } else if(idOcc.description === "HsLit" || + idOcc.description === "HsOverLit"|| + idOcc.description === "LitPat" || + idOcc.description === "NPat" || + idOcc.description === "NPlusKPat" || + idOcc.description === "OverLit") { + color = colorTheme.literalColor; + } else { + const idInfo = identifiers[identifierElement.dataset.identifier]; + if(idInfo) { + if(isDefinedInCurrentModule(moduleName,path,idInfo)) { + color = colorTheme.topLevelIdFromCurrentModule; + } else if(idInfo.sort === "Internal" && idInfo.locationInfo.tag === "ExactLocation") { + const colorNumber = modulo(identifierElement.id,colorTheme.localIdentifierColor.length); + color = colorTheme.localIdentifierColor[colorNumber]; + fontWeight = "bold"; + } + } + } + } + + return "color:"+color+";" + +(fontWeight ? "font-weight:" + fontWeight : "")+";" + +(idOcc.isBinder ? "text-decoration:underline;" : ""); +} + +function initializeIdentifiers (sourceCodeContainerElement,component) { + const identifierElements = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("span.identifier")); + if(identifierElements.length > 0) { + const timeout = 250;//milliseconds + let timer = null; + + identifierElements.forEach((identifierElement) => { + + const cssText = identifierStyle(identifierElement, + component.get('identifiers'), + component.get('occurrences'), + component.get('path'), + component.get('colorTheme'), + component.get('name')); + + identifierElement.style.cssText = cssText; + + //go to definition + identifierElement.onmouseup = (event) => { + if(timer) { + clearTimeout(timer); + } + + if(!window.getSelection().isCollapsed) { + return; + } + + const identifierInfo = component.get('identifiers')[identifierElement.dataset.identifier]; + const idOccurrenceInfo = component.get('occurrences')[identifierElement.dataset.occurrence]; + + const currentLineNumber = parseInt(identifierElement.parentNode.dataset.line); + + if(idOccurrenceInfo.sort.tag === "ModuleId") { + goToDefinition(component.get('store'), + idOccurrenceInfo.sort.contents, + event.which, + currentLineNumber); + } + else { + if(!idOccurrenceInfo.isBinder && identifierInfo + && (event.which === 1 || event.which === 2)) { + goToDefinition(component.get('store'), + identifierInfo.locationInfo, + event.which, + currentLineNumber); + } + } + } + + identifierElement.onmouseover = () => { + highlightIdentifiers(sourceCodeContainerElement,identifierElement,true); + if(timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + Ember.run.next(component,() => { + const identifierInfo = component.get('identifiers')[identifierElement.dataset.identifier]; + const identifierOccurrence = component.get('occurrences')[identifierElement.dataset.occurrence]; + console.log(identifierOccurrence); + console.log(identifierInfo); + + component.set('selectedIdentifier',identifierElement); + component.set('currentLineNumber',parseInt(identifierElement.parentNode.dataset.line) || 1); + component.set('identifierInfo',identifierInfo); + component.set('identifierOccurrence',identifierOccurrence); + component.set('hasSelectedExpression',false); + component.set('isHoveredOverIdentifier',true); + + }); + },timeout); + }; + + identifierElement.onmouseout = () => { + highlightIdentifiers(sourceCodeContainerElement,identifierElement,false); + + if(timer) { + clearTimeout(timer); + } + + timer = setTimeout (() => { + Ember.run.next(component,() => { + component.set('isHoveredOverIdentifier',false); + }); + },timeout); + }; + }); + component.timer = timer; + } +} + + + +function contains (node, other) { + return node === other || !!(node.compareDocumentPosition(other) & 16); +} + +function initializeExpressionInfo(sourceCodeContainerElement,component) { + const lineElements = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("td.line-content")); + if(lineElements.length > 0) { + + //Line numbers start with 1 + let sourceCodeLines = [""]; + + lineElements.forEach((el) => { + sourceCodeLines.push(el.textContent); + }); + + const allowedNodeNames = ["#text","SPAN","TD"]; + let isLoading = false; + let shouldWait = false; + const timeout = 400;//milliseconds + + const onmouseup = function() { + Ember.run.next(() => { + if(isLoading || shouldWait) { + return; + } + shouldWait = true; + setTimeout(() => {shouldWait = false;},timeout); + + component.set('hasSelectedExpression',false); + + const selection = window.getSelection(); + + //Selection of multiple lines inside a table doesn't work in Firefox + //https://bugzilla.mozilla.org/show_bug.cgi?id=365900 + + if(!(selection.anchorNode && selection.focusNode) + || !contains(sourceCodeContainerElement,selection.anchorNode) + || !contains(sourceCodeContainerElement,selection.focusNode) + || (allowedNodeNames.indexOf(selection.anchorNode.nodeName) === -1) + || (allowedNodeNames.indexOf(selection.focusNode.nodeName) === -1) + || selection.isCollapsed) { + return; + } + + // Detects whether the selection is backwards + const detectionRange = document.createRange(); + detectionRange.setStart(selection.anchorNode, selection.anchorOffset); + detectionRange.setEnd(selection.focusNode, selection.focusOffset); + const isBackward = detectionRange.collapsed; + + let startNode,startNodeOffset,endNode,endNodeOffset; + + if(isBackward) { + startNode = selection.focusNode; + startNodeOffset = selection.focusOffset; + endNode = selection.anchorNode; + endNodeOffset = selection.anchorOffset; + } else { + startNode = selection.anchorNode; + startNodeOffset = selection.anchorOffset; + endNode = selection.focusNode; + endNodeOffset = selection.focusOffset; + } + + let lineStart,columnStart,lineEnd,columnEnd; + let infoWindowTargetElement; + + + //HTML inside source code container : + //abc... + //... + if(startNode.nodeName === "#text") { + const parent = startNode.parentNode;// + columnStart = parseInt(parent.dataset.start) + startNodeOffset; + lineStart = parseInt(parent.parentNode.dataset.line); + + if(startNodeOffset === startNode.textContent.length && parent.nextSibling === null) { + const tr = startNode.parentNode.parentNode.parentNode;// span -> td -> tr + + //Skipping empty lines + let nextLine = tr.nextSibling; + while(nextLine.children[1].textContent === "") { + nextLine = nextLine.nextSibling; + } + infoWindowTargetElement = nextLine.children[1].children[0]; + + } else { + if(!(startNodeOffset === 0) && (parent.nextSibling)) { + infoWindowTargetElement = parent.nextSibling; + } else { + infoWindowTargetElement = parent; + } + } + } else if(startNode.nodeName === "SPAN") { + columnStart = 1; + lineStart = parseInt(startNode.parentNode.dataset.line); + + const tr = startNode.parentNode.parentNode; // td -> tr + let nextLine = tr.nextSibling; + while(nextLine.children[1].textContent === "") { + nextLine = nextLine.nextSibling; + } + infoWindowTargetElement = nextLine.children[1].children[0]; + + } else if(startNode.nodeName === "TD") { + if(startNodeOffset > 0) { + const child = startNode.children[startNodeOffset-1]; + columnStart = parseInt(child.dataset.start); + } else { + columnStart = 1; + } + lineStart = parseInt(startNode.id.slice(2)); + infoWindowTargetElement = startNode.children[0]; + } + + if(endNode.nodeName === "#text") { + columnEnd = parseInt(endNode.parentNode.dataset.start) + endNodeOffset; + lineEnd = parseInt(endNode.parentNode.parentNode.dataset.line); + } else if(endNode.nodeName === "SPAN") { + columnEnd = 1; + lineEnd = parseInt(endNode.parentNode.dataset.line); + } else if(endNode.nodeName === "TD"){ + if(endNodeOffset > 0) { + const child = endNode.children[endNodeOffset-1]; + columnEnd = parseInt(child.dataset.start); + } else { + columnEnd = 1; + } + lineEnd = parseInt(endNode.id.slice(2)); + } + + const loadExprPromise = component.get('store').loadExpressions( + component.get('packageId'), + component.get('path'), + lineStart, + columnStart, + lineEnd, + columnEnd); + isLoading = true; + + loadExprPromise.then((expressions) => { + Ember.run.next(() => { + if(expressions && expressions.length > 0) { + expressions.sort(function(expr1,expr2) { + if( compareLocations(expr1.srcSpan.start,expr2.srcSpan.start) <= 0 + && compareLocations(expr1.srcSpan.end,expr2.srcSpan.end) >= 0 ) { + return -1; + } else { + return 1; + } + }); + + const expressionsWithSourceCode = expressions.reduce((result,expression) => { + const object = Ember.copy(expression); + const srcSpan = buildSrcSpan(sourceCodeLines, + expression.srcSpan.start, + expression.srcSpan.end); + if(srcSpan) { + object.sourceCode = srcSpan; + return result.concat(object); + } else { + return result; + } + },[]); + + if(expressionsWithSourceCode.length > 0) { + component.set('selectedIdentifier',infoWindowTargetElement); + component.set('expressions',expressionsWithSourceCode); + component.set('currentLineNumber',parseInt(infoWindowTargetElement.parentNode.dataset.line) || 1); + component.set('hasSelectedExpression',true); + + } + } + isLoading = false; + }); + }); + }); + }; + + sourceCodeContainerElement.addEventListener('mouseup',onmouseup); + component._onmouseup = onmouseup; + } +} + +export default Ember.Component.extend({ + store : Ember.inject.service('store'), + selectedIdentifier : null, + isHoveredOverIdentifier : false, + hasSelectedExpression : false, + showDeclarations : true, + showDeclarationsLabel : Ember.computed('showDeclarations',function () { + return this.get('showDeclarations') ? "Hide" : "Show"; + }), + queryObserver : Ember.observer("query",function() { + Ember.run.debounce(this, () => { + const regExp = new RegExp(this.get('query'),"i"); + const filteredDeclarations = this.get('declarations').filter((d) => d.name.search(regExp) != -1); + Ember.run.next(() => { + this.set('filteredDeclarations',filteredDeclarations); + }); + }, 300); + }), + identifierLocationInfo : Ember.computed('identifierInfo','identifierOccurrence',function() { + const idOcc = this.get('identifierOccurrence'); + const idInfo = this.get('identifierInfo'); + if(idOcc) { + if(idOcc.sort.tag === "ModuleId") { + return idOcc.sort.contents; + } else { + if(idInfo) { + return idInfo.locationInfo; + } else { + return null; + } + } + } + }), + themeObserver : Ember.observer('colorTheme',function() { + Ember.run.next(this,() => { + this.cleanup(); + this.didInsertElement(); + }); + }), + fileObserver : Ember.observer('path',function() { + Ember.run.next(this,() => { + this.cleanup(); + this.didInsertElement(); + }); + }), + cleanup() { + if(this.timer) { + clearTimeout(this.timer); + } + if(this._onhashchange) { + window.removeEventListener('hashchange',this._onhashchange); + } + if(this._onkeydown) { + document.removeEventListener('keydown',this._onkeydown); + } + if(this._onkeyup) { + document.removeEventListener('keyup',this._onkeyup); + } + if(this._onmouseup) { + this.sourceCodeContainerElement.removeEventListener('mouseup',this._onmouseup); + } + this.set('selectedIdentifier',null); + this.set('isHoveredOverIdentifier',false); + this.set('hasSelectedExpression',false); + this.set('showDeclarations',true); + }, + didReceiveAttrs() { + this.set('filteredDeclarations',this.get('declarations')); + }, + didInsertElement() { + this._super(...arguments); + const sourceCodeContainerElement = this.element.querySelector('.source-code-container'); + sourceCodeContainerElement.innerHTML = this.get('html'); + this.sourceCodeContainerElement = sourceCodeContainerElement; + this.element.parentNode.scrollTop = 0; + const declarations = this.element.querySelector('.declarations-content'); + this.set('query',''); + if(declarations) { + declarations.scrollTop = 0; + } + Ember.run.next(this,() => { + initializeIdentifiers(sourceCodeContainerElement,this); + initializeLineSelection(sourceCodeContainerElement,this); + initializeExpressionInfo(sourceCodeContainerElement,this); + }); + }, + willDestroyElement() { + this.cleanup(); + }, + actions : { + goToLine(lineNumber) { + window.location.hash = "L"+lineNumber; + }, + toggleShowDeclarations() { + this.toggleProperty('showDeclarations'); + } + } +}); diff --git a/javascript/app/components/identifier-info.js b/javascript/app/components/identifier-info.js new file mode 100644 index 0000000..537697a --- /dev/null +++ b/javascript/app/components/identifier-info.js @@ -0,0 +1,62 @@ +import Ember from 'ember'; +import {goToDefinition} from '../utils/go-to-definition'; + +export default Ember.Component.extend({ + store : Ember.inject.service('store'), + downloadedDocumentation : null, + didInsertElement () { + const onmouseup = (event) => { + if(event.target.dataset.location) { + let location; + try { + location = JSON.parse(event.target.dataset.location); + } catch (e) { + console.log(e); + } + if(location) { + goToDefinition(this.get('store'),location,event.which,this.get('currentLineNumber')); + } + } + }; + this.element.addEventListener('mouseup',onmouseup); + this._onmouseup = onmouseup; + }, + willDestroyElement : function () { + if(this._onmouseup) { + this.element.removeEventListener('mouseup',this._onmouseup); + } + }, + //Naughty record selectors : + //https://github.com/ghc/ghc/blob/ced2cb5e8fbf4493488d1c336da7b00d174923ce/compiler/typecheck/TcTyDecls.hs#L940-L961 + isNaughtyRecSel : Ember.computed('identifierInfo',function () { + const idInfo = this.get('identifierInfo'); + return idInfo ? (idInfo.details === "RecSelIdNaughty") : false; + }), + isExternalIdentifier : Ember.computed('identifierInfo',function () { + const idInfo = this.get('identifierInfo'); + return idInfo ? (idInfo.sort === "External") : false; + }), + identifierObserver : Ember.observer('identifierInfo',function () { + this.set("downloadedDocumentation",""); + const idInfo = this.get('identifierInfo'); + if(idInfo) { + const locationInfo = idInfo.locationInfo; + if(locationInfo.tag === "ApproximateLocation") { + const packageId = locationInfo.packageId.name + "-" + locationInfo.packageId.version; + const currentIdentifier = idInfo; + + this.get('store').loadDefinitionSite(packageId, + locationInfo.moduleName, + locationInfo.componentId, + locationInfo.entity, + locationInfo.name) + .then((definitionSite) => { + Ember.run.next(this,() => { + if(currentIdentifier === this.get('identifierInfo')) { + this.set('downloadedDocumentation',definitionSite.documentation); + }}); + }); + } + } + }) +}); diff --git a/javascript/app/components/identifier-name.js b/javascript/app/components/identifier-name.js new file mode 100644 index 0000000..950b8c7 --- /dev/null +++ b/javascript/app/components/identifier-name.js @@ -0,0 +1,60 @@ +import Ember from 'ember'; +import {goToDefinition} from '../utils/go-to-definition'; + +export default Ember.Component.extend({ + store : Ember.inject.service('store'), + name : Ember.computed('identifierElement',function() { + const element = this.get('identifierElement'); + if(element) { + return element.innerText; + } + }), + style : Ember.computed('identifierElement',function() { + const element = this.get('identifierElement'); + if(element) { + return new Ember.String.htmlSafe("color:"+element.style.color); + } + }), + locationInfo : Ember.computed('identifierInfo','identifierOccurrence',function() { + if(this.get('identifierOccurrence.sort.tag') === "ModuleId") { + return this.get('identifierOccurrence.sort.contents'); + } else { + return this.get('identifierInfo.locationInfo'); + } + }), + location : Ember.computed('locationInfo',function() { + const loc = this.get('locationInfo'); + if(loc) { + if(loc.tag === "ExactLocation") { + return loc.modulePath; + } else if(loc.tag === "ApproximateLocation") { + if(loc.entity === "Mod") { + return loc.packageId.name + "-" + loc.packageId.version; + } else { + return loc.packageId.name + "-" + loc.packageId.version + " " + loc.moduleName; + } + } else { + return loc.contents; + } + } else { + return ""; + } + }), + isExternalIdentifier : Ember.computed('identifierInfo',function () { + return (this.get('identifierInfo.sort') === "External"); + }), + actions : { + goToDefinition (event) { + goToDefinition(this.get('store'), + this.get('locationInfo'), + event.which, + this.get('currentLineNumber')); + return false; + }, + findReferences (identifierInfo,currentPackageId) { + this.get('findReferences')(currentPackageId, + identifierInfo.externalId, + identifierInfo.demangledOccName); + } + } +}); diff --git a/javascript/app/components/infinite-list.js b/javascript/app/components/infinite-list.js new file mode 100644 index 0000000..b73b6d4 --- /dev/null +++ b/javascript/app/components/infinite-list.js @@ -0,0 +1,50 @@ +import Component from '@ember/component'; +import { run } from '@ember/runloop'; +import { observer } from '@ember/object'; + +let pageNumber; +let updating = false; + +function initialize(component) { + component.set('renderedElements',component.get('elements').slice(0,component.get('perPage'))); + pageNumber = 1; +} + +export default Component.extend({ + renderedElements : [], + init() { + this._super(...arguments); + initialize(this); + }, + elementsObserver : observer('elements',function() { + initialize(this); + const containerElement = document.getElementById(this.get('containerElementId')); + if(containerElement) { + containerElement.scrollTop = 0; + } + }), + didInsertElement() { + const containerElement = document.getElementById(this.get('containerElementId')); + if(containerElement) { + const component = this; + containerElement.onscroll = function() { + const perPage = component.get('perPage'); + const elements = component.get('elements'); + + if(!updating && + (pageNumber * perPage < elements.length) && + (containerElement.scrollTop + containerElement.offsetHeight + > component.element.offsetHeight - 100)) { + + updating = true; + run.next(component,() => { + const newElements = elements.slice(pageNumber * perPage,(pageNumber + 1) * perPage); + component.get('renderedElements').pushObjects(newElements); + pageNumber ++; + updating = false; + }); + } + } + } + } +}); diff --git a/javascript/app/components/info-window.js b/javascript/app/components/info-window.js new file mode 100644 index 0000000..a011f99 --- /dev/null +++ b/javascript/app/components/info-window.js @@ -0,0 +1,144 @@ +import Ember from 'ember'; + +let resizing = false; +let dragging = false; + +function updatePosition(component) { + const targetElement = component.get('targetElement'); + if(targetElement) { + const infoWindowHeight = component.element.offsetHeight; + const targetElementHeight = targetElement.offsetHeight; + + const parent = targetElement.parentNode;// element + const containerElement = document.querySelector("#" + component.get('containerElementId')); + + //getBoundingClientRect() returns the smallest rectangle which contains + //the entire element, with read-only left, top, right, bottom, x, y, width, + //and height properties describing the overall border-box in pixels. Properties + //other than width and height are relative to the top-left of the *viewport*. + const targetTopViewport = targetElement.getBoundingClientRect().top; + + let containerTopViewport; + if (containerElement) { + containerTopViewport = containerElement.getBoundingClientRect().top; + } else { + containerTopViewport = 0; + } + + let infoWindowTop; + if(targetTopViewport < infoWindowHeight + containerTopViewport) { + //offsetTop is the number of pixels from the top of the closest relatively + //positioned parent element. + infoWindowTop = targetElement.offsetTop + parent.offsetTop + + targetElementHeight + 10 + "px"; + } else { + infoWindowTop = targetElement.offsetTop + parent.offsetTop + - infoWindowHeight + "px"; + } + + const infoWindowLeft = targetElement.offsetLeft + parent.offsetLeft + "px"; + + component.$().css({ + top:infoWindowTop, + left:infoWindowLeft + }); + } else { + component.set('isPinned',false); + } +} + +export default Ember.Component.extend({ + classNames : ["info-window-container"], + attributeBindings: ['hidden'], + isPinned : false, + isFocused: false, + didInsertElement () { + const component = this; + + const $headerElement = Ember.$(component.element.querySelector(".info-window-header")); + const $contentElement = Ember.$(component.element.querySelector(".info-window-content")); + const $infoWindowElement = Ember.$(component.element.querySelector(".info-window")); + const $infoWindowContainerElement = Ember.$(component.element); + + this.$headerElement = $headerElement; + this.$contentElement = $contentElement; + + this.$().resizable({ + handles: "n,w", + minHeight: 80, + minWidth: 400, + start: function() { + resizing = true; + }, + stop: function() { + resizing = false; + }, + resize : function() { + const containerHeight = $infoWindowContainerElement.height(); + $infoWindowElement.css({ + "height": containerHeight + 2 + "px" + }); + $contentElement.css({ + "max-height":(containerHeight - $headerElement.outerHeight(true)) + "px" + }); + } + }); + this.$().draggable({ + containment:"#" + this.get('containerElementId'), + handle: $headerElement, + start: function() { + dragging = true; + }, + stop: function() { + dragging = false; + } + }); + }, + mouseEnter () { + if(!this.get('hasSelectedExpression')) { + this.set('isFocused',true); + } + }, + mouseLeave (event) { + //Workaround for a bug in Chrome + const element = document.elementFromPoint(event.clientX,event.clientY); + if(element && element.classList.contains('link')) { + return; + } + if(!resizing + && !dragging + && !this.get('isPinned') + && !this.get('hasSelectedExpression')) { + this.set('isFocused',false); + } + }, + hidden : Ember.computed('isHoveredOverIdentifier', + 'isFocused', + 'hasSelectedExpression', + 'isPinned', function() { + if (this.$contentElement) { + this.$contentElement.scrollTop(0); + } + if (this.get('isPinned') + || this.get('isFocused') + || this.get('isHoveredOverIdentifier') + || this.get('hasSelectedExpression')) { + return false; + } else { + return true; + } + }), + didUpdate() { + updatePosition(this); + }, + actions : { + close() { + this.set('isPinned',false); + this.set('isFocused',false); + this.set('hasSelectedExpression',false); + }, + pin() { + this.toggleProperty('isPinned'); + } + } +}); diff --git a/javascript/app/components/input-with-autocomplete.js b/javascript/app/components/input-with-autocomplete.js new file mode 100644 index 0000000..2b09ea4 --- /dev/null +++ b/javascript/app/components/input-with-autocomplete.js @@ -0,0 +1,131 @@ +import Ember from 'ember'; +export default Ember.Component.extend({ + store : Ember.inject.service('store'), + highlightedItemIndex: -1, + items : [], + query: null, + didInsertElement() { + const $input = Ember.$(this.element).find(".search-input"); + const $autocompleteContainer = Ember.$(this.element).find(".autocomplete-container"); + this.$input = $input; + this.$autocompleteContainer = $autocompleteContainer; + const width = $input.width() + 300; + $autocompleteContainer.css({ + "width" : width+"px", + "top" : $input.outerHeight() + }); + $input.keyup((e) => { + if(e.which === 13) { + this.onEnter(); + } else if(e.which === 27) { + this.onEsc(); + } else if(e.which === 40) { + this.onDown(); + } else if(e.which === 38) { + this.onUp(); + } + }); + $input.focusin(() => { + this.showAutocompleteList(); + }); + $input.focusout(() => { + //Timeout is needed to make sure that click event fires + Ember.run.later((() => { + this.hideAutocompleteList(); + }), 100); + }); + }, + willDestroyElement() { + this._super(...arguments); + this.$input.off('keyup'); + this.$input.off('focusin'); + this.$input.off('focusout'); + }, + onEnter() { + if(this.get('highlightedItemIndex') !== -1) { + const item = this.get('items')[this.get('highlightedItemIndex')]; + if(item) { + this.hideAutocompleteList(); + this.get('selectItem')(item); + } + } else { + this.hideAutocompleteList(); + this.get('onSubmit')(this.get('query')); + } + }, + onEsc() { + this.hideAutocompleteList(); + }, + onDown() { + this.showAutocompleteList(); + const index = this.get('highlightedItemIndex'); + const items = this.get('items'); + const itemsCount = items.length; + if(itemsCount > 0) { + if(index !== -1) { + if(index === itemsCount - 1) { + this.set('highlightedItemIndex',0); + } else { + this.set('highlightedItemIndex',index+1); + } + } else { + this.set('highlightedItemIndex',0); + } + } + }, + onUp() { + this.showAutocompleteList(); + const index = this.get('highlightedItemIndex'); + const items = this.get('items'); + const itemsCount = items.length; + if(itemsCount > 0) { + if(index !== -1) { + if(index === 0) { + this.set('highlightedItemIndex',itemsCount - 1); + } else { + this.set('highlightedItemIndex',index - 1); + } + } else { + this.set('highlightedItemIndex',itemsCount - 1); + } + } + }, + hideAutocompleteList() { + this.set('highlightedItemIndex',-1); + this.$autocompleteContainer.css({ + "display":"none", + }); + }, + showAutocompleteList() { + this.$autocompleteContainer.css({ + "display":"block" + }); + }, + queryObserver : Ember.observer("query",function() { + if(this.get('query')) { + const perPage = this.get('maxItems') ? this.get('maxItems') : 10; + const url = this.get('createSearchUrlFunction')(this.get('query')) + "?per_page=" + perPage; + Ember.run.debounce(this, () => { + this.get('store').loadFromUrlPaginated(url).then((result) => { + Ember.run.next(() => { + this.set('items',result.items); + }); + }); + }, 400); + this.showAutocompleteList(); + } else { + this.hideAutocompleteList(); + this.set('items',[]); + } + }), + actions : { + onSubmit() { + this.hideAutocompleteList(); + this.get('onSubmit')(this.get('query')); + }, + goToDefinition (item) { + this.hideAutocompleteList(); + this.get('selectItem')(item); + } + } +}); diff --git a/javascript/app/components/instance-info.js b/javascript/app/components/instance-info.js new file mode 100644 index 0000000..a0e04ed --- /dev/null +++ b/javascript/app/components/instance-info.js @@ -0,0 +1,21 @@ +import Ember from 'ember'; +import {goToDefinition} from '../utils/go-to-definition'; + +export default Ember.Component.extend({ + store : Ember.inject.service('store'), + style : Ember.computed('nestedLevel',function() { + return new Ember.String.htmlSafe("margin-left :" + this.get('nestedLevel') * 10 + "px"); + }), + nextNestedLevel : Ember.computed('nestedLevel',function () { + return this.get('nestedLevel') + 1; + }), + actions : { + goToDefinition (event) { + goToDefinition(this.get('store'), + this.get('instance.location'), + event.which, + this.get('currentLineNumber')); + return false; + } + } +}); diff --git a/javascript/app/components/paginated-list.js b/javascript/app/components/paginated-list.js new file mode 100644 index 0000000..9d85610 --- /dev/null +++ b/javascript/app/components/paginated-list.js @@ -0,0 +1,48 @@ +import Ember from 'ember'; +function loadItems(store,component,url) { + store.loadFromUrlPaginated(url).then((result) => { + Ember.run.next(() => { + component.set('total',result.total); + component.set('items',result.items); + component.set('first',result.linkHeader.first); + component.set('next',result.linkHeader.next); + component.set('prev',result.linkHeader.prev); + component.set('last',result.linkHeader.last); + + const pageMatch = url.match(/(&|\?)page=(\d+)/); + const perPageMatch = url.match(/(&|\?)per_page=(\d+)/); + + const page = pageMatch ? pageMatch[2] : 1; + const perPage = perPageMatch ? perPageMatch[2] : 20; + + if(result.linkHeader.next || result.linkHeader.prev) { + component.set('firstItemOnPage',(page - 1) * perPage + 1); + if(!result.linkHeader.last) { + component.set('lastItemOnPage',result.total); + } else { + component.set('lastItemOnPage',page * perPage); + } + } + }); + }); +} + +export default Ember.Component.extend({ + store : Ember.inject.service('store'), + init() { + this._super(...arguments); + if(this.get('url')) { + loadItems(this.get('store'),this,this.get('url')); + } + }, + urlObserver : Ember.observer('url',function () { + loadItems(this.get('store'),this,this.get('url')); + this.element.querySelector(".paginated-list-content").scrollTop = 0; + }), + actions : { + update(url) { + this.element.querySelector(".paginated-list-content").scrollTop = 0; + loadItems(this.get('store'),this,url); + } + } +}); diff --git a/javascript/app/components/resizable-panel.js b/javascript/app/components/resizable-panel.js new file mode 100644 index 0000000..8dae7ed --- /dev/null +++ b/javascript/app/components/resizable-panel.js @@ -0,0 +1,72 @@ +import Ember from 'ember'; + +function hide (component,byUser) { + component.$alsoResizeElement.css({left: 0}); + component.$().css({width:0}); + component.set('hidden',true); + component.$(".show-left-panel-button").show(); + if(byUser) { + component.set('hiddenByUser',true); + } +} + +function show (component,byUser) { + component.$alsoResizeElement.css({left: 300}); + component.$().css({width:300}); + component.set('hidden',false); + component.$(".show-left-panel-button").hide(); + if(byUser) { + component.set('hiddenByUser',false); + } +} + +export default Ember.Component.extend({ + hidden:false, + hiddenByUser:false, + didInsertElement : function () { + this._super(...arguments); + Ember.run.next(this,() => { + const onresize = () => { + if(!this.get('hiddenByUser')) { + const width = window.innerWidth; + if(!this.get('hidden') && width < 700) { + hide(this,false); + } else if(this.get('hidden') && width > 700) { + show(this,false); + } + } + }; + this._onresize = onresize; + window.addEventListener('resize', onresize); + const $alsoResizeElement = Ember.$(this.get('alsoResizeElementId')); + Ember.$(this.element).resizable({ + maxWidth: 800, + minWidth: 200, + handles: 'e', + resize: (event,ui) => { + Ember.run.next(this,() => { + $alsoResizeElement.css({left: ui.size.width}); + }); + } + }); + this.$alsoResizeElement = $alsoResizeElement; + }); + }, + hideButtonLabel : Ember.computed('hidden',function() { + return this.get('hidden') ? ">" : "<"; + }), + willDestroyElement() { + if(this._onresize) { + window.removeEventListener('resize',this._onresize); + } + }, + actions : { + hide() { + if(this.get('hidden')) { + show(this,true); + } else { + hide(this,true); + } + } + } +}); diff --git a/javascript/app/components/text-file.js b/javascript/app/components/text-file.js new file mode 100644 index 0000000..05be31a --- /dev/null +++ b/javascript/app/components/text-file.js @@ -0,0 +1,67 @@ +/* global showdown */ +import Ember from 'ember'; +import {initializeLineSelection} from '../utils/line-selection'; + +function escapeHtml(text) { + return text.replace(/[\"&<>]/g, function (a) { + return { '"': '"', '&': '&', '<': '<', '>': '>' }[a]; + }); +} + +function addLineNumbers (text) { + const start = ""; + const end = "
"; + let lineNumber = 0; + const lines = text.split("\n").map((line) => { + lineNumber ++; + const lineNumberHtml = ""+lineNumber+""; + const lineContentHtml = ""+escapeHtml(line)+""; + return ""+ lineNumberHtml + lineContentHtml + ""; + }).join(""); + return start + lines + end; +} + +const markdownExtensions = ["markdown", "mdown", "mkdn", "mkd", "md"]; + +export default Ember.Component.extend({ + isMarkdown : Ember.computed('path',function() { + const maybeExtension = this.get('path').split('.').pop(); + return markdownExtensions.any((extension) => (maybeExtension === extension)); + }), + html : Ember.computed('path','isMarkdown',function() { + if(this.get('isMarkdown')) { + return this.markdownConverter.makeHtml(this.get('text')); + } else { + return addLineNumbers(this.get('text')); + } + }), + init() { + this._super(...arguments); + this.markdownConverter = new showdown.Converter(); + }, + didInsertElement() { + const sourceCodeContainerElement = this.element.querySelector('.source-code-container'); + initializeLineSelection(sourceCodeContainerElement,this); + this.element.parentNode.scrollTop = 0; + }, + willDestroyElement : function () { + this.cleanup(); + }, + cleanup() { + if(this._onhashchange) { + window.removeEventListener('hashchange',this._onhashchange); + } + if(this._onkeydown) { + document.removeEventListener('keydown',this._onkeydown); + } + if(this._onkeyup) { + document.removeEventListener('keyup',this._onkeyup); + } + }, + pathObserver : Ember.observer('path',function() { + Ember.run.next(this,() => { + this.cleanup(); + this.didInsertElement(); + }); + }) +}); diff --git a/javascript/app/components/type-component.js b/javascript/app/components/type-component.js new file mode 100644 index 0000000..c19facc --- /dev/null +++ b/javascript/app/components/type-component.js @@ -0,0 +1,29 @@ +import Ember from 'ember'; +import {goToDefinition} from '../utils/go-to-definition'; + +export default Ember.Component.extend({ + store : Ember.inject.service('store'), + tagName : 'span', + classNames: ["type-component"], + contextMenu() {//right mouse button click to show kind of a type constructor or type variable + if(this.get('identifiers') && this.get('internalId')) { + this.set('expanded',true); + } + return false; + }, + linkClass : Ember.computed('identifierInfo',function() { + return this.get('identifierInfo') ? "link" : ""; + }), + identifierInfo : Ember.computed('internalId',function() { + return this.get('internalId') ? this.get('identifiers')[this.get('internalId')] : null; + }), + actions : { + onmouseup (event) { + if(this.get('identifierInfo') && (event.which !== 3 )) { + const locationInfo = this.get('identifierInfo').locationInfo; + goToDefinition(this.get('store'),locationInfo,event.which,this.get('currentLineNumber')); + return false; + } + } + } +}); diff --git a/javascript/app/components/type-signature-text.js b/javascript/app/components/type-signature-text.js new file mode 100644 index 0000000..e4eb200 --- /dev/null +++ b/javascript/app/components/type-signature-text.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; +export default Ember.Component.extend({ + tagName : "span" +}); diff --git a/javascript/app/components/type-signature.js b/javascript/app/components/type-signature.js new file mode 100644 index 0000000..ed1849c --- /dev/null +++ b/javascript/app/components/type-signature.js @@ -0,0 +1,23 @@ +import Ember from 'ember'; +export default Ember.Component.extend({ + tagName : "span", + expandTypeSynonyms: false, + expandTypeSynonymsLabel : Ember.computed('expandTypeSynonyms',function() { + return this.get('expandTypeSynonyms') ? "Show type synonyms" : "Expand type synonyms"; + }), + components : Ember.computed('type','expandTypeSynonyms',function() { + if(this.get('expandTypeSynonyms') && this.get('type.componentsExpanded')) { + return this.get('type.componentsExpanded'); + } else { + return this.get('type.components'); + } + }), + typeObserver : Ember.observer('type',function() { + this.set('expandTypeSynonyms',false); + }), + actions : { + toggleExpandTypeSynonyms () { + this.toggleProperty('expandTypeSynonyms'); + } + } +}); -- cgit v1.2.3