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/haskell-module.js | 492 ++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 javascript/app/components/haskell-module.js (limited to 'javascript/app/components/haskell-module.js') 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'); + } + } +}); -- cgit v1.2.3