Initial commit
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
dist
|
||||
dist-*
|
||||
.stack-work/
|
||||
.haskell-code-explorer/
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
cabal.config
|
||||
log/
|
||||
tmp/
|
||||
TAGS
|
||||
\#*\#
|
||||
.\#*
|
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2018 Alexey Kiryushin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
116
README.md
Normal file
@ -0,0 +1,116 @@
|
||||
# Haskell code explorer
|
||||
|
||||
Haskell code explorer is a web application for exploring and understanding Haskell codebases. It provides IDE-like code intelligence features such as types and documentation on hover, "go to definition", "find references" and semantic highlighting.
|
||||
|
||||
Examples :
|
||||
|
||||
- [https://haskell-code-explorer.mfix.io/package/stm-2.4.5.0/show/Control/Concurrent/STM/TQueue.hs#L87](https://haskell-code-explorer.mfix.io/package/stm-2.4.5.0/show/Control/Concurrent/STM/TQueue.hs#L87)
|
||||
- [https://haskell-code-explorer.mfix.io/package/async-2.1.1.1/show/Control/Concurrent/Async.hs#L251](https://haskell-code-explorer.mfix.io/package/async-2.1.1.1/show/Control/Concurrent/Async.hs#L251)
|
||||
- [https://haskell-code-explorer.mfix.io/package/haxl-0.5.1.0/show/Haxl/Core/Monad.hs#L569](https://haskell-code-explorer.mfix.io/package/haxl-0.5.1.0/show/Haxl/Core/Monad.hs#L569)
|
||||
|
||||
![Haskell code explorer](https://haskell-code-explorer.mfix.io/screenshot.png)
|
||||
|
||||
Haskell code explorer consists of an indexer, an HTTP server, and a JavaScript application. The indexer uses GHC API to create a data structure that contains detailed information about the source code of a Cabal package. The HTTP server reads that data structure into memory and responds to HTTP requests from the JavaScript application.
|
||||
|
||||
## Motivation
|
||||
|
||||
Reading and understanding code is an essential part of the software development process. Understanding code in any statically typed language is much easier when code intelligence features (types on hover, go-to-definition, etc.) are available. Code intelligence for Haskell is especially useful because types are informative and precise (thanks to Haskell's purity and global type inference).
|
||||
|
||||
## Features
|
||||
|
||||
* Types (actual type, instantiated type, instance resolution tree) and documentation on hover. Types are interactive : left-click on type constructor -> go to definition, right click on type constructor -> show kind signature.<br />
|
||||
![Hover](https://haskell-code-explorer.mfix.io/hover.png)
|
||||
|
||||
* Go to definition
|
||||
|
||||
* Find references
|
||||
|
||||
* Type of each expression<br />
|
||||
![Expression](https://haskell-code-explorer.mfix.io/expressions.png)<br />
|
||||
Select a piece of text to get the type of each Haskell expression inside the selection.
|
||||
|
||||
* Semantic highlighting
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alexwl/haskell-code-explorer
|
||||
cd haskell-code-explorer
|
||||
```
|
||||
|
||||
To build Haskell code explorer Stack ([https://docs.haskellstack.org/en/stable/README/](https://docs.haskellstack.org/en/stable/README/)) is needed.
|
||||
|
||||
At the moment Haskell code explorer supports GHC 8.2.2 and 8.0.2.
|
||||
|
||||
For GHC 8.2.2:
|
||||
|
||||
```bash
|
||||
stack install
|
||||
```
|
||||
|
||||
For GHC 8.0.2:
|
||||
|
||||
```bash
|
||||
stack --stack-yaml=stack-8.0.2.yaml install
|
||||
```
|
||||
|
||||
|
||||
## Indexing source code of a Cabal package
|
||||
|
||||
`haskell-code-indexer` executable is responsible for indexing packages (by default index is saved to `.haskell-code-explorer` directory).
|
||||
|
||||
`haskell-code-indexer` requires globally installed GHC and cabal-install (`cabal`). The reason for this is that `haskell-code-indexer` uses `cabal-helper` library [https://hackage.haskell.org/package/cabal-helper](https://hackage.haskell.org/package/cabal-helper) to get package build information. `cabal-helper` builds (at runtime) an executable linked against a version of Cabal library that was used to configure the package.
|
||||
|
||||
If there is no globally installed GHC on the system, then it is possible to use `stack exec` command ([https://docs.haskellstack.org/en/stable/GUIDE/#exec](https://docs.haskellstack.org/en/stable/GUIDE/#exec)) that adds a path to GHC binaries installed by Stack to `PATH` environment variable :
|
||||
|
||||
```bash
|
||||
stack --resolver=lts-11.3 exec haskell-code-indexer -- INDEXER_OPTIONS
|
||||
```
|
||||
|
||||
### Examples :
|
||||
|
||||
Show all indexer options:
|
||||
```bash
|
||||
haskell-code-indexer -h
|
||||
```
|
||||
|
||||
Index package :
|
||||
```bash
|
||||
haskell-code-indexer --package PATH
|
||||
```
|
||||
|
||||
Index package with specific dist directory :
|
||||
```bash
|
||||
haskell-code-indexer --package PATH --dist dist-newstyle
|
||||
```
|
||||
|
||||
Index package built by Stack :
|
||||
```bash
|
||||
haskell-code-indexer --package PATH --dist $(stack path --dist-dir)
|
||||
```
|
||||
|
||||
## Starting HTTP Server
|
||||
|
||||
`haskell-code-server` executable reads the package index created by `haskell-code-indexer` and starts the HTTP server. The HTTP server responds to API requests and serves static assets (JavaScript files that are in `haskell-code-explorer/javascript/release` directory).
|
||||
|
||||
### Examples :
|
||||
|
||||
Show all server options :
|
||||
|
||||
```bash
|
||||
haskell-code-server -h
|
||||
```
|
||||
|
||||
Load the indexed package and start the server :
|
||||
|
||||
```bash
|
||||
haskell-code-server --package PATH --port 8080 --js-path haskell-code-explorer/javascript/release
|
||||
```
|
||||
|
||||
Load multiple indexed packages and start the server :
|
||||
|
||||
```bash
|
||||
haskell-code-server --package PATH1 --package PATH2 --package PATH3 --port 8080 --js-path haskell-code-explorer/javascript/release
|
||||
```
|
||||
|
||||
Open [http://localhost:8080](http://localhost:8080) in a browser to explore source code of the package.
|
208
app/Indexer.hs
Normal file
@ -0,0 +1,208 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Codec.Compression.GZip(compress)
|
||||
import Control.Exception (SomeException, handle)
|
||||
import Control.Monad (when)
|
||||
import Control.Monad.Logger (LogLevel(..), runLoggingT)
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import qualified Data.ByteString.Lazy.Char8 as BSC
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Semigroup ((<>))
|
||||
import qualified Data.Serialize as S
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import Data.Time (getZonedTime)
|
||||
import Data.Version (Version(..),showVersion)
|
||||
import HaskellCodeExplorer.PackageInfo (createPackageInfo)
|
||||
import qualified HaskellCodeExplorer.Types as HCE
|
||||
import Network.URI.Encode (encode)
|
||||
import Options.Applicative
|
||||
( Parser
|
||||
, (<|>)
|
||||
, execParser
|
||||
, flag
|
||||
, fullDesc
|
||||
, help
|
||||
, helper
|
||||
, info
|
||||
, long
|
||||
, many
|
||||
, metavar
|
||||
, optional
|
||||
, progDesc
|
||||
, short
|
||||
, strOption
|
||||
)
|
||||
import Paths_haskell_code_explorer as HSE (version)
|
||||
import System.Directory (createDirectoryIfMissing)
|
||||
import System.Exit (ExitCode(..), exitWith)
|
||||
import System.FilePath ((</>))
|
||||
import System.Log.FastLogger
|
||||
( LoggerSet
|
||||
, ToLogStr(..)
|
||||
, defaultBufSize
|
||||
, fromLogStr
|
||||
, newFileLoggerSet
|
||||
, newStdoutLoggerSet
|
||||
, pushLogStrLn
|
||||
, rmLoggerSet
|
||||
)
|
||||
|
||||
data IndexerConfig = IndexerConfig
|
||||
{ configPackageDirectoryPath :: FilePath
|
||||
, configPackageDistDirRelativePath :: Maybe FilePath
|
||||
, configOutputDirectoryName :: Maybe String
|
||||
, configLog :: !HCE.Log
|
||||
, configMinLogLevel :: !LogLevel
|
||||
, configSourceCodePreprocessing :: !HCE.SourceCodePreprocessing
|
||||
, configCompression :: !Compression
|
||||
, configGhcOptions :: [String]
|
||||
, configIgnoreDirectories :: [String]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data Compression
|
||||
= Gzip
|
||||
| NoCompression
|
||||
deriving (Show, Eq)
|
||||
|
||||
#if MIN_VERSION_GLASGOW_HASKELL(8,2,2,0)
|
||||
ghcVersion :: Version
|
||||
ghcVersion = Version {versionBranch = [8, 2, 2, 0], versionTags = []}
|
||||
#else
|
||||
ghcVersion :: Version
|
||||
ghcVersion = Version {versionBranch = [8, 0, 2, 0], versionTags = []}
|
||||
#endif
|
||||
|
||||
versionInfo :: String
|
||||
versionInfo =
|
||||
"haskell-code-indexer version " ++
|
||||
showVersion version ++ ", GHC version " ++ showVersion ghcVersion
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
let description =
|
||||
"haskell-code-indexer collects and saves information about the source code of a Cabal package. " ++
|
||||
versionInfo
|
||||
config <-
|
||||
execParser $
|
||||
info (helper <*> configParser) (fullDesc <> progDesc description)
|
||||
loggerSet <-
|
||||
case configLog config of
|
||||
HCE.ToFile logfile -> newFileLoggerSet defaultBufSize logfile
|
||||
HCE.StdOut -> newStdoutLoggerSet defaultBufSize
|
||||
let minLogLevel = configMinLogLevel config
|
||||
logger loggerSet minLogLevel LevelInfo versionInfo
|
||||
logger loggerSet minLogLevel LevelDebug $ show config
|
||||
handle
|
||||
(\(e :: SomeException) -> do
|
||||
logger loggerSet minLogLevel LevelError (show e)
|
||||
rmLoggerSet loggerSet
|
||||
exitWith (ExitFailure 1)) $ do
|
||||
packageInfo <-
|
||||
runLoggingT
|
||||
(createPackageInfo
|
||||
(configPackageDirectoryPath config)
|
||||
(configPackageDistDirRelativePath config)
|
||||
(configSourceCodePreprocessing config)
|
||||
(configGhcOptions config)
|
||||
(configIgnoreDirectories config))
|
||||
(\_loc _source level msg -> logger loggerSet minLogLevel level msg)
|
||||
let outputDir =
|
||||
configPackageDirectoryPath config </>
|
||||
fromMaybe
|
||||
HCE.defaultOutputDirectoryName
|
||||
(configOutputDirectoryName config)
|
||||
createDirectoryIfMissing False outputDir
|
||||
logger loggerSet minLogLevel LevelDebug $ "Output directory : " ++ outputDir
|
||||
BS.writeFile
|
||||
(outputDir </> HCE.packageInfoBinaryFileName)
|
||||
(S.encode $ HCE.toCompactPackageInfo packageInfo)
|
||||
mapM_
|
||||
(\(HCE.HaskellModulePath path, modInfo) ->
|
||||
let (compressFunction, compressExtension) =
|
||||
case configCompression config of
|
||||
Gzip -> (compress, ".gz")
|
||||
NoCompression -> (id, "")
|
||||
filePath =
|
||||
outputDir </>
|
||||
(encode (T.unpack path) ++ ".json" ++ compressExtension)
|
||||
in BSL.writeFile filePath . compressFunction . A.encode $ modInfo) .
|
||||
HM.toList $
|
||||
HCE.moduleMap (packageInfo :: HCE.PackageInfo HCE.ModuleInfo)
|
||||
BSL.writeFile
|
||||
(outputDir </> HCE.packageInfoJsonFileName)
|
||||
(A.encode packageInfo)
|
||||
BSL.writeFile (outputDir </> "version.txt") (BSC.pack $ showVersion version)
|
||||
logger loggerSet minLogLevel LevelInfo ("Finished" :: T.Text)
|
||||
rmLoggerSet loggerSet
|
||||
|
||||
configParser :: Parser IndexerConfig
|
||||
configParser =
|
||||
IndexerConfig <$>
|
||||
strOption
|
||||
(long "package" <> short 'p' <> metavar "PATH" <>
|
||||
help "Path to a Cabal package") <*>
|
||||
optional
|
||||
(strOption
|
||||
(long "dist" <> metavar "RELATIVE_PATH" <>
|
||||
help "Relative path to a dist directory")) <*>
|
||||
optional
|
||||
(strOption
|
||||
(long "output" <> metavar "DIRECTORY_NAME" <>
|
||||
help "Output directory (default is '.haskell-code-explorer')")) <*>
|
||||
(pure HCE.StdOut <|>
|
||||
(HCE.ToFile <$>
|
||||
strOption
|
||||
(long "logfile" <> metavar "PATH" <>
|
||||
help "Path to a log file (by default log is written to stdout)"))) <*>
|
||||
flag
|
||||
LevelInfo
|
||||
LevelDebug
|
||||
(long "verbose" <> short 'v' <> help "Write debug information to a log") <*>
|
||||
flag
|
||||
HCE.AfterPreprocessing
|
||||
HCE.BeforePreprocessing
|
||||
(long "before-preprocessing" <>
|
||||
help
|
||||
"Index source code before preprocessor pass (by default source code after preprocessing is indexed)") <*>
|
||||
flag
|
||||
Gzip
|
||||
NoCompression
|
||||
(long "no-compression" <>
|
||||
help
|
||||
"Do not compress json files (by default json files are compressed using gzip)") <*>
|
||||
many
|
||||
(strOption
|
||||
(long "ghc" <> metavar "OPTIONS" <> help "Command-line options for GHC")) <*>
|
||||
many
|
||||
(strOption
|
||||
(long "ignore" <> metavar "DIRECTORY_NAME" <>
|
||||
help "Directories to ignore (e.g. node_modules)"))
|
||||
|
||||
logger :: ToLogStr msg => LoggerSet -> LogLevel -> LogLevel -> msg -> IO ()
|
||||
logger loggerSet minLogLevel logLevel msg =
|
||||
when (logLevel >= minLogLevel) $ do
|
||||
time <- getZonedTime
|
||||
let showLogLevel :: LogLevel -> T.Text
|
||||
showLogLevel LevelDebug = "[debug]"
|
||||
showLogLevel LevelInfo = "[info]"
|
||||
showLogLevel LevelWarn = "[warn]"
|
||||
showLogLevel LevelError = "[error]"
|
||||
showLogLevel (LevelOther t) = T.concat ["[",t,"]"]
|
||||
text =
|
||||
T.concat
|
||||
[ T.pack $ show time
|
||||
, " : "
|
||||
, showLogLevel logLevel
|
||||
, " "
|
||||
, TE.decodeUtf8 . fromLogStr . toLogStr $ msg
|
||||
]
|
||||
pushLogStrLn loggerSet $ toLogStr text
|
1014
app/Server.hs
Normal file
137
haskell-code-explorer.cabal
Normal file
@ -0,0 +1,137 @@
|
||||
name: haskell-code-explorer
|
||||
version: 0.1.0.0
|
||||
synopsis: Web application for exploring and understanding Haskell codebases
|
||||
Category: GHC,Web
|
||||
description: Please see README.md
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
author: Alexey Kiryushin
|
||||
maintainer: alexey.a.kiryushin@gmail.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.20
|
||||
|
||||
library
|
||||
default-language: Haskell2010
|
||||
exposed-modules: HaskellCodeExplorer.PackageInfo
|
||||
, HaskellCodeExplorer.ModuleInfo
|
||||
, HaskellCodeExplorer.Types
|
||||
, HaskellCodeExplorer.Preprocessor
|
||||
, HaskellCodeExplorer.GhcUtils
|
||||
, HaskellCodeExplorer.AST.RenamedSource
|
||||
, HaskellCodeExplorer.AST.TypecheckedSource
|
||||
hs-source-dirs: src
|
||||
ghc-options: -Wall -O2 -funbox-strict-fields
|
||||
build-depends: IntervalMap
|
||||
, aeson
|
||||
, attoparsec
|
||||
, base
|
||||
, blaze-html
|
||||
, deepseq
|
||||
, bytestring
|
||||
, cabal-helper
|
||||
, cereal
|
||||
, containers
|
||||
, directory
|
||||
, directory-tree
|
||||
, filemanip
|
||||
, filepath
|
||||
, ghc
|
||||
, ghc-paths
|
||||
, hashable
|
||||
, haddock-library
|
||||
, mtl
|
||||
, process
|
||||
, syb
|
||||
, uniplate
|
||||
, text
|
||||
, unordered-containers
|
||||
, vector
|
||||
, fast-logger
|
||||
, monad-logger
|
||||
, extra
|
||||
|
||||
executable haskell-code-indexer
|
||||
main-is: Indexer.hs
|
||||
ghc-options: -Wall -rtsopts -O2 -funbox-strict-fields
|
||||
hs-source-dirs: app
|
||||
build-depends: IntervalMap
|
||||
, aeson
|
||||
, base
|
||||
, bytestring
|
||||
, cereal
|
||||
, directory
|
||||
, filepath
|
||||
, optparse-applicative
|
||||
, text
|
||||
, time
|
||||
, unordered-containers
|
||||
, uri-encode
|
||||
, fast-logger
|
||||
, monad-logger
|
||||
, zlib
|
||||
, haskell-code-explorer
|
||||
default-language: Haskell2010
|
||||
|
||||
executable haskell-code-server
|
||||
main-is: Server.hs
|
||||
ghc-options: -Wall -O2 -rtsopts -funbox-strict-fields -threaded
|
||||
hs-source-dirs: app,src
|
||||
other-modules: HaskellCodeExplorer.Types
|
||||
build-depends: IntervalMap
|
||||
, aeson
|
||||
, base
|
||||
, bytestring
|
||||
, cereal
|
||||
, containers
|
||||
, deepseq
|
||||
, directory
|
||||
, filemanip
|
||||
, filepath
|
||||
, blaze-html
|
||||
, text
|
||||
, unordered-containers
|
||||
, hashable
|
||||
, vector
|
||||
, wai
|
||||
, syb
|
||||
, haddock-library
|
||||
, servant
|
||||
, servant-server
|
||||
, mime-types
|
||||
, mtl
|
||||
, wai-extra
|
||||
, wai-middleware-static
|
||||
, warp
|
||||
, http-types
|
||||
, http-api-data
|
||||
, fast-logger
|
||||
, monad-logger
|
||||
, optparse-applicative
|
||||
, data-default
|
||||
, pagination
|
||||
if impl(ghc >= 8.4.3)
|
||||
build-depends: ghc-compact
|
||||
|
||||
test-suite test
|
||||
default-language: Haskell2010
|
||||
type: exitcode-stdio-1.0
|
||||
ghc-options: -Wall
|
||||
hs-source-dirs: test
|
||||
main-is: Main.hs
|
||||
build-depends: IntervalMap
|
||||
, QuickCheck
|
||||
, attoparsec
|
||||
, base
|
||||
, bytestring
|
||||
, containers
|
||||
, directory
|
||||
, filepath
|
||||
, hspec
|
||||
, text
|
||||
, uniplate
|
||||
, unordered-containers
|
||||
, monad-logger
|
||||
, process
|
||||
, vector
|
||||
, syb
|
||||
, haskell-code-explorer
|
9
javascript/.ember-cli
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": true
|
||||
}
|
15
javascript/.eslintrc.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: 'module'
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
env: {
|
||||
browser: true
|
||||
},
|
||||
rules: {
|
||||
'no-console':0,
|
||||
"no-useless-escape": 0
|
||||
}
|
||||
};
|
15
javascript/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/bower_components
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage/*
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
3
javascript/.watchmanconfig
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
}
|
26
javascript/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# haskell-code-explorer
|
||||
|
||||
## Prerequisites
|
||||
* [Node.js](https://nodejs.org/) (with NPM)
|
||||
* [Ember CLI](https://ember-cli.com/)
|
||||
* [bower](https://bower.io/)
|
||||
|
||||
## Installation
|
||||
* `cd haskell-code-explorer/javascript`
|
||||
* `npm install`
|
||||
* `bower install`
|
||||
|
||||
## Running / Development
|
||||
* Start haskell-code-server on port 8080
|
||||
* `ember server --port=4200 --proxy=http://localhost:8080`
|
||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
||||
|
||||
To test the app with real-world data :
|
||||
* ember server --proxy=https://haskell-code-explorer.mfix.io/
|
||||
|
||||
## Running Tests
|
||||
* `ember server --port=4200`
|
||||
* Open [http://localhost:4200/tests](http://localhost:4200/tests) in a browser
|
||||
|
||||
## Building
|
||||
* `ember build --environment production`
|
15
javascript/app/app.js
Normal file
@ -0,0 +1,15 @@
|
||||
import Ember from 'ember';
|
||||
import Resolver from 'ember-resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import config from './config/environment';
|
||||
|
||||
var App;
|
||||
|
||||
App = Ember.Application.extend({
|
||||
modulePrefix: config.modulePrefix,
|
||||
Resolver: Resolver
|
||||
});
|
||||
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
export default App;
|
0
javascript/app/components/.gitkeep
Normal file
52
javascript/app/components/bottom-panel.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
3
javascript/app/components/expression-info.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
export default Ember.Component.extend({
|
||||
});
|
94
javascript/app/components/file-tree.js
Normal file
@ -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')();
|
||||
}
|
||||
}
|
||||
});
|
492
javascript/app/components/haskell-module.js
Normal file
@ -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 :
|
||||
//<tr><td><span data-start="1" date-end="3">abc</span><span>...</span></td></tr>
|
||||
//<tr>...</tr>
|
||||
if(startNode.nodeName === "#text") {
|
||||
const parent = startNode.parentNode;//<span>
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
62
javascript/app/components/identifier-info.js
Normal file
@ -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);
|
||||
}});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
60
javascript/app/components/identifier-name.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
50
javascript/app/components/infinite-list.js
Normal file
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
144
javascript/app/components/info-window.js
Normal file
@ -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;//<td> 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');
|
||||
}
|
||||
}
|
||||
});
|
131
javascript/app/components/input-with-autocomplete.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
21
javascript/app/components/instance-info.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
48
javascript/app/components/paginated-list.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
72
javascript/app/components/resizable-panel.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
67
javascript/app/components/text-file.js
Normal file
@ -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 = "<table class='source-code'><tbody>";
|
||||
const end = "</tbody></table>";
|
||||
let lineNumber = 0;
|
||||
const lines = text.split("\n").map((line) => {
|
||||
lineNumber ++;
|
||||
const lineNumberHtml = "<td id='LN"+lineNumber+"' class='line-number'>"+lineNumber+"</td>";
|
||||
const lineContentHtml = "<td id='LC"+lineNumber+"' class='line-content'>"+escapeHtml(line)+"</td>";
|
||||
return "<tr>"+ lineNumberHtml + lineContentHtml + "</tr>";
|
||||
}).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();
|
||||
});
|
||||
})
|
||||
});
|
29
javascript/app/components/type-component.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
4
javascript/app/components/type-signature-text.js
Normal file
@ -0,0 +1,4 @@
|
||||
import Ember from 'ember';
|
||||
export default Ember.Component.extend({
|
||||
tagName : "span"
|
||||
});
|
23
javascript/app/components/type-signature.js
Normal file
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
21
javascript/app/controllers/application.js
Normal file
@ -0,0 +1,21 @@
|
||||
import {updateColorThemeCss,themes} from '../utils/color-themes';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
settings : Ember.inject.service('settings'),
|
||||
themes: Object.values(themes),
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
updateColorThemeCss(this.get('settings').get('colorTheme'));
|
||||
},
|
||||
currentTheme: Ember.computed('settings',function() {
|
||||
return this.get('settings.colorTheme.id');
|
||||
}),
|
||||
actions : {
|
||||
themeChanged (themeId) {
|
||||
const theme = themes[themeId];
|
||||
this.get('settings').set('colorTheme',theme);
|
||||
updateColorThemeCss(theme);
|
||||
}
|
||||
}
|
||||
});
|
24
javascript/app/controllers/package.js
Normal file
@ -0,0 +1,24 @@
|
||||
import Ember from 'ember';
|
||||
import {goToDefinition} from '../utils/go-to-definition';
|
||||
export default Ember.Controller.extend({
|
||||
store : Ember.inject.service('store'),
|
||||
currentFile : null,
|
||||
loadItemsFunction : null,
|
||||
query : null,
|
||||
actions : {
|
||||
searchIdentifier (query) {
|
||||
if(query) {
|
||||
this.set('currentFile',null);
|
||||
document.title = this.get('model.id');
|
||||
this.transitionToRoute('package.search',query);
|
||||
}
|
||||
},
|
||||
showIdentifier (identifierInfo) {
|
||||
goToDefinition(this.get('store'),
|
||||
identifierInfo.locationInfo,
|
||||
1,//left mouse button
|
||||
null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
14
javascript/app/controllers/package/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Ember from 'ember';
|
||||
export default Ember.Controller.extend({
|
||||
modulesFiltered : Ember.computed('model','query',function () {
|
||||
const query = this.get('query');
|
||||
const modules = Object.keys(this.get('model.modules')).sort();
|
||||
if(query) {
|
||||
const regExp = new RegExp(query,"i");
|
||||
return modules.filter((p) => p.search(regExp) != -1);
|
||||
} else {
|
||||
return modules;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
15
javascript/app/controllers/package/search.js
Normal file
@ -0,0 +1,15 @@
|
||||
import Ember from 'ember';
|
||||
import {goToDefinition} from '../../utils/go-to-definition';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
store : Ember.inject.service('store'),
|
||||
actions : {
|
||||
goToDefinition (locationInfo,event) {
|
||||
goToDefinition(this.get('store'),
|
||||
locationInfo,
|
||||
event.which,
|
||||
null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
9
javascript/app/controllers/package/show/file.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Ember from 'ember';
|
||||
export default Ember.Controller.extend({
|
||||
settings : Ember.inject.service('settings'),
|
||||
actions : {
|
||||
findReferences(packageId,externalId,occName) {
|
||||
this.send('updateReferences',packageId,externalId,occName);
|
||||
}
|
||||
}
|
||||
});
|
12
javascript/app/controllers/packages.js
Normal file
@ -0,0 +1,12 @@
|
||||
import Ember from 'ember';
|
||||
export default Ember.Controller.extend({
|
||||
queryObserver : Ember.observer("query",function() {
|
||||
Ember.run.debounce(this, () => {
|
||||
const regExp = new RegExp(this.get('query'),"i");
|
||||
const packages = this.get('model').filter((p) => p.name.search(regExp) != -1);
|
||||
Ember.run.next(() => {
|
||||
this.set('packages',packages);
|
||||
});
|
||||
}, 300);
|
||||
})
|
||||
});
|
0
javascript/app/helpers/.gitkeep
Normal file
22
javascript/app/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Haskell code explorer</title>
|
||||
<meta name="description" content="Haskell code explorer">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{{content-for 'head'}}
|
||||
<link rel="stylesheet" href="/assets/vendor.css">
|
||||
<link rel="stylesheet" href="/assets/haskell-code-explorer.css">
|
||||
{{content-for 'head-footer'}}
|
||||
</head>
|
||||
<body>
|
||||
{{content-for 'body'}}
|
||||
<script pace-src="/assets/vendor.js"></script>
|
||||
<script pace-src="/assets/haskell-code-explorer.js"></script>
|
||||
{{content-for 'body-footer'}}
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
</body>
|
||||
</html>
|
20
javascript/app/router.js
Normal file
@ -0,0 +1,20 @@
|
||||
import Ember from 'ember';
|
||||
import config from './config/environment';
|
||||
|
||||
var Router = Ember.Router.extend({
|
||||
location: config.locationType
|
||||
});
|
||||
|
||||
Router.map(function() {
|
||||
this.route('packages',{path:''});
|
||||
this.route('package', {path:'/package/:packageId'}, function() {
|
||||
this.route('show',function() {
|
||||
this.route('file', {path:'*filePath'}, function() {
|
||||
});
|
||||
});
|
||||
this.route('search',{path:'/search/:query'});
|
||||
});
|
||||
this.route('bad-url', { path: '/*badurl' });
|
||||
});
|
||||
|
||||
export default Router;
|
3
javascript/app/routes/application.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
export default Ember.Route.extend({
|
||||
});
|
38
javascript/app/routes/package.js
Normal file
@ -0,0 +1,38 @@
|
||||
import Ember from 'ember';
|
||||
import {urls} from '../utils/api-urls';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
store : Ember.inject.service('store'),
|
||||
model (params) {
|
||||
return this.get('store').loadPackage(params.packageId)
|
||||
.catch((e) => {console.log(e);this.transitionTo("/package-not-found");});
|
||||
},
|
||||
afterModel(model) {
|
||||
document.title = model.id;
|
||||
},
|
||||
setupController(controller, model) {
|
||||
this._super(controller, model);
|
||||
const packageId = this.modelFor('package').id;
|
||||
controller.set('bottomPanelVisible',false);
|
||||
controller.set('createSearchUrlFunction',(query) => {
|
||||
return urls.identifierSearchUrl(packageId,query);
|
||||
});
|
||||
},
|
||||
actions : {
|
||||
openFile (filePath) {
|
||||
this.transitionTo('package.show.file',filePath);
|
||||
},
|
||||
fileOpened (filePath) {
|
||||
if(this.get('controller')) {
|
||||
this.set('controller.currentFile',filePath);
|
||||
}
|
||||
},
|
||||
updateReferences(packageId,externalId,occName) {
|
||||
this.set('controller.packageId',packageId);
|
||||
this.set('controller.externalId',externalId);
|
||||
this.set('controller.occName',occName);
|
||||
this.set('controller.bottomPanelVisible',true);
|
||||
this.set('controller.referencesUrl',urls.referencesUrl(packageId,externalId)+"?per_page=50");
|
||||
}
|
||||
}
|
||||
});
|
7
javascript/app/routes/package/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
afterModel : function (model,transition) {
|
||||
transition.send("fileOpened",null);
|
||||
}
|
||||
});
|
36
javascript/app/routes/package/search.js
Normal file
@ -0,0 +1,36 @@
|
||||
import Ember from 'ember';
|
||||
import {urls} from '../../utils/api-urls';
|
||||
import {goToDefinition} from '../../utils/go-to-definition';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
store : Ember.inject.service('store'),
|
||||
model (params) {
|
||||
return {
|
||||
query: params.query,
|
||||
url: urls.identifierSearchUrl(this.modelFor('package').id,params.query)+"?per_page=20"
|
||||
};
|
||||
},
|
||||
afterModel () {
|
||||
const onmouseup = (event) => {
|
||||
// This makes links in documentation clickable
|
||||
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._onmouseup = onmouseup;
|
||||
document.addEventListener('mouseup',onmouseup);
|
||||
},
|
||||
deactivate() {
|
||||
if(this._onmouseup) {
|
||||
document.removeEventListener('mouseup',this._onmouseup);
|
||||
}
|
||||
}
|
||||
});
|
27
javascript/app/routes/package/show/file.js
Normal file
@ -0,0 +1,27 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
store : Ember.inject.service(),
|
||||
model : function (params) {
|
||||
const packageInfo = this.modelFor('package');
|
||||
if(packageInfo.modules[params.filePath]) {
|
||||
return this.get('store').loadHaskellModule(packageInfo.id,params.filePath)
|
||||
.catch((e) => {console.log(e);this.transitionTo("/not-found");});
|
||||
} else {
|
||||
return this.get('store').loadFile(packageInfo.id,params.filePath)
|
||||
.then((result) => {
|
||||
document.title = packageInfo.id;
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {console.log(e);this.transitionTo("/not-found");});
|
||||
}
|
||||
},
|
||||
afterModel (model) {
|
||||
document.title = model.id + " - " + this.modelFor('package').id;
|
||||
},
|
||||
actions : {
|
||||
didTransition : function () {
|
||||
this.send("fileOpened",this.currentModel.id);
|
||||
}
|
||||
}
|
||||
});
|
7
javascript/app/routes/package/show/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
afterModel : function (model,transition) {
|
||||
transition.send("fileOpened",null);
|
||||
}
|
||||
});
|
16
javascript/app/routes/packages.js
Normal file
@ -0,0 +1,16 @@
|
||||
import Ember from 'ember';
|
||||
import {urls} from '../utils/api-urls';
|
||||
import config from '../config/environment';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model () {
|
||||
return Ember.$.getJSON(urls.packagesUrl);
|
||||
},
|
||||
setupController(controller, model) {
|
||||
this._super(controller, model);
|
||||
controller.set('packages',model);
|
||||
},
|
||||
afterModel () {
|
||||
document.title = config.APP.title;
|
||||
}
|
||||
});
|
21
javascript/app/services/settings.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Ember from 'ember';
|
||||
import {themes} from '../utils/color-themes';
|
||||
|
||||
export default Ember.Service.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
if(localStorage) {
|
||||
const colorThemeId = localStorage.getItem("colorThemeId");
|
||||
const colorTheme = themes[colorThemeId];
|
||||
if(colorThemeId) {
|
||||
this.set('colorTheme',colorTheme);
|
||||
}
|
||||
}
|
||||
},
|
||||
colorTheme : themes["darkTheme"],
|
||||
settingsObserver : Ember.observer("colorTheme",function() {
|
||||
if(localStorage) {
|
||||
localStorage.setItem("colorThemeId",this.get('colorTheme').id);
|
||||
}
|
||||
})
|
||||
});
|
141
javascript/app/services/store.js
Normal file
@ -0,0 +1,141 @@
|
||||
import Ember from 'ember';
|
||||
import RSVP from 'rsvp';
|
||||
import {urls} from '../utils/api-urls';
|
||||
|
||||
|
||||
//********************************************************************************
|
||||
//https://coderwall.com/p/zrlulq/parsing-a-link-header-in-javascript
|
||||
function unquote(value) {
|
||||
if (value.charAt(0) == '"' && value.charAt(value.length - 1) == '"') {
|
||||
return value.substring(1, value.length - 1);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function parseLinkHeader(header) {
|
||||
if(!header) {return {}}
|
||||
var linkexp = /<[^>]*>\s*(\s*;\s*[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*")))*(,|$)/g;
|
||||
var paramexp = /[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*"))/g;
|
||||
|
||||
var matches = header.match(linkexp);
|
||||
var rels = new Object();
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
var split = matches[i].split('>');
|
||||
var href = split[0].substring(1);
|
||||
var ps = split[1];
|
||||
var link = new Object();
|
||||
link.href = href;
|
||||
var s = ps.match(paramexp);
|
||||
for (let j = 0; j < s.length; j++) {
|
||||
var p = s[j];
|
||||
var paramsplit = p.split('=');
|
||||
var name = paramsplit[0];
|
||||
link[name] = unquote(paramsplit[1]);
|
||||
}
|
||||
|
||||
if (link.rel != undefined) {
|
||||
rels[link.rel] = link;
|
||||
}
|
||||
}
|
||||
return rels;
|
||||
}
|
||||
//********************************************************************************
|
||||
|
||||
|
||||
export default Ember.Service.extend({
|
||||
init() {
|
||||
this.packages = {};
|
||||
this.files = {};
|
||||
this.haskellModules = {};
|
||||
this.definitionSites = {};
|
||||
this.modulePaths = {};
|
||||
this.expressions = {};
|
||||
this.references = {};
|
||||
},
|
||||
loadPackage(packageId) {
|
||||
const packageInfo = this.packages[packageId];
|
||||
if(packageInfo) {
|
||||
return new RSVP.Promise((resolve) => {resolve(packageInfo);});
|
||||
} else {
|
||||
const url = urls.packageInfoUrl(packageId);
|
||||
return Ember.$.getJSON(url).then((packageInfo) => {
|
||||
this.packages[packageId] = packageInfo;
|
||||
return packageInfo;
|
||||
});
|
||||
}
|
||||
},
|
||||
loadFile(packageId,filePath) {
|
||||
const fileId = packageId + "/" + filePath;
|
||||
const file = this.files[fileId];
|
||||
if(file) {
|
||||
return new RSVP.Promise((resolve) => {resolve(file);});
|
||||
} else {
|
||||
const url = urls.fileUrl(packageId,filePath);
|
||||
return Ember.$.get({url:url,dataType:"text"}).then((text) => {
|
||||
const file = {};
|
||||
file.text = text;
|
||||
file.packageId = packageId;
|
||||
file.isHaskellModule = false;
|
||||
file.id = filePath;
|
||||
this.files[fileId] = file;
|
||||
return file;
|
||||
});
|
||||
}
|
||||
},
|
||||
loadHaskellModule(packageId,filePath) {
|
||||
const moduleId = packageId + "/" + filePath ;
|
||||
const module = this.haskellModules[moduleId];
|
||||
if(module) {
|
||||
return new RSVP.Promise((resolve)=>{resolve(module);});
|
||||
} else {
|
||||
const url = urls.haskellModuleUrl(packageId,filePath);
|
||||
return Ember.$.getJSON(url).then((module) => {
|
||||
module.packageId = packageId;
|
||||
module.isHaskellModule = true;
|
||||
this.haskellModules[moduleId] = module;
|
||||
return module;
|
||||
});
|
||||
}
|
||||
},
|
||||
loadDefinitionSite(packageId,moduleName,componentId,entity,name) {
|
||||
const id = packageId + "/"+ componentId + "/" + moduleName + "/" + entity + "/" + name;
|
||||
const definitionSite = this.definitionSites[id];
|
||||
if(definitionSite) {
|
||||
return new RSVP.Promise((resolve)=>{resolve(definitionSite);});
|
||||
} else {
|
||||
const url = urls.identifierDefinitionSiteUrl(packageId,moduleName,componentId,entity,name);
|
||||
return Ember.$.getJSON(url).then((definitionSite) => {
|
||||
this.definitionSites[id] = definitionSite;
|
||||
return definitionSite;
|
||||
});
|
||||
}
|
||||
},
|
||||
loadExpressions(packageId,modulePath,lineStart,columnStart,lineEnd,columnEnd) {
|
||||
const id = packageId + "/" + encodeURIComponent(modulePath)
|
||||
+ "/" + lineStart + "/" + columnStart + "/" + lineEnd + "/" + columnEnd;
|
||||
const exprs = this.expressions[id];
|
||||
if(exprs) {
|
||||
return new RSVP.Promise((resolve)=>{resolve(exprs);});
|
||||
} else {
|
||||
const url = urls.expressionsUrl(packageId,modulePath,lineStart,columnStart,lineEnd,columnEnd);
|
||||
return Ember.$.getJSON(url).then((exprs) => {
|
||||
this.expressions[id] = exprs;
|
||||
return exprs;
|
||||
});
|
||||
}
|
||||
},
|
||||
loadFromUrlPaginated(url) {
|
||||
return Ember.$.getJSON(url).then((items,textStatus,jqXHR) => {
|
||||
const linkHeaderText = jqXHR.getResponseHeader('Link');
|
||||
const totalCountHeaderText = jqXHR.getResponseHeader('x-total-count');
|
||||
const linkHeader = parseLinkHeader(linkHeaderText);
|
||||
const total = parseInt(totalCountHeaderText);
|
||||
return {
|
||||
items:items,
|
||||
total:total,
|
||||
linkHeader:linkHeader
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
555
javascript/app/styles/app.scss
Normal file
@ -0,0 +1,555 @@
|
||||
@import "ember-cli-bootstrap-4/bootstrap";
|
||||
|
||||
body {
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
a, a:visited, a:focus, a:active, a:hover{
|
||||
outline:0 none;
|
||||
}
|
||||
|
||||
input:focus{
|
||||
outline:none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top:1rem;
|
||||
}
|
||||
|
||||
.ember-radio-button {
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom:0px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding : 0;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
padding : 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.documentation {
|
||||
margin-top:10px;
|
||||
li {
|
||||
list-style: disc outside none;
|
||||
}
|
||||
ul {
|
||||
padding-left:15px;
|
||||
}
|
||||
ol {
|
||||
padding-left:15px;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
height:100%;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.absolute-container {
|
||||
position:absolute;
|
||||
top:0;
|
||||
bottom:0;
|
||||
left:0;
|
||||
right:0;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding:5px;
|
||||
img {
|
||||
float:left;
|
||||
}
|
||||
}
|
||||
|
||||
.header-item {
|
||||
display:inline-block;
|
||||
margin-top:5px;
|
||||
font-size : 1rem;
|
||||
margin-right: 40px;
|
||||
white-space: nowrap;
|
||||
@media screen and (max-width: 700px) {
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: auto;
|
||||
position:relative;
|
||||
overflow:auto;
|
||||
}
|
||||
|
||||
.packages {
|
||||
flex:auto;
|
||||
overflow-y:auto;
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
a {
|
||||
margin-right:5px;
|
||||
}
|
||||
}
|
||||
|
||||
.package-content {
|
||||
flex:auto;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.package-header {
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
.package-header-input {
|
||||
display:inline-block;
|
||||
margin-right:10px;
|
||||
}
|
||||
|
||||
.package-modules {
|
||||
margin-left:10px;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
font-size:0.7rem;
|
||||
}
|
||||
|
||||
.package-search-form {
|
||||
max-width:500px;
|
||||
margin-bottom:10px;
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.package-header-filename {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.package-header-package-name {
|
||||
margin-right:10px;
|
||||
font-size:17px;
|
||||
}
|
||||
|
||||
.module-search-input {
|
||||
max-width:500px;
|
||||
margin:5px;
|
||||
}
|
||||
|
||||
ul.modules {
|
||||
position:absolute;
|
||||
top:50px;
|
||||
bottom:0px;
|
||||
margin:0px;
|
||||
padding-left:15px;
|
||||
left:0px;
|
||||
right:0px;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
margin-right: 10px;
|
||||
}
|
||||
overflow-x:auto;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
position: absolute;
|
||||
bottom : 0;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 300px;
|
||||
}
|
||||
|
||||
.show-left-panel-button {
|
||||
position:absolute;
|
||||
top:0px;
|
||||
right:-20px;
|
||||
z-index:2;
|
||||
width:20px;
|
||||
height:20px;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
a {
|
||||
text-decoration:none;
|
||||
}
|
||||
opacity:0.6;
|
||||
display:none;
|
||||
}
|
||||
|
||||
.show-left-panel-button:hover {
|
||||
opacity:1;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.hide-file-tree {
|
||||
position: relative;
|
||||
top: 8px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.hide-declarations {
|
||||
margin:7px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
position: absolute;
|
||||
bottom : 0;
|
||||
top : 0;
|
||||
right : 0;
|
||||
left : 300px;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-tree-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
bottom : 0;
|
||||
top : 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.file-container {
|
||||
position: absolute;
|
||||
bottom : 0px;
|
||||
top : 0;
|
||||
right : 0;
|
||||
left : 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
display:none;
|
||||
position: absolute;
|
||||
bottom : 0;
|
||||
right : 0;
|
||||
left : 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
z-index:4;
|
||||
}
|
||||
|
||||
.bottom-panel-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right : 0;
|
||||
left : 0;
|
||||
height: 35px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.bottom-panel-content {
|
||||
position: absolute;
|
||||
bottom : 0;
|
||||
top : 35px;
|
||||
right : 0;
|
||||
left : 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bottom-panel-header-options {
|
||||
float:right;
|
||||
}
|
||||
|
||||
.bottom-panel-header-content {
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.highlighted-identifier {
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 1px #B4B4B4;
|
||||
}
|
||||
|
||||
.identifier {
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
td.line-number {
|
||||
cursor:pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
width: 1%;
|
||||
min-width: 60px;
|
||||
text-align: left;
|
||||
padding-left:10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
td.line-number:hover {
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
td.line-content {
|
||||
padding-left:5px;
|
||||
|
||||
}
|
||||
|
||||
$source-code-font:Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
|
||||
|
||||
.source-code {
|
||||
font-family: $source-code-font;
|
||||
white-space:pre;
|
||||
line-height:1.25;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
code,pre {
|
||||
font-family: $source-code-font !important;
|
||||
white-space:pre;
|
||||
line-height:1.25;
|
||||
font-size:100%;
|
||||
color : unset;
|
||||
}
|
||||
|
||||
.source-code-font {
|
||||
font-family: $source-code-font;
|
||||
line-height:1.25;
|
||||
}
|
||||
|
||||
.source-code-snippet {
|
||||
padding: 4px;
|
||||
text-decoration:none !important;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.source-code-snippet > b {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #B4B4B4;
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.source-code-snippet:hover {
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.source-code-container {
|
||||
margin : 5px;
|
||||
}
|
||||
|
||||
.declarations {
|
||||
position:fixed;
|
||||
top:150px;
|
||||
right:15px;
|
||||
width: 450px;
|
||||
z-index:1;
|
||||
div {
|
||||
margin : 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.declarations-header {
|
||||
opacity:0.9;
|
||||
height:50px;
|
||||
}
|
||||
|
||||
.declarations-content {
|
||||
opacity:0.9;
|
||||
position:absolute;
|
||||
top:50px;
|
||||
left:0px;
|
||||
right:0px;
|
||||
max-height:500px;
|
||||
overflow-y:auto;
|
||||
overflow-x:hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1500px) {
|
||||
.declarations {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-window-container {
|
||||
position:absolute;
|
||||
width:750px;
|
||||
z-index:10;
|
||||
}
|
||||
|
||||
.info-window-header {
|
||||
margin:5px;
|
||||
width: 100%;
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.info-window-content {
|
||||
max-height:200px;
|
||||
overflow-x:auto;
|
||||
overflow-y:auto;
|
||||
}
|
||||
|
||||
.info-window-options {
|
||||
margin-right:10px;
|
||||
margin-left:10px;
|
||||
float:right;
|
||||
}
|
||||
|
||||
|
||||
.ui-draggable-dragging .info-window-header {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
span.link {
|
||||
cursor:pointer;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
span.link:hover {
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
.ui-resizable-n {
|
||||
height: 13px !important;;
|
||||
}
|
||||
|
||||
.ui-resizable-w {
|
||||
width: 13px !important;;
|
||||
}
|
||||
|
||||
.type-info {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.type-component {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
position:absolute;
|
||||
z-index:10;
|
||||
display:none;
|
||||
left:0px;
|
||||
}
|
||||
|
||||
ul.autocomplete-items {
|
||||
li {
|
||||
cursor:pointer;
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
.file-tree-header {
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
height:50px;
|
||||
margin:5px;
|
||||
}
|
||||
|
||||
.file-tree-content {
|
||||
position:absolute;
|
||||
top:50px;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
overflow-x:auto;
|
||||
overflow-x:auto;
|
||||
}
|
||||
|
||||
button {
|
||||
color:black;
|
||||
}
|
||||
|
||||
.paginated-list-header {
|
||||
position:absolute;
|
||||
top:0px;
|
||||
left:0px;
|
||||
right:0px;
|
||||
height:40px;
|
||||
padding: 5px;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.paginated-list-content {
|
||||
position:absolute;
|
||||
top:40px;
|
||||
bottom:0px;
|
||||
left:0px;
|
||||
right:0px;
|
||||
overflow:auto;
|
||||
}
|
||||
|
||||
.pagination-button {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.search-results-header {
|
||||
position:absolute;
|
||||
top:0px;
|
||||
left:0px;
|
||||
right:0px;
|
||||
height:35px;
|
||||
padding: 5px;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.search-results-content {
|
||||
position:absolute;
|
||||
top:35px;
|
||||
bottom:0px;
|
||||
left:0px;
|
||||
right:0px;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
.expression {
|
||||
margin-bottom:15px;
|
||||
}
|
||||
|
||||
.expressions {
|
||||
margin:5px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
margin-left:5px;
|
||||
}
|
||||
|
||||
.identifier-info {
|
||||
margin:5px;
|
||||
}
|
||||
|
||||
.identifier-module {
|
||||
margin-top:10px;
|
||||
}
|
||||
|
||||
.identifier-menu-item {
|
||||
margin-right:10px;
|
||||
}
|
26
javascript/app/templates/application.hbs
Normal file
@ -0,0 +1,26 @@
|
||||
<div class="absolute-container">
|
||||
<div class="flex-container">
|
||||
<div class="header">
|
||||
<img src="/assets/haskell.ico">
|
||||
<div class="container">
|
||||
<span class="header-item">{{#link-to 'packages'}}Haskell packages{{/link-to}}</span>
|
||||
<span class="header-item"><a target="_blank" href="https://github.com/alexwl/haskell-code-explorer">About</a></span>
|
||||
<span class="header-item">
|
||||
{{#each themes as |theme|}}
|
||||
{{#radio-button
|
||||
value=theme.id
|
||||
groupValue=currentTheme
|
||||
changed="themeChanged"}}
|
||||
<span>{{theme.name}}</span>
|
||||
{{/radio-button}}
|
||||
{{/each}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="absolute-container">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
6
javascript/app/templates/bad-url.hbs
Normal file
@ -0,0 +1,6 @@
|
||||
<div class="container">
|
||||
<div style="margin-top:10px">
|
||||
<h1>Not found</h1>
|
||||
<div><a href="/">Main page</a></div>
|
||||
</div>
|
||||
</div>
|
0
javascript/app/templates/components/.gitkeep
Normal file
9
javascript/app/templates/components/bottom-panel.hbs
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="bottom-panel-header">
|
||||
<span class="bottom-panel-header-content">{{yield "header"}}</span>
|
||||
<span class="bottom-panel-header-options">
|
||||
<a href="#" {{action "close"}}>Close</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="bottom-panel-content">
|
||||
{{yield "body"}}
|
||||
</div>
|
8
javascript/app/templates/components/expression-info.hbs
Normal file
@ -0,0 +1,8 @@
|
||||
<div class="expressions">
|
||||
{{#each expressions as |expression|}}
|
||||
<div class="expression">
|
||||
<span class="source-code">{{expression.sourceCode}}</span>
|
||||
<br/>:: {{type-signature type=expression.info.exprType identifiers=identifiers currentLineNumber=currentLineNumber}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
8
javascript/app/templates/components/file-tree.hbs
Normal file
@ -0,0 +1,8 @@
|
||||
<div class="file-tree-header">
|
||||
<div class="input-group">
|
||||
{{input class="form-control" value=query placeholder="Filename"}}<a class="hide-file-tree" href="#" {{action "hide"}}>Hide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-tree-content">
|
||||
<div class="file-tree"></div>
|
||||
</div>
|
57
javascript/app/templates/components/haskell-module.hbs
Normal file
@ -0,0 +1,57 @@
|
||||
<div class="source-code-container"></div>
|
||||
{{#if declarations}}
|
||||
<div class="declarations">
|
||||
<div class="declarations-header">
|
||||
<div class="input-group">
|
||||
{{input class="form-control" value=query placeholder="Identifier"}}<a class="hide-declarations" href="#" {{action "toggleShowDeclarations"}}>{{showDeclarationsLabel}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{#if showDeclarations}}
|
||||
<div id="declarations-content" class="declarations-content">
|
||||
<ul>
|
||||
{{#infinite-list containerElementId="declarations-content" elements=filteredDeclarations perPage=30 as |declaration|}}
|
||||
<li class="declaration">
|
||||
<div class="declaration">
|
||||
<a {{action "goToLine" declaration.lineNumber}} href="#"><span class="source-code-font">{{declaration.name}}{{#if declaration.declType}} :: {{type-signature-text components=declaration.declType.components}}{{/if}}</span>
|
||||
</a><span>{{#unless declaration.isExported}}<i>(not exported)</i>{{/unless}}</span>
|
||||
</div>
|
||||
</li>
|
||||
{{/infinite-list}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#info-window
|
||||
targetElement=selectedIdentifier
|
||||
isHoveredOverIdentifier=isHoveredOverIdentifier
|
||||
hasSelectedExpression=hasSelectedExpression
|
||||
containerElementId="right-panel" as |section|}}
|
||||
{{#if (eq section "header")}}
|
||||
{{#if hasSelectedExpression}}
|
||||
<div>Selected expressions</div>
|
||||
{{else}}
|
||||
{{identifier-name
|
||||
identifierInfo=identifierInfo
|
||||
identifierOccurrence=identifierOccurrence
|
||||
identifierElement=selectedIdentifier
|
||||
currentPackageId=packageId
|
||||
isBinder=identifierOccurrence.isBinder
|
||||
currentLineNumber=currentLineNumber
|
||||
findReferences=findReferences}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless hasSelectedExpression}}
|
||||
{{identifier-info
|
||||
identifierInfo=identifierInfo
|
||||
identifierOccurrence=identifierOccurrence
|
||||
identifiers=identifiers
|
||||
currentLineNumber=currentLineNumber}}
|
||||
{{else}}
|
||||
{{expression-info
|
||||
expressions=expressions
|
||||
identifiers=identifiers
|
||||
currentLineNumber=currentLineNumber}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{/info-window}}
|
39
javascript/app/templates/components/identifier-info.hbs
Normal file
@ -0,0 +1,39 @@
|
||||
{{#if (or identifierInfo identifierOccurrence.idOccType)}}
|
||||
<div class="identifier-info">
|
||||
{{#if identifierInfo}}
|
||||
{{#if isNaughtyRecSel}}
|
||||
<i>This record selector can never be called because its type mentions a type variable that isn't in the result type of the constructor</i>
|
||||
{{else}}
|
||||
{{type-signature
|
||||
type=identifierInfo.idType
|
||||
identifiers=identifiers
|
||||
currentLineNumber=currentLineNumber}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (and identifierInfo identifierOccurrence identifierOccurrence.idOccType)}}
|
||||
<div style="height:10px"></div>
|
||||
{{/if}}
|
||||
{{#if (and identifierOccurrence identifierOccurrence.idOccType)}}
|
||||
{{type-signature
|
||||
type=identifierOccurrence.idOccType
|
||||
identifiers=identifiers
|
||||
currentLineNumber=currentLineNumber}}
|
||||
{{/if}}
|
||||
<div class="documentation">
|
||||
{{{identifierInfo.doc}}}
|
||||
{{{downloadedDocumentation}}}
|
||||
</div>
|
||||
{{#if identifierOccurrence.instanceResolution}}
|
||||
<div style="height:10px"></div>
|
||||
{{/if}}
|
||||
<div>
|
||||
{{#if identifierOccurrence.instanceResolution}}
|
||||
{{instance-info
|
||||
instance=identifierOccurrence.instanceResolution
|
||||
identifiers=identifiers
|
||||
nestedLevel=0
|
||||
currentLineNumber=currentLineNumber}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
12
javascript/app/templates/components/identifier-name.hbs
Normal file
@ -0,0 +1,12 @@
|
||||
<div>
|
||||
<span class="source-code-font break-word identifier-menu-item" style={{style}}>{{#if identifierInfo.demangledOccName}}{{identifierInfo.demangledOccName}}{{else}}{{name}}{{/if}}</span>
|
||||
{{#unless isBinder}}
|
||||
{{#if location}}
|
||||
<span class="identifier-menu-item">{{location}}</span>
|
||||
<span class="link identifier-menu-item" onmouseup={{action "goToDefinition"}}>Go to definition</span>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{#if isExternalIdentifier}}
|
||||
<span class="link identifier-menu-item" onmouseup={{action "findReferences" identifierInfo currentPackageId}}>Find references</span>
|
||||
{{/if}}
|
||||
</div>
|
3
javascript/app/templates/components/infinite-list.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
{{#each renderedElements as |element|}}
|
||||
{{yield element}}
|
||||
{{/each}}
|
12
javascript/app/templates/components/info-window.hbs
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="info-window">
|
||||
<div class="info-window-header">
|
||||
<div class="info-window-options">
|
||||
<a href="#" {{action "pin"}}>{{#if isPinned}}Unpin{{else}}Pin{{/if}}</a>
|
||||
<a href="#" {{action "close"}}>Close</a>
|
||||
</div>
|
||||
{{yield "header"}}
|
||||
</div>
|
||||
<div class="info-window-content">
|
||||
{{yield "body"}}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,19 @@
|
||||
<div class="input-group">
|
||||
{{input class="form-control search-input" value=query placeholder=placeholder}}
|
||||
<div class="autocomplete-container">
|
||||
{{# if items}}
|
||||
<ul class="autocomplete-items">
|
||||
{{#each items as |item index|}}
|
||||
<li class="{{if (eq index highlightedItemIndex) "highlighted"}}" onclick={{action "goToDefinition" item}}>
|
||||
<div class="autocomplete-item">
|
||||
{{yield item}}
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<button id="search-input" class="btn btn-outline-secondary" {{action "onSubmit" query}}>Search</button>
|
||||
</div>
|
||||
</div>
|
10
javascript/app/templates/components/instance-info.hbs
Normal file
@ -0,0 +1,10 @@
|
||||
{{#if instance}}
|
||||
<div style={{style}}>
|
||||
<span class="source-code-font">
|
||||
instance {{type-signature type=instance.instanceType identifiers=identifiers currentLineNumber=currentLineNumber noExpand=true}}</span>
|
||||
(<span class="link" onclick={{action "goToDefinition"}}>Go to definition</span>)
|
||||
{{#each instance.instances as |inst|}}
|
||||
{{instance-info instance=inst identifiers=identifiers nestedLevel=nextNestedLevel currentLineNumber=currentLineNumber}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
17
javascript/app/templates/components/paginated-list.hbs
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="paginated-list-header">
|
||||
<span>Found {{total}}</span>
|
||||
{{#if (or next prev)}}
|
||||
|
||||
|
||||
<span>
|
||||
{{#if first}}<button class="btn btn-outline-secondary btn-sm" {{action "update" first.href}}><<</button>{{/if}}
|
||||
{{#if prev}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" prev.href}}><</button>{{/if}}
|
||||
{{firstItemOnPage}} - {{lastItemOnPage}}
|
||||
{{#if next}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" next.href}}>></button>{{/if}}
|
||||
{{#if last}}<button button class="btn btn-outline-secondary btn-sm" {{action "update" last.href}}>>></button>{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="paginated-list-content">
|
||||
{{yield items}}
|
||||
</div>
|
5
javascript/app/templates/components/resizable-panel.hbs
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="absolute-container">{{yield (action "hide")}}
|
||||
<div onclick={{action "hide"}} class="show-left-panel-button">
|
||||
{{{hideButtonLabel}}}
|
||||
</div>
|
||||
</div>
|
3
javascript/app/templates/components/text-file.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="source-code-container">
|
||||
{{{html}}}
|
||||
</div>
|
3
javascript/app/templates/components/type-component.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
{{#if (and expanded identifierInfo)}}({{/if}}
|
||||
<span class="source-code {{linkClass}}" onmouseup={{action "onmouseup"}}>{{occName}}</span>
|
||||
{{#if (and expanded identifierInfo)}} :: {{type-signature type=identifierInfo.idType identifiers=identifiers noExpand=true}}){{/if}}
|
@ -0,0 +1,2 @@
|
||||
{{!-- No newlines to get rid of spaces between spans --}}
|
||||
{{#each components as |typeComponent|}}{{#if (eq typeComponent.tag "Text")}}{{typeComponent.contents}}{{else}}{{typeComponent.name}}{{/if}}{{/each}}
|
10
javascript/app/templates/components/type-signature.hbs
Normal file
@ -0,0 +1,10 @@
|
||||
{{!-- No newlines to get rid of spaces between spans --}}
|
||||
{{#each components as |typeComponent|}}{{#if (eq typeComponent.tag "Text")}}<span class="source-code">{{typeComponent.contents}}</span>{{else}}{{type-component occName=typeComponent.name internalId=typeComponent.internalId identifiers=identifiers currentLineNumber=currentLineNumber}}{{/if}}{{/each}}
|
||||
{{#unless noExpand}}
|
||||
{{#if type.componentsExpanded}}
|
||||
<div style="margin-top:5px">
|
||||
<button class="btn btn-outline-secondary btn-sm" {{action "toggleExpandTypeSynonyms"}}>{{expandTypeSynonymsLabel}}
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
57
javascript/app/templates/package.hbs
Normal file
@ -0,0 +1,57 @@
|
||||
<div class="flex-container">
|
||||
<div class="package-header">
|
||||
<span class="package-header-package-name">{{#link-to 'package' model}}{{model.id}}{{/link-to}}</span>
|
||||
<span class="package-header-input">
|
||||
{{#input-with-autocomplete
|
||||
onSubmit=(action 'searchIdentifier')
|
||||
createSearchUrlFunction=createSearchUrlFunction
|
||||
maxItems=10
|
||||
selectItem=(action 'showIdentifier')
|
||||
placeholder="Identifier" as |identifier|}}
|
||||
<span class="source-code-font">{{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}}</span>
|
||||
<div class="module-name">
|
||||
{{#if identifier.locationInfo.modulePath}}
|
||||
{{identifier.locationInfo.modulePath}}
|
||||
{{else}}
|
||||
{{identifier.locationInfo.moduleName}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/input-with-autocomplete}}
|
||||
</span>
|
||||
{{#if currentFile}}
|
||||
<span class="package-header-filename">
|
||||
{{currentFile}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="package-content">
|
||||
{{#resizable-panel class="left-panel" alsoResizeElementId="#right-panel" as |hide|}}
|
||||
<div class="file-tree-container">
|
||||
{{file-tree directoryTree=model.directoryTree openFile="openFile" currentFile=currentFile packageId=model.id hide=hide}}
|
||||
</div>
|
||||
{{/resizable-panel}}
|
||||
<div id="right-panel" class="right-panel">
|
||||
<div id="file-container" class="file-container">
|
||||
{{outlet}}
|
||||
</div>
|
||||
{{#bottom-panel visible=bottomPanelVisible topPanelElementId="#file-container" containerElementId="#right-panel" as |section|}}
|
||||
{{#if (eq section "header")}}
|
||||
References to <b><span class="source-code">{{occName}}</span></b> in <span class="source-code">{{packageId}}</span>
|
||||
{{else}}
|
||||
{{#paginated-list url=referencesUrl as |files|}}
|
||||
<ul>
|
||||
{{#each files as |file|}}
|
||||
<li>
|
||||
<div class="file-name"><a href="/package/{{packageId}}/show/{{file.name}}">{{file.name}}</a></div>
|
||||
{{#each file.references as |reference|}}
|
||||
<a class="source-code source-code-snippet" href="/package/{{packageId}}/show/{{file.name}}#L{{reference.idSrcSpan.line}}">{{{reference.sourceCodeHtml}}}</a>
|
||||
{{/each}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/paginated-list}}
|
||||
{{/if}}
|
||||
{{/bottom-panel}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
12
javascript/app/templates/package/index.hbs
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="package-modules">
|
||||
<div class="module-search-input">
|
||||
{{input class="form-control" type="text" value=query placeholder="Module name"}}
|
||||
</div>
|
||||
<ul class="modules">
|
||||
{{#each modulesFiltered as |module|}}
|
||||
<li>
|
||||
<a href="/package/{{model.id}}/show/{{module}}">{{module}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
21
javascript/app/templates/package/search.hbs
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="search-results-header">
|
||||
Query : {{model.query}}
|
||||
</div>
|
||||
<div class="search-results-content">
|
||||
{{#paginated-list url=model.url as |identifiers|}}
|
||||
<ul>
|
||||
{{#each identifiers as |identifier|}}
|
||||
<li class="search-result">
|
||||
<span class="source-code" >{{identifier.demangledOccName}} :: {{type-signature-text components=identifier.idType.components}}</span>
|
||||
<div><a href="#" onmouseup={{action "goToDefinition" identifier.locationInfo}}>Go to definition</a></div>
|
||||
<div class="identifier-module">
|
||||
{{#if identifier.locationInfo.modulePath}}
|
||||
Defined in <a href="/package/{{identifier.locationInfo.packageId.name}}-{{identifier.locationInfo.packageId.version}}/show/{{identifier.locationInfo.modulePath}}">{{identifier.locationInfo.modulePath}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div>{{{identifier.doc}}}</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/paginated-list}}
|
||||
</div>
|
1
javascript/app/templates/package/show.hbs
Normal file
@ -0,0 +1 @@
|
||||
{{outlet}}
|
16
javascript/app/templates/package/show/file.hbs
Normal file
@ -0,0 +1,16 @@
|
||||
{{#if model.isHaskellModule}}
|
||||
{{haskell-module
|
||||
path=model.id
|
||||
name=model.name
|
||||
packageId=model.packageId
|
||||
componentId=model.componentId
|
||||
html=model.sourceCodeHtml
|
||||
identifiers=model.identifiers
|
||||
occurrences=model.occurrences
|
||||
colorTheme=settings.colorTheme
|
||||
declarations=model.declarations
|
||||
findReferences=(action "findReferences")
|
||||
}}
|
||||
{{else}}
|
||||
{{text-file text=model.text path=model.id}}
|
||||
{{/if}}
|
26
javascript/app/templates/packages.hbs
Normal file
@ -0,0 +1,26 @@
|
||||
<div class="flex-container container">
|
||||
<div>
|
||||
<!-- <p class="lead"><b>Haskell code explorer</b> </p> -->
|
||||
<div class="package-search-form">
|
||||
{{input class="form-control" type="text" value=query placeholder="Package name"}}
|
||||
<span>Number of packages : {{packages.length}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="packages" class="packages">
|
||||
<ul>
|
||||
{{#infinite-list containerElementId="packages" elements=packages perPage=80 as |package|}}
|
||||
<li>
|
||||
{{#each package.versions as |version index|}}
|
||||
{{#link-to 'package' (concat package.name "-" version)}}
|
||||
{{#if (gt index 0)}}
|
||||
{{version}}
|
||||
{{else}}
|
||||
{{package.name}}-{{version}}
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
</li>
|
||||
{{/infinite-list}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
29
javascript/app/utils/api-urls.js
Normal file
@ -0,0 +1,29 @@
|
||||
import config from '../config/environment';
|
||||
|
||||
export const urls = {
|
||||
packageInfoUrl : function(packageId) {
|
||||
return config.APP.staticUrlPrefix+"/"+packageId+"/"+config.APP.haskellCodeExplorerDirectory+"/packageInfo.json";
|
||||
},
|
||||
fileUrl : function(packageId,filePath) {
|
||||
return config.APP.staticUrlPrefix+"/"+packageId+"/"+filePath;
|
||||
},
|
||||
haskellModuleUrl : function (packageId,filePath) {
|
||||
return config.APP.staticUrlPrefix+"/"+packageId+"/"+config.APP.haskellCodeExplorerDirectory+"/"+encodeURIComponent(encodeURIComponent(filePath))+ ".json";
|
||||
},
|
||||
packagesUrl : config.APP.apiUrlPrefix + "/packages",
|
||||
identifierDefinitionSiteUrl : function(packageId,moduleName,componentId,entity,name) {
|
||||
return config.APP.apiUrlPrefix + "/definitionSite/" + packageId+"/"+componentId+"/"+moduleName+"/"+entity+"/"+encodeURIComponent(name).replace(/\./g, '%2E');
|
||||
},
|
||||
modulePathUrl : function (packageId,moduleName,componentId) {
|
||||
return config.APP.apiUrlPrefix + "/modulePath/"+packageId+"/"+componentId+"/"+moduleName;
|
||||
},
|
||||
expressionsUrl : function (packageId,modulePath,lineStart,columnStart,lineEnd,columnEnd) {
|
||||
return config.APP.apiUrlPrefix + "/expressions/"+packageId+"/"+encodeURIComponent(modulePath) +"/"+lineStart+"/"+columnStart+"/"+lineEnd+"/"+columnEnd;
|
||||
},
|
||||
referencesUrl : function (packageId,externalId) {
|
||||
return config.APP.apiUrlPrefix + "/references/"+packageId+"/"+encodeURIComponent(externalId);
|
||||
},
|
||||
identifierSearchUrl : function (packageId,query) {
|
||||
return config.APP.apiUrlPrefix + "/identifiers/"+packageId+"/"+encodeURIComponent(query).replace(/\./g, '%2E');
|
||||
}
|
||||
}
|
188
javascript/app/utils/color-themes.js
Normal file
@ -0,0 +1,188 @@
|
||||
function colorThemeToCss(colorTheme) {
|
||||
const css = `
|
||||
body {
|
||||
color: ${colorTheme.defaultColor} !important;
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
}
|
||||
input {
|
||||
color: ${colorTheme.defaultColor} !important;
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
border-color: ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.package-content {
|
||||
border-top: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.header a {
|
||||
color : ${colorTheme.menuLinkColor} !important;
|
||||
}
|
||||
a {
|
||||
color: ${colorTheme.typeColor} !important;
|
||||
}
|
||||
span.link {
|
||||
color: ${colorTheme.typeColor} !important;
|
||||
}
|
||||
.header {
|
||||
background-color: ${colorTheme.menuColor} !important;
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.declarations-content {
|
||||
background-color: ${colorTheme.navigationPanelColor} !important;
|
||||
border: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.declarations-header {
|
||||
background-color: ${colorTheme.navigationPanelColor} !important;
|
||||
border: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
li.declaration {
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.left-panel {
|
||||
background-color: ${colorTheme.navigationPanelColor} !important;
|
||||
border-right: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.show-left-panel-button {
|
||||
background-color: ${colorTheme.navigationPanelColor} !important;
|
||||
border-right:1px solid ${colorTheme.borderColor} !important;
|
||||
border-bottom:1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.right-panel {
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
}
|
||||
a.jstree-anchor {
|
||||
color: ${colorTheme.defaultColor} !important;
|
||||
}
|
||||
.declaration > a {
|
||||
color: ${colorTheme.defaultColor} !important;
|
||||
}
|
||||
.highlighted-line {
|
||||
background : ${colorTheme.highlightedLineColor} !important;
|
||||
}
|
||||
table.source-code {
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
color: ${colorTheme.defaultColor} !important;
|
||||
}
|
||||
.jstree-clicked {
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
}
|
||||
.jstree-hovered {
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
}
|
||||
ul.autocomplete-items {
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
border-top: 1px solid ${colorTheme.borderColor} !important;
|
||||
border-left: 1px solid ${colorTheme.borderColor} !important;
|
||||
border-right: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
ul.autocomplete-items > li {
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
ul.autocomplete-items > li:hover {
|
||||
background-color: ${colorTheme.highlightedLineColor} !important;
|
||||
}
|
||||
ul.autocomplete-items > li.highlighted {
|
||||
background-color: ${colorTheme.highlightedLineColor} !important;
|
||||
}
|
||||
.source-code-snippet {
|
||||
color: ${colorTheme.defaultColor} !important;
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.source-code-snippet:hover {
|
||||
background-color: ${colorTheme.highlightedLineColor} !important;
|
||||
}
|
||||
.bottom-panel {
|
||||
background-color: ${colorTheme.backgroundColor} !important;
|
||||
border-top: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.bottom-panel-header {
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.paginated-list-header {
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
li.search-result {
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.search-results-header {
|
||||
border-bottom: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.info-window-content {
|
||||
border-top: 1px solid ${colorTheme.borderColor} !important;
|
||||
}
|
||||
.info-window {
|
||||
border: 1px solid ${colorTheme.borderColor} !important;
|
||||
background-color:${colorTheme.infoWindowColor} !important;
|
||||
color: ${colorTheme.defaultColor} !important;
|
||||
}
|
||||
.type-info {
|
||||
border-top: 1px solid ${colorTheme.borderColor} !important;
|
||||
}`;
|
||||
return css;
|
||||
}
|
||||
|
||||
const darkTheme = {
|
||||
id: "darkTheme",
|
||||
name: "Dark theme",
|
||||
description: "Dark theme (Monokai based)",
|
||||
defaultColor: "#F8F8F2",
|
||||
backgroundColor: "#272822",
|
||||
typeColor: "#66D9EF",
|
||||
literalColor: "#E6DB74",
|
||||
topLevelIdFromCurrentModule : "#A6E22E",
|
||||
localIdentifierColor: ["#F0A3FF","#0075DC","#993F00",
|
||||
"#2BCE48","#FFCC99","#808080","#94FFB5","#8F7C00",
|
||||
"#C20088","#FFA405","#FFA8BB","#426600","#FF0010",
|
||||
"#5EF1F2","#00998F","#E0FF66","#FFFF80",
|
||||
"#FFFF00","#FF5005"],
|
||||
menuColor: "#3c3b37",
|
||||
menuLinkColor : "#F8F8F2",
|
||||
infoWindowColor: "#3c3b37",
|
||||
navigationPanelColor: "#3c3b37",
|
||||
linkColor : "#0366d6",
|
||||
borderColor: "#535557",
|
||||
highlightedLineColor: "#4a4a4a"
|
||||
};
|
||||
|
||||
const lightTheme = {
|
||||
id: "lightTheme",
|
||||
name: "Light theme",
|
||||
description: "Light theme (Github based)",
|
||||
defaultColor: "#24292e",
|
||||
backgroundColor: "#ffffff",
|
||||
typeColor: "#005cc5",
|
||||
literalColor: "#032f62",
|
||||
topLevelIdFromCurrentModule : "#6f42c1",
|
||||
localIdentifierColor: ["#005C31",
|
||||
"#2BCE48","#808080","#8F7C00",
|
||||
"#C20088","#FFA405","#ffa8bb","#426600","#FF0010",
|
||||
"#09d7d8","#00998F","#990000","#FF5005"],
|
||||
menuColor: "#f2f4f8",
|
||||
menuLinkColor : "#24292e",
|
||||
infoWindowColor: "#f2f4f8",
|
||||
navigationPanelColor: "#f2f4f8",
|
||||
linkColor : "#0366d6",
|
||||
borderColor: "#e1e4e8",
|
||||
highlightedLineColor: "#eaeaea"
|
||||
};
|
||||
|
||||
function updateColorThemeCss (colorTheme) {
|
||||
const newStyle = document.createElement('style');
|
||||
newStyle.type = 'text/css';
|
||||
newStyle.innerHTML = colorThemeToCss(colorTheme);
|
||||
newStyle.id = 'color-theme';
|
||||
const oldStyle = document.querySelector("style#color-theme");
|
||||
if(oldStyle) {
|
||||
oldStyle.parentElement.removeChild(oldStyle);
|
||||
}
|
||||
document.getElementsByTagName('head')[0].appendChild(newStyle);
|
||||
}
|
||||
|
||||
const themes = {
|
||||
darkTheme: darkTheme,
|
||||
lightTheme: lightTheme
|
||||
};
|
||||
|
||||
export {
|
||||
updateColorThemeCss,
|
||||
colorThemeToCss,
|
||||
themes
|
||||
}
|
95
javascript/app/utils/go-to-definition.js
Normal file
@ -0,0 +1,95 @@
|
||||
function exactLocationToUrl(exactLocation) {
|
||||
const modulePath = exactLocation.modulePath;
|
||||
const packageId = exactLocation.packageId.name + "-" + exactLocation.packageId.version;
|
||||
let hash = "";
|
||||
if(exactLocation.startLine != 1) {
|
||||
hash = "#L" + exactLocation.startLine;
|
||||
}
|
||||
return "/package/"+packageId+"/show/"+modulePath+hash;
|
||||
}
|
||||
|
||||
function hackageUrl(packageId,locationInfo) {
|
||||
const dasherizedModuleName = locationInfo.moduleName.replace(/\./g,'-');
|
||||
let key;
|
||||
if(locationInfo.entity === "Val") {
|
||||
key = "v";
|
||||
} else {
|
||||
key = "t";
|
||||
}
|
||||
let hash = "";
|
||||
if(locationInfo.entity === "Val" || locationInfo.entity === "Typ") {
|
||||
hash = "#"+key+":"+locationInfo.haddockAnchorId;
|
||||
}
|
||||
return "https://hackage.haskell.org/package/"+packageId+"/docs/"+dasherizedModuleName+".html"+hash;
|
||||
}
|
||||
|
||||
function openUrl(buttonId,url) {
|
||||
if(buttonId === 2) {//middle mouse button
|
||||
window.open(url, '_blank');
|
||||
} else if(buttonId == 1) {//left mouse button
|
||||
window.location = url;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function saveCurrentLocation(currentLineNumber) {
|
||||
if(currentLineNumber) {
|
||||
const url = window.location.origin + window.location.pathname + "#L" + currentLineNumber;
|
||||
if(location.href != url) {
|
||||
window.location.hash = "#L" + currentLineNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goToDefinition(store,locationInfo,buttonId,currentLineNumber) {
|
||||
if(locationInfo.tag === "ExactLocation") {
|
||||
const url = exactLocationToUrl(locationInfo);
|
||||
if(locationInfo.startLine !== currentLineNumber) {
|
||||
saveCurrentLocation(currentLineNumber);
|
||||
}
|
||||
openUrl(buttonId,url);
|
||||
} else if(locationInfo.tag === "ApproximateLocation") {
|
||||
const packageId = locationInfo.packageId.name+"-"+locationInfo.packageId.version;
|
||||
if(locationInfo.entity === "Mod") {
|
||||
store.loadDefinitionSite(packageId,
|
||||
locationInfo.moduleName,
|
||||
locationInfo.componentId,
|
||||
locationInfo.entity,
|
||||
locationInfo.moduleName)
|
||||
.then((defSite) => {
|
||||
const packageId = defSite.location.packageId.name + "-" + defSite.location.packageId.version;
|
||||
openUrl(buttonId,"/package/" + packageId + "/show/" + defSite.location.modulePath);
|
||||
}).catch(() => {
|
||||
openUrl(buttonId,hackageUrl(packageId,locationInfo));
|
||||
});
|
||||
} else {
|
||||
store.loadDefinitionSite(packageId,
|
||||
locationInfo.moduleName,
|
||||
locationInfo.componentId,
|
||||
locationInfo.entity,
|
||||
locationInfo.name)
|
||||
.then((definitionSite) => {
|
||||
if(definitionSite.location.tag === "ExactLocation") {
|
||||
const url = exactLocationToUrl(definitionSite.location);
|
||||
if(locationInfo.startLine !== currentLineNumber) {
|
||||
saveCurrentLocation(currentLineNumber);
|
||||
}
|
||||
openUrl(buttonId,url);
|
||||
} else {
|
||||
saveCurrentLocation(currentLineNumber);
|
||||
openUrl(buttonId,hackageUrl(packageId,locationInfo));
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.log(e);
|
||||
saveCurrentLocation(currentLineNumber);
|
||||
openUrl(buttonId,hackageUrl(packageId,locationInfo));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert('No location info');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
goToDefinition,openUrl
|
||||
}
|
111
javascript/app/utils/line-selection.js
Normal file
@ -0,0 +1,111 @@
|
||||
function initializeLineSelection(sourceCodeContainerElement,component) {
|
||||
const lineNumbers = Array.prototype.slice.call(sourceCodeContainerElement.querySelectorAll("td.line-number"));
|
||||
if(lineNumbers.length > 0) {
|
||||
const onhashchange = function () {
|
||||
highlightSelectedLines(sourceCodeContainerElement);
|
||||
}
|
||||
window.addEventListener("hashchange",onhashchange);
|
||||
component._onhashchange = onhashchange;
|
||||
|
||||
let shiftPressed;
|
||||
const onkeydown = function (event) {
|
||||
if(event.keyCode === 16) { shiftPressed = true; }
|
||||
};
|
||||
const onkeyup = function (event) {
|
||||
if(event.keyCode === 16) { shiftPressed = false; }
|
||||
};
|
||||
|
||||
document.addEventListener('keydown',onkeydown);
|
||||
document.addEventListener('keyup',onkeyup);
|
||||
component._onkeydown = onkeydown;
|
||||
component._onkeyup = onkeyup;
|
||||
|
||||
let selectedLine1,selectedLine2;
|
||||
lineNumbers.forEach((lineNumberElement) => {
|
||||
lineNumberElement.onclick = function() {
|
||||
const number = parseInt(this.textContent);
|
||||
if(shiftPressed && selectedLine1) {
|
||||
if(selectedLine1 != number) {
|
||||
selectedLine2 = number;
|
||||
if(selectedLine1 < selectedLine2) {
|
||||
highlightLines(sourceCodeContainerElement,selectedLine1,selectedLine2);
|
||||
window.location.hash = "L"+selectedLine1+"-L"+selectedLine2;
|
||||
} else {
|
||||
highlightLines(sourceCodeContainerElement,selectedLine2,selectedLine1);
|
||||
window.location.hash = "L"+selectedLine2+"-L"+selectedLine1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedLine1 = number;
|
||||
selectedLine2 = null;
|
||||
highlightLines(sourceCodeContainerElement,selectedLine1,selectedLine1);
|
||||
window.location.hash = "L"+number;
|
||||
}
|
||||
}
|
||||
});
|
||||
const lines = highlightSelectedLines(sourceCodeContainerElement);
|
||||
if(lines.length) {
|
||||
selectedLine1 = lines[0];
|
||||
selectedLine2 = lines[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function highlightSelectedLines (sourceCodeContainerElement) {
|
||||
const lineInfo = window.location.hash.slice(1);
|
||||
if(lineInfo) {
|
||||
if(lineInfo.includes('-')) {
|
||||
const lines = lineInfo.split("-");
|
||||
const lineNumber1 = parseInt(lines[0].substring(1));
|
||||
const lineNumber2 = parseInt(lines[1].substring(1));
|
||||
if(lineNumber1 && lineNumber2 && lineNumber1 <= lineNumber2) {
|
||||
highlightLines(sourceCodeContainerElement,lineNumber1,lineNumber2);
|
||||
const line = sourceCodeContainerElement.querySelector("td#LC"+lineNumber1);
|
||||
if(line) {
|
||||
scrollLineIntoView(line,sourceCodeContainerElement);
|
||||
}
|
||||
return [lineNumber1,lineNumber2];
|
||||
}
|
||||
} else {
|
||||
const lineNumber = parseInt(lineInfo.substring(1));
|
||||
if(lineNumber) {
|
||||
highlightLines(sourceCodeContainerElement,lineNumber,lineNumber);
|
||||
const line = sourceCodeContainerElement.querySelector("td#LC"+lineNumber);
|
||||
if(line) {
|
||||
scrollLineIntoView(line,sourceCodeContainerElement);
|
||||
}
|
||||
return [lineNumber];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
highlightLines(sourceCodeContainerElement,0,0);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function scrollLineIntoView(lineElement,sourceCodeContainerElement) {
|
||||
lineElement.parentNode.scrollIntoView();
|
||||
const container = sourceCodeContainerElement.parentNode.parentNode;
|
||||
const windowHeight = container.offsetHeight;
|
||||
const fullHeight = sourceCodeContainerElement.offsetHeight;
|
||||
|
||||
if(fullHeight - container.scrollTop > windowHeight) {
|
||||
container.scrollTop = container.scrollTop - (windowHeight/2 - 20);
|
||||
}
|
||||
}
|
||||
|
||||
function highlightLines(parentElement,startLine,endLine) {
|
||||
const lineElements = Array.prototype.slice.call(parentElement.querySelectorAll("td.line-content"));
|
||||
lineElements.forEach((lineElement) => {
|
||||
const number = parseInt(lineElement.id.substring(2)); //<td "id"="LC10">...</td>
|
||||
if(number >= startLine && number <= endLine) {
|
||||
lineElement.classList.add('highlighted-line');
|
||||
} else {
|
||||
lineElement.classList.remove('highlighted-line');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
initializeLineSelection,highlightLines,highlightSelectedLines
|
||||
}
|
6
javascript/bower.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "haskell-code-explorer",
|
||||
"dependencies": {
|
||||
"pace": "^1.0.2"
|
||||
}
|
||||
}
|
64
javascript/config/environment.js
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-env node */
|
||||
'use strict';
|
||||
|
||||
module.exports = function(environment) {
|
||||
let ENV = {
|
||||
modulePrefix: 'haskell-code-explorer',
|
||||
environment,
|
||||
rootURL: '/',
|
||||
locationType: 'auto',
|
||||
EmberENV: {
|
||||
FEATURES: {
|
||||
// Here you can enable experimental features on an ember canary build
|
||||
// e.g. 'with-controller': true
|
||||
},
|
||||
EXTEND_PROTOTYPES: {
|
||||
// Prevent Ember Data from overriding Date.parse.
|
||||
Date: false
|
||||
}
|
||||
},
|
||||
pace: {
|
||||
theme: 'minimal',
|
||||
color: 'silver',
|
||||
target: 'body',
|
||||
elements: {
|
||||
checkInterval: 100,
|
||||
selectors: ['body', '.ember-view']
|
||||
},
|
||||
ajax: false
|
||||
},
|
||||
APP: {
|
||||
staticUrlPrefix: "/files",
|
||||
apiUrlPrefix: "/api",
|
||||
haskellCodeExplorerDirectory: ".haskell-code-explorer",
|
||||
title: "Haskell code explorer"
|
||||
}
|
||||
};
|
||||
|
||||
if (environment === 'development') {
|
||||
// ENV.APP.LOG_RESOLVER = true;
|
||||
// ENV.APP.LOG_ACTIVE_GENERATION = true;
|
||||
// ENV.APP.LOG_TRANSITIONS = true;
|
||||
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
|
||||
// ENV.APP.LOG_VIEW_LOOKUPS = true;
|
||||
}
|
||||
|
||||
if (environment === 'test') {
|
||||
// Testem prefers this...
|
||||
ENV.locationType = 'none';
|
||||
|
||||
// keep test console output quieter
|
||||
ENV.APP.LOG_ACTIVE_GENERATION = false;
|
||||
ENV.APP.LOG_VIEW_LOOKUPS = false;
|
||||
|
||||
ENV.APP.autoboot = false;
|
||||
|
||||
ENV.APP.rootElement = '#ember-testing';
|
||||
}
|
||||
|
||||
if (environment === 'production') {
|
||||
|
||||
}
|
||||
|
||||
return ENV;
|
||||
};
|
9
javascript/config/targets.js
Normal file
@ -0,0 +1,9 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
browsers: [
|
||||
'ie 9',
|
||||
'last 1 Chrome versions',
|
||||
'last 1 Firefox versions',
|
||||
'last 1 Safari versions'
|
||||
]
|
||||
};
|
21
javascript/ember-cli-build.js
Normal file
@ -0,0 +1,21 @@
|
||||
/* eslint-env node */
|
||||
'use strict';
|
||||
|
||||
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
|
||||
|
||||
module.exports = function(defaults) {
|
||||
let app = new EmberApp(defaults, {
|
||||
'ember-cli-bootstrap-4': {
|
||||
js: null
|
||||
},
|
||||
'ember-cli-babel': {
|
||||
includePolyfill: true
|
||||
}
|
||||
});
|
||||
app.import('vendor/jquery-ui-1.12.1.custom/jquery-ui.min.js')// only draggable and resizable
|
||||
app.import('vendor/jquery-ui-1.12.1.custom/jquery-ui.structure.min.css')
|
||||
app.import('node_modules/jstree/dist/jstree.min.js');
|
||||
app.import('node_modules/jstree/dist/themes/default/style.min.css');
|
||||
app.import('node_modules/showdown/dist/showdown.min.js');
|
||||
return app.toTree();
|
||||
};
|
53
javascript/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "haskell-code-explorer",
|
||||
"version": "0.0.1",
|
||||
"description": "Haskell code explorer",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"author": "alexwl",
|
||||
"directories": {
|
||||
"doc": "doc",
|
||||
"test": "tests"
|
||||
},
|
||||
"repository": "",
|
||||
"scripts": {
|
||||
"build": "ember build --environment=production",
|
||||
"release": "ember build --environment=production --output-path=release",
|
||||
"start": "ember server"
|
||||
},
|
||||
"devDependencies": {
|
||||
"broccoli-asset-rev": "^2.4.5",
|
||||
"ember-cli": "^3.2.0",
|
||||
"ember-cli-app-version": "^3.0.0",
|
||||
"ember-cli-babel": "^6.12.0",
|
||||
"ember-cli-bootstrap-4": "^0.5.6",
|
||||
"ember-cli-dependency-checker": "^1.3.0",
|
||||
"ember-cli-eslint": "^4.2.1",
|
||||
"ember-cli-htmlbars": "^2.0.1",
|
||||
"ember-cli-htmlbars-inline-precompile": "^1.0.0",
|
||||
"ember-cli-inject-live-reload": "^1.4.1",
|
||||
"ember-cli-pace": "^0.1.0",
|
||||
"ember-cli-qunit": "^4.1.1",
|
||||
"ember-cli-sass": "^7.1.7",
|
||||
"ember-cli-shims": "^1.1.0",
|
||||
"ember-cli-sri": "^2.1.0",
|
||||
"ember-cli-uglify": "^1.2.0",
|
||||
"ember-export-application-global": "^2.0.0",
|
||||
"ember-load-initializers": "^1.0.0",
|
||||
"ember-radio-button": "^1.1.1",
|
||||
"ember-resolver": "^4.0.0",
|
||||
"ember-sinon": "^1.0.1",
|
||||
"ember-sinon-qunit": "^2.0.0",
|
||||
"ember-source": "3.2.0",
|
||||
"ember-truth-helpers": "^2.0.0",
|
||||
"express": "^4.16.2",
|
||||
"glob": "^4.5.3",
|
||||
"jstree": "3.3.5",
|
||||
"loader.js": "^4.2.3",
|
||||
"morgan": "^1.9.0",
|
||||
"showdown": "^1.8.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^4.5 || 6.* || >= 7.*"
|
||||
}
|
||||
}
|
BIN
javascript/public/assets/32px.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
javascript/public/assets/40px.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
javascript/public/assets/favicon.ico
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
javascript/public/assets/haskell.ico
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
javascript/public/assets/throbber.gif
Normal file
After Width: | Height: | Size: 1.7 KiB |
3
javascript/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# http://www.robotstxt.org
|
||||
User-agent: *
|
||||
Disallow:
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 5.6 KiB |
BIN
javascript/release/assets/favicon.ico
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
javascript/release/assets/haskell.ico
Normal file
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.7 KiB |
5057
javascript/release/assets/vendor-1da6cdb46c95e61ce73ee429881d2b12.js
Normal file
25
javascript/release/index.html
Normal file
3
javascript/release/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# http://www.robotstxt.org
|
||||
User-agent: *
|
||||
Disallow:
|