Skip to content

Commit 86f5532

Browse files
authored
Merge pull request #73 from lamdera/clickable-errors-locations
Clickable error locations
2 parents 3f941db + 9db32f5 commit 86f5532

12 files changed

Lines changed: 394 additions & 295 deletions

File tree

builder/src/Build.hs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ compile (Env key root projectType _ buildID _ _) docsNeed (Details.Local path ti
708708
let
709709
pkg = projectTypeToPkg projectType
710710
in
711-
case Compile.compile pkg ifaces modul of
711+
case Compile.compile (Just (FP.makeRelative root path)) pkg ifaces modul of
712712
Right (Compile.Artifacts canonical annotations objects) ->
713713
case makeDocs docsNeed canonical of
714714
Left err ->
@@ -926,7 +926,7 @@ finalizeReplArtifacts env@(Env _ root projectType _ _ _ _) source modul@(Src.Mod
926926
projectTypeToPkg projectType
927927

928928
compileInput ifaces =
929-
case Compile.compile pkg ifaces modul of
929+
case Compile.compile Nothing pkg ifaces modul of
930930
Right (Compile.Artifacts canonical annotations objects) ->
931931
let
932932
h = Can._name canonical
@@ -1173,12 +1173,12 @@ checkRoot env@(Env _ root _ _ _ _ _) results rootStatus =
11731173

11741174

11751175
compileOutside :: Env -> Details.Local -> B.ByteString -> Map.Map ModuleName.Raw I.Interface -> Src.Module -> IO RootResult
1176-
compileOutside (Env key _ projectType _ _ _ _) (Details.Local path time _ _ _ _) source ifaces modul =
1176+
compileOutside (Env key root projectType _ _ _ _) (Details.Local path time _ _ _ _) source ifaces modul =
11771177
let
11781178
pkg = projectTypeToPkg projectType
11791179
name = Src.getName modul
11801180
in
1181-
case Compile.compile pkg ifaces modul of
1181+
case Compile.compile (Just (FP.makeRelative root path)) pkg ifaces modul of
11821182
Right (Compile.Artifacts canonical annotations objects) ->
11831183
do Reporting.report key Reporting.BDone
11841184
return $ ROutsideOk name (I.fromModule pkg canonical annotations) objects
@@ -1271,4 +1271,4 @@ lamderaIsLiveHarnessModule modul =
12711271
lamderaLiveHarnessEnv :: Env -> Env
12721272
lamderaLiveHarnessEnv env =
12731273
-- adds the Lamdera cache directory as an additional source directory
1274-
env { _srcDirs = AbsoluteSrcDir (Lamdera.lamderaCache $ _root env) : _srcDirs env }
1274+
env { _srcDirs = AbsoluteSrcDir (Lamdera.lamderaCache $ _root env) : _srcDirs env }

builder/src/Elm/Details.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ compile pkg mvar status =
689689
return Nothing
690690

691691
Just results ->
692-
case Compile.compile pkg (Map.mapMaybe getInterface results) modul of
692+
case Compile.compile Nothing pkg (Map.mapMaybe getInterface results) modul of
693693
Left _ ->
694694
return Nothing
695695

compiler/src/Compile.hs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,11 @@ import qualified Type.Solve as Type
3030

3131
import qualified Lamdera.Wire3.Core
3232
import qualified Lamdera.Wire3.Interfaces
33-
import qualified Lamdera.Wire3.Helpers as Lamdera.Wire
3433
import Lamdera
35-
import qualified CanSer.CanSer as ToSource
36-
import qualified Data.Text as T
37-
import qualified Data.Utf8
3834
import qualified Lamdera.UiSourceMap
3935
import qualified Lamdera.Nitpick.DebugLog
4036
import qualified Lamdera.Evergreen.ModifyAST
37+
import qualified System.FilePath as FP
4138

4239

4340
-- import StandaloneInstances
@@ -53,10 +50,10 @@ data Artifacts =
5350
}
5451

5552

56-
{- The original compile function for reference -}
57-
compile :: Pkg.Name -> Map.Map ModuleName.Raw I.Interface -> Src.Module -> Either E.Error Artifacts
58-
compile pkg ifaces modul =
59-
Lamdera.alternativeImplementationWhen Lamdera.isWireEnabled_ (compile_ pkg ifaces modul) $
53+
{- The original compile function for reference. `maybePath` parameter added by Lamdera. -}
54+
compile :: Maybe FilePath -> Pkg.Name -> Map.Map ModuleName.Raw I.Interface -> Src.Module -> Either E.Error Artifacts
55+
compile maybePath pkg ifaces modul =
56+
Lamdera.alternativeImplementationWhen Lamdera.isWireEnabled_ (compile_ maybePath pkg ifaces modul) $
6057
do canonical <- canonicalize pkg ifaces modul
6158
annotations <- typeCheck modul canonical
6259
() <- nitpick canonical
@@ -108,8 +105,8 @@ optimize modul annotations canonical =
108105

109106
-- @LAMDERA
110107

111-
compile_ :: Pkg.Name -> Map.Map ModuleName.Raw I.Interface -> Src.Module -> Either E.Error Artifacts
112-
compile_ pkg ifaces modul = do
108+
compile_ :: Maybe FilePath -> Pkg.Name -> Map.Map ModuleName.Raw I.Interface -> Src.Module -> Either E.Error Artifacts
109+
compile_ maybePath pkg ifaces modul = do
113110
-- @TEMPORARY debugging
114111
-- Inject stub definitions for wire functions, so the canonicalize phase can run
115112
-- Necessary for user-code which references yet-to-be generated functions
@@ -151,8 +148,13 @@ compile_ pkg ifaces modul = do
151148
canonical3 :: Can.Module
152149
canonical3 =
153150
if (Lamdera.unsafePerformIO Lamdera.isLiveMode)
154-
then Lamdera.UiSourceMap.updateDecls (Can._name canonical2) (Can._decls canonical2)
155-
& (\newDecls -> canonical2 { Can._decls = newDecls })
151+
then
152+
case maybePath of
153+
Just path ->
154+
Lamdera.UiSourceMap.updateDecls path (Can._name canonical2) (Can._decls canonical2)
155+
& (\newDecls -> canonical2 { Can._decls = newDecls })
156+
Nothing ->
157+
canonical2
156158
else canonical2
157159

158160
-- () <- debugPassText "starting optimize" moduleName (pure ())

elm.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ Executable lamdera
267267
Lamdera.Repl
268268
Lamdera.Reporting.Evergreen
269269
Lamdera.Reporting.Suggestions
270+
Lamdera.String
270271
Lamdera.TypeHash
271272
Lamdera.Types
272273
Lamdera.Update

ext-common/Ext/Common.hs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,6 @@ cq_ bin args input = do
420420
pure $ (exit, stdOut, stdErr)
421421

422422

423-
execCombineStdOutErr :: String -> [String] -> String -> IO String
424-
execCombineStdOutErr bin args input = do
425-
(exit, stdOut, stdErr) <- c_ bin args input
426-
pure $ stdErr <> stdOut
427-
428-
429423
requireBinary :: String -> IO FilePath
430424
requireBinary name = do
431425
x <- Dir.findExecutable name

ext-common/Ext/Query/Canonical.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ loadSingleArtifacts path = do
5454
source <- File.readUtf8 path
5555
case Parse.fromByteString Parse.Application source of
5656
Right modul ->
57-
case Compile.compile Pkg.dummyName ifaces modul of
57+
case Compile.compile Nothing Pkg.dummyName ifaces modul of
5858
Right artifacts ->
5959
pure artifacts
6060

extra/Lamdera/CLI/Live.hs

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Lamdera.CLI.Live where
99
import qualified Data.ByteString.Builder as B
1010
import qualified Data.ByteString as BS
1111
import qualified Data.ByteString.Lazy as BSL
12+
import qualified Data.ByteString.Char8
1213
import qualified Data.Text as T
1314
import qualified Data.Text.Lazy as TL
1415
import qualified Data.Text.Encoding as TE
@@ -20,11 +21,12 @@ import Data.Maybe (fromMaybe)
2021
import GHC.Word (Word64)
2122

2223
import qualified System.Directory as Dir
23-
import System.FilePath ((</>), takeExtension)
24+
import qualified System.FilePath as FP
25+
import System.FilePath ((</>))
2426
import Control.Applicative ((<|>))
2527
import Control.Arrow ((***))
2628
import Control.Concurrent.STM (atomically, newTVarIO, readTVar, readTVarIO, writeTVar, TVar)
27-
import Control.Exception (finally)
29+
import Control.Exception (finally, try, SomeException)
2830
import qualified Language.Haskell.TH as TH
2931
import Data.FileEmbed (bsToExp)
3032
import qualified Data.Aeson.Encoding as A
@@ -49,12 +51,13 @@ import System.Entropy (getEntropy)
4951
import Snap.Util.FileServe (
5052
getSafePath, serveDirectoryWith, defaultDirectoryConfig, defaultMimeTypes, mimeTypes, DirectoryConfig
5153
)
52-
import Control.Monad (guard)
54+
import Control.Monad (guard, mfilter)
5355

5456
import qualified Lamdera.CLI.Check
5557
import qualified Lamdera.Relative
5658
import qualified Lamdera.Version
5759
import qualified Ext.Common
60+
import qualified GHC.IO.Exception
5861

5962

6063

@@ -119,7 +122,7 @@ directoryConfig =
119122
serveUnmatchedUrlsToIndex :: FilePath -> (FilePath -> Snap()) -> Snap ()
120123
serveUnmatchedUrlsToIndex root serveElm =
121124
do file <- getSafePath
122-
guard (takeExtension file == "")
125+
guard (FP.takeExtension file == "")
123126
serveElm (lamderaCache root </> "Lamdera" </> "Live.elm")
124127

125128

@@ -309,26 +312,24 @@ serveWebsocket root (mClients, mLeader, mChan, beState) =
309312

310313
openEditorHandler :: FilePath -> Snap ()
311314
openEditorHandler root = do
312-
fullpath <- T.pack <$> getSafePath
313-
let
314-
handlers =
315-
-- *nix dir paths
316-
[ ("_x/editor", serveEditorOpen root)
317-
-- Windows dir paths
318-
, ("_x\\editor", serveEditorOpen root)
319-
]
320-
handlers
321-
& List.find (\(prefix, handler) ->
322-
prefix `T.isPrefixOf` fullpath
323-
)
324-
& fmap (\(prefix, handler) -> do
325-
let path =
326-
fullpath & T.replace (prefix <> "/") "" -- Strip when sub-dirs
327-
& T.replace (prefix <> "\\") "" -- Strip when sub-dirs windows
328-
& T.replace prefix "" -- Strip when root dir
329-
handler path
330-
)
331-
& withDefault pass
315+
fullpath <- getSafePath
316+
debug $ "_x/editor fullpath: " ++ fullpath
317+
case FP.splitDirectories fullpath of
318+
"_x" : "editor" : rest -> do
319+
maybeRow <- getQueryParam "row"
320+
maybeColumn <- getQueryParam "column"
321+
322+
let parseNonNegative = mfilter (>= 0) . readMaybe . Data.ByteString.Char8.unpack
323+
324+
case (maybeRow >>= parseNonNegative, maybeColumn >>= parseNonNegative) of
325+
(Just row, Just column) ->
326+
serveEditorOpen root (FP.joinPath rest) row column
327+
328+
_ ->
329+
error400PlainText "Unexpected request, expecting format: /_x/editor/<filename>?row=<row>&column=<column>"
330+
331+
_ ->
332+
pass
332333

333334

334335
serveBem :: LiveState -> Snap ()
@@ -431,80 +432,83 @@ serveExperimentalList root path = do
431432
error404 "folder not found"
432433

433434

434-
serveEditorOpen :: FilePath -> Text -> Snap ()
435-
serveEditorOpen root path = do
435+
serveEditorOpen :: FilePath -> FilePath -> Int -> Int -> Snap ()
436+
serveEditorOpen root path row column = do
436437
debug $ "_x/editor received: " ++ show path
437-
case path & T.splitOn ":" of
438-
file:row:column:xs -> do
439-
let fullpath = (root </> T.unpack file)
440-
debug $ "_x/editor: " ++ show fullpath
441-
exists_ <- liftIO $ Dir.doesFileExist fullpath
442-
if exists_
443-
then do
444-
tryOpenInDetectedEditor root fullpath row column
445-
446-
else do
447-
error404 "file not found"
448-
_ ->
449-
error404 "unexpected identifier, expecting format: <filename>:<row>:<column>"
438+
let fullpath = root </> path
439+
debug $ "_x/editor: " ++ show fullpath
440+
exists_ <- liftIO $ Dir.doesFileExist fullpath
441+
if exists_
442+
then do
443+
tryOpenInDetectedEditor root fullpath row column
444+
445+
else do
446+
error400PlainText "File not found"
450447

451448

452-
tryOpenInDetectedEditor :: FilePath -> FilePath -> Text -> Text -> Snap ()
449+
tryOpenInDetectedEditor :: FilePath -> FilePath -> Int -> Int -> Snap ()
453450
tryOpenInDetectedEditor root file row column = do
454-
res <- liftIO $ mapM id (editors root)
451+
res <- liftIO $ sequence (editors root)
455452
case justs res of
456453
[] ->
457-
-- @TODO give more helpful error that guides user how to configure things?
458-
error404 "No supported editors found"
454+
error404 "No supported editors found. See the Lamdera docs for more information."
459455

460-
(editor, openEditor):xs -> do
456+
(editor, openEditor):_ -> do
461457
debug "📝 found the following editors, opening first:"
462458
justs res & fmap fst & show & debug
463459

464-
liftIO $ openEditor file row column
465-
jsonResponse $ "{ status: 'tried opening editor " <> editor <> "' }"
460+
runRes <- liftIO (try (openEditor file row column) :: IO (Either SomeException (GHC.IO.Exception.ExitCode, String, String)))
461+
case runRes of
462+
Right (exit, stdout, stderr) ->
463+
case exit of
464+
GHC.IO.Exception.ExitSuccess ->
465+
noContentResponse
466+
467+
GHC.IO.Exception.ExitFailure exitCode ->
468+
error400PlainText $ Ext.Common.stringToBuilder $ "exit " <> show exitCode <> ": " <> stdout <> stderr
469+
470+
Left err ->
471+
error400PlainText $ Ext.Common.stringToBuilder $ show err
466472

467473

468-
type EditorOpenIO = (FilePath -> Text -> Text -> IO String)
474+
type EditorOpenIO = (FilePath -> Int -> Int -> IO (GHC.IO.Exception.ExitCode, String, String))
469475

470476

471477
editors :: FilePath -> [IO (Maybe (B.Builder, EditorOpenIO))]
472478
editors projectRoot =
473479
[ detectEditor "custom-*nix"
474480
(Dir.doesFileExist (projectRoot </> "openEditor.sh"))
475-
(\file row column -> Ext.Common.execCombineStdOutErr (projectRoot </> "openEditor.sh") [file, T.unpack row, T.unpack column] "")
481+
(\file row column -> Ext.Common.c_ (projectRoot </> "openEditor.sh") [file, show row, show column] "")
476482

477483
, detectEditor "custom-windows"
478484
(do
479485
exists <- Dir.doesFileExist (projectRoot </> "openEditor.bat")
480486
pure $ exists && ostype == Windows
481487
)
482-
(\file row column -> Ext.Common.execCombineStdOutErr (projectRoot </> "openEditor.bat") [file, T.unpack row, T.unpack column] "")
488+
(\file row column -> Ext.Common.c_ (projectRoot </> "openEditor.bat") [file, show row, show column] "")
483489

484490
, detectExecutable "code-insiders"
485491
(\executablePath file row column -> do
486-
Ext.Common.execCombineStdOutErr executablePath [ "-g", file <> ":" <> T.unpack row <> ":" <> T.unpack column] ""
492+
Ext.Common.c_ executablePath [ "-g", file <> ":" <> show row <> ":" <> show column] ""
487493
)
488494

489495
, detectExecutable "code"
490496
(\executablePath file row column -> do
491-
Ext.Common.execCombineStdOutErr executablePath [ "-g", file <> ":" <> T.unpack row <> ":" <> T.unpack column] ""
497+
Ext.Common.c_ executablePath [ "-g", file <> ":" <> show row <> ":" <> show column] ""
492498
)
493499

494500
, detectEditor "intellij-ce"
495501
(Dir.doesDirectoryExist "/Applications/IntelliJ IDEA CE.app")
496502
(\file row column -> do
497-
let column_ :: Int = column & readMaybeText & withDefault 1
498503
-- IntelliJ seems to number it's columns from 1 index
499-
Ext.Common.execCombineStdOutErr "open" ["-na", "IntelliJ IDEA CE.app", "--args", "--line", T.unpack row, "--column", show (column_ - 1), file] ""
504+
Ext.Common.c_ "open" ["-na", "IntelliJ IDEA CE.app", "--args", "--line", show row, "--column", show (column - 1), file] ""
500505
)
501506

502507
, detectEditor "intellij"
503508
(Dir.doesDirectoryExist "/Applications/IntelliJ IDEA.app")
504509
(\file row column -> do
505-
let column_ :: Int = column & readMaybeText & withDefault 1
506510
-- IntelliJ seems to number it's columns from 1 index
507-
Ext.Common.execCombineStdOutErr "open" ["-na", "IntelliJ IDEA.app", "--args", "--line", T.unpack row, "--column", show (column_ - 1), file] ""
511+
Ext.Common.c_ "open" ["-na", "IntelliJ IDEA.app", "--args", "--line", show row, "--column", show (column - 1), file] ""
508512
)
509513
]
510514

@@ -747,6 +751,14 @@ logger =
747751
atomicPutStrLn $ T.unpack $ TE.decodeUtf8 bs
748752
)
749753

754+
755+
noContentResponse :: Snap ()
756+
noContentResponse = do
757+
modifyResponse $ setResponseStatus 204 "No Content"
758+
r <- getResponse
759+
finishWith r
760+
761+
750762
jsonResponse :: B.Builder -> Snap ()
751763
jsonResponse s =
752764
do modifyResponse $ setContentType "application/json; charset=utf-8"
@@ -784,11 +796,11 @@ error503 s =
784796
r <- getResponse
785797
finishWith r
786798

787-
error400 :: B.Builder -> Snap ()
788-
error400 s =
799+
error400PlainText :: B.Builder -> Snap ()
800+
error400PlainText s =
789801
do modifyResponse $ setResponseStatus 400 "Bad Request"
790-
modifyResponse $ setContentType "application/json; charset=utf-8"
791-
writeBuilder $ "{\"error\":\"" <> s <> "\"}"
802+
modifyResponse $ setContentType "text/plain; charset=utf-8"
803+
writeBuilder s
792804
r <- getResponse
793805
finishWith r
794806

extra/Lamdera/String.hs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module Lamdera.String (fromChars) where
2+
3+
import qualified Data.Utf8 as Utf8
4+
import qualified Elm.String as ES
5+
6+
7+
fromChars :: [Char] -> ES.String
8+
fromChars =
9+
Utf8.fromChars . escape
10+
11+
12+
escape :: [Char] -> [Char]
13+
escape =
14+
foldr
15+
(\c acc ->
16+
case c of
17+
'\\' -> '\\' : '\\' : acc
18+
'\'' -> '\\' : '\'' : acc
19+
'\n' -> '\\' : 'n' : acc
20+
'\r' -> '\\' : 'r' : acc
21+
_ -> c : acc
22+
)
23+
[]

0 commit comments

Comments
 (0)