Skip to content

Commit 8774126

Browse files
committed
Generate editor keywords
- part of #344
1 parent ef6cba0 commit 8774126

File tree

8 files changed

+225
-37
lines changed

8 files changed

+225
-37
lines changed

app/Main.hs

+21-12
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@
33

44
module Main where
55

6-
import Data.Text (
7-
Text,
8-
pack,
9-
)
6+
import Data.Foldable qualified
7+
import Data.Text (Text, pack)
108
import Data.Text.IO qualified as Text
11-
import GitHash
9+
import GitHash (giBranch, giHash, tGitInfoCwdTry)
1210
import Options.Applicative
1311
import Swarm.App (appMain)
14-
import Swarm.DocGen (GenerateDocs (..), generateDocs)
12+
import Swarm.DocGen (EditorType (..), GenerateDocs (..), generateDocs)
1513
import Swarm.Language.LSP (lspMain)
1614
import Swarm.Language.Pipeline (processTerm)
17-
import System.Exit
15+
import System.Exit (exitFailure, exitSuccess)
1816

1917
data CLI
2018
= Run
@@ -29,9 +27,11 @@ data CLI
2927
cliParser :: Parser CLI
3028
cliParser =
3129
subparser
32-
( command "format" (info (format <**> helper) (progDesc "Format a file"))
33-
<> command "lsp" (info (pure LSP) (progDesc "Start the LSP"))
34-
<> command "generate" (info (DocGen <$> docgen <**> helper) (progDesc "Generate docs"))
30+
( mconcat
31+
[ command "format" (info (format <**> helper) (progDesc "Format a file"))
32+
, command "lsp" (info (pure LSP) (progDesc "Start the LSP"))
33+
, command "generate" (info (DocGen <$> docgen <**> helper) (progDesc "Generate docs"))
34+
]
3535
)
3636
<|> Run <$> seed <*> scenario <*> run <*> cheat
3737
where
@@ -41,8 +41,17 @@ cliParser =
4141
<|> (Format . File <$> strArgument (metavar "FILE"))
4242
docgen :: Parser GenerateDocs
4343
docgen =
44-
subparser
45-
(command "recipes" (info (pure RecipeGraph) $ progDesc "Output graphviz dotfile of entity dependencies based on recipes"))
44+
subparser . mconcat $
45+
[ command "recipes" (info (pure RecipeGraph) $ progDesc "Output graphviz dotfile of entity dependencies based on recipes")
46+
, command "editors" (info (EditorKeywords <$> editor <**> helper) $ progDesc "Output editor keywords")
47+
]
48+
editor :: Parser (Maybe EditorType)
49+
editor =
50+
Data.Foldable.asum
51+
[ pure Nothing
52+
, Just VSCode <$ switch (long "code" <> help "Generate for the VS Code editor")
53+
, Just Emacs <$ switch (long "emacs" <> help "Generate for the Emacs editor")
54+
]
4655
seed :: Parser (Maybe Int)
4756
seed = optional $ option auto (long "seed" <> short 's' <> metavar "INT" <> help "Seed to use for world generation")
4857
scenario :: Parser (Maybe String)

editors/emacs/swarm-mode.el

+67-11
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,74 @@
2525

2626
(setq swarm-font-lock-keywords
2727
(let* (
28-
;; We should figure out how to autogenerate these, so we don't have
29-
;; to edit the emacs mode every time we add new commands.
28+
;; Generate the current keywords with:
29+
;; cabal run swarm:swarm -- generate editors --emacs
3030
(x-keywords '("def" "end"))
31-
(x-builtins '("if" "run" "return" "try" "raise" "force" "fst" "snd"))
32-
(x-commands
33-
'("noop" "wait" "selfdestruct" "move" "turn" "grab" "place" "give"
34-
"install" "make" "build" "salvage" "reprogram" "knows"
35-
"say" "log" "view" "appear" "create" "getx" "gety"
36-
"blocked" "scan" "upload" "ishere" "whoami"
37-
"random" "not"
38-
"left" "right" "back" "forward" "north" "south" "east" "west" "down"
39-
))
31+
(x-builtins '(
32+
"if"
33+
"run"
34+
"return"
35+
"try"
36+
"fail"
37+
"force"
38+
"fst"
39+
"snd"
40+
))
41+
(x-commands '(
42+
"noop"
43+
"wait"
44+
"selfdestruct"
45+
"move"
46+
"turn"
47+
"grab"
48+
"harvest"
49+
"place"
50+
"give"
51+
"install"
52+
"make"
53+
"has"
54+
"count"
55+
"drill"
56+
"build"
57+
"salvage"
58+
"reprogram"
59+
"say"
60+
"log"
61+
"view"
62+
"appear"
63+
"create"
64+
"whereami"
65+
"blocked"
66+
"scan"
67+
"upload"
68+
"ishere"
69+
"self"
70+
"parent"
71+
"base"
72+
"whoami"
73+
"setname"
74+
"random"
75+
"inl"
76+
"inr"
77+
"case"
78+
"undefined"
79+
"not"
80+
"format"
81+
"teleport"
82+
"as"
83+
"robotnamed"
84+
"robotnumbered"
85+
"knows"
86+
"left"
87+
"right"
88+
"back"
89+
"forward"
90+
"north"
91+
"south"
92+
"east"
93+
"west"
94+
"down"
95+
))
4096
(x-types '("int" "string" "dir" "bool" "cmd"))
4197

4298
(x-keywords-regexp (regexp-opt x-keywords 'words))

editors/vscode/DEVELOPING.md

+3-11
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,9 @@
2929

3030
Whenever swarm language adds new features, the highlighing needs to be updated.
3131

32-
To save some time, get the current reserved words by running `cabal repl`:
33-
```haskell
34-
import Swarm.Language.Syntax
35-
import qualified Data.Text as T
36-
:set -XOverloadedStrings
37-
38-
-- get basic functions/commands
39-
T.intercalate "|" $ map (syntax . constInfo) (filter isUserFunc allConst)
40-
41-
-- get list of directions
42-
T.intercalate "|" $ map (dirSyntax . dirInfo) allDirs
32+
To save some time, get the current reserved words by running `swarm generate`:
33+
```bash
34+
cabal run swarm:swarm -- generate editors
4335
```
4436

4537
You still have to add for example types manually.

editors/vscode/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "swarm-language",
33
"displayName": "swarm-language",
44
"description": "VSCode support for swarm (the game) programming language.",
5-
"version": "0.0.3",
5+
"version": "0.0.4",
66
"icon": "images/swarm-logo.png",
77
"publisher": "xsebek",
88
"repository": {

editors/vscode/syntaxes/swarm.tmLanguage.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
},
4848
{
4949
"name": "keyword.operator",
50-
"match": "(<|>|==|<=|>=|!=|=|;|<-|-|\\+|\\*|\\^|\\$|/(?![/|*]))"
50+
"match": "-|==|!=|<|>|<=|>=|\\|\\||&&|\\+|-|\\*|/(?![/|*])|\\^|\\+\\+|\\$"
5151
},
5252
{
5353
"name": "keyword.operator.lambda",
@@ -56,7 +56,7 @@
5656
},
5757
{
5858
"name": "keyword.other",
59-
"match": "\\b(?i)(noop|wait|selfdestruct|move|turn|grab|place|give|install|make|has|count|drill|build|salvage|reprogram|say|log|view|appear|create|whereami|blocked|scan|upload|ishere|whoami|random|run|if|inl|inr|case|fst|snd|force|return|try|raise|not)\\b"
59+
"match": "\\b(?i)(noop|wait|selfdestruct|move|turn|grab|harvest|place|give|install|make|has|count|drill|build|salvage|reprogram|say|log|view|appear|create|whereami|blocked|scan|upload|ishere|self|parent|base|whoami|setname|random|run|if|inl|inr|case|fst|snd|force|return|try|undefined|fail|not|format|teleport|as|robotnamed|robotnumbered|knows)\\b"
6060
}
6161
]
6262
},

src/Swarm/DocGen.hs

+89
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
module Swarm.DocGen (
44
generateDocs,
55
GenerateDocs (..),
6+
EditorType (..),
7+
8+
-- ** Formatted keyword lists
9+
keywordsCommands,
10+
keywordsDirections,
11+
operatorNames,
12+
builtinCommandsListEmacs,
13+
editorList,
614
) where
715

816
import Control.Lens (view, (^.))
@@ -17,13 +25,17 @@ import Data.Maybe (fromMaybe)
1725
import Data.Set (Set)
1826
import Data.Set qualified as Set
1927
import Data.Text (Text, unpack)
28+
import Data.Text qualified as T
29+
import Data.Text.IO qualified as T
2030
import Data.Tuple (swap)
2131
import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityName, loadEntities)
2232
import Swarm.Game.Entity qualified as E
2333
import Swarm.Game.Recipe (Recipe, loadRecipes, recipeInputs, recipeOutputs, recipeRequirements)
2434
import Swarm.Game.Robot (installedDevices, robotInventory, setRobotID)
2535
import Swarm.Game.Scenario (Scenario, loadScenario, scenarioRobots)
2636
import Swarm.Game.WorldGen (testWorld2Entites)
37+
import Swarm.Language.Syntax (Const (..), ConstMeta (..))
38+
import Swarm.Language.Syntax qualified as Syntax
2739
import Swarm.Util (isRightOr)
2840
import Text.Dot (Dot, NodeId, (.->.))
2941
import Text.Dot qualified as Dot
@@ -39,11 +51,88 @@ import Text.Dot qualified as Dot
3951
data GenerateDocs where
4052
-- | Entity dependencies by recipes.
4153
RecipeGraph :: GenerateDocs
54+
-- | Keyword lists for editors.
55+
EditorKeywords :: Maybe EditorType -> GenerateDocs
4256
deriving (Eq, Show)
4357

58+
data EditorType = Emacs | VSCode
59+
deriving (Eq, Show, Enum, Bounded)
60+
4461
generateDocs :: GenerateDocs -> IO ()
4562
generateDocs = \case
4663
RecipeGraph -> generateRecipe >>= putStrLn
64+
EditorKeywords e ->
65+
case e of
66+
Just et -> generateEditorKeywords et
67+
Nothing -> do
68+
putStrLn "All editor completions:"
69+
let editorGen et = do
70+
putStrLn $ replicate 40 '-'
71+
putStrLn $ "-- " <> show et
72+
putStrLn $ replicate 40 '-'
73+
generateEditorKeywords et
74+
mapM_ editorGen [minBound .. maxBound]
75+
76+
-- ----------------------------------------------------------------------------
77+
-- GENERATE KEYWORDS: LIST OF WORDS TO BE HIGHLIGHTED
78+
-- ----------------------------------------------------------------------------
79+
80+
generateEditorKeywords :: EditorType -> IO ()
81+
generateEditorKeywords = \case
82+
Emacs -> do
83+
putStrLn "(x-builtins '("
84+
T.putStr . editorList Emacs $ map constSyntax builtinCommandsEmacs
85+
putStrLn "))\n(x-commands '("
86+
T.putStr $ keywordsCommands Emacs
87+
T.putStr $ keywordsDirections Emacs
88+
putStrLn "))"
89+
VSCode -> do
90+
putStrLn "Functions and commands:"
91+
T.putStrLn $ keywordsCommands VSCode
92+
putStrLn "\nDirections:"
93+
T.putStrLn $ keywordsDirections VSCode
94+
putStrLn "\nOperators:"
95+
T.putStrLn operatorNames
96+
97+
builtinCommandsEmacs :: [Const]
98+
builtinCommandsEmacs = [If, Run, Return, Try, Fail, Force, Fst, Snd]
99+
100+
builtinCommandsListEmacs :: Text
101+
builtinCommandsListEmacs = editorList Emacs $ map constSyntax builtinCommandsEmacs
102+
103+
editorList :: EditorType -> [Text] -> Text
104+
editorList = \case
105+
Emacs -> T.unlines . map ((" " <>) . quote)
106+
VSCode -> T.intercalate "|"
107+
where
108+
quote = T.cons '"' . flip T.snoc '"'
109+
110+
constSyntax :: Const -> Text
111+
constSyntax = Syntax.syntax . Syntax.constInfo
112+
113+
-- | Get formatted list of basic functions/commands.
114+
keywordsCommands :: EditorType -> Text
115+
keywordsCommands e = editorList e $ map constSyntax (filter isFunc Syntax.allConst)
116+
where
117+
isFunc c = Syntax.isUserFunc c && (e /= Emacs || c `notElem` builtinCommandsEmacs)
118+
119+
-- | Get formatted list of directions.
120+
keywordsDirections :: EditorType -> Text
121+
keywordsDirections e = editorList e $ map (Syntax.dirSyntax . Syntax.dirInfo) Syntax.allDirs
122+
123+
operatorNames :: Text
124+
operatorNames = T.intercalate "|" $ map (escape . constSyntax) (filter isOperator Syntax.allConst)
125+
where
126+
special :: String
127+
special = "*+$[]|^"
128+
slashNotComment = \case
129+
'/' -> "/(?![/|*])"
130+
c -> T.singleton c
131+
escape = T.concatMap (\c -> if c `elem` special then T.snoc "\\\\" c else slashNotComment c)
132+
isOperator c = case Syntax.constMeta $ Syntax.constInfo c of
133+
ConstMUnOp {} -> True
134+
ConstMBinOp {} -> True
135+
ConstMFunc {} -> False
47136

48137
-- ----------------------------------------------------------------------------
49138
-- GENERATE GRAPHVIZ: ENTITY DEPENDENCIES BY RECIPES

swarm.cabal

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ category: Game
2121
tested-with: GHC ==8.10.7 || ==9.0.2 || ==9.2.3
2222
extra-source-files: CHANGELOG.md
2323
example/*.sw
24+
editors/emacs/*.el
25+
editors/vscode/syntaxes/*.json
2426
data-dir: data/
2527
data-files: *.yaml, scenarios/**/*.yaml, scenarios/**/*.txt, scenarios/**/*.sw, *.txt
2628

test/integration/Main.hs

+40
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ import Control.Lens (Ixed (ix), to, use, view, (&), (.~), (<&>), (^.), (^?!))
99
import Control.Monad (filterM, forM_, unless, void, when)
1010
import Control.Monad.State (StateT (runStateT), gets)
1111
import Control.Monad.Trans.Except (runExceptT)
12+
import Data.Char (isSpace)
1213
import Data.Containers.ListUtils (nubOrd)
1314
import Data.Foldable (Foldable (toList), find)
1415
import Data.IntSet qualified as IS
1516
import Data.Map qualified as M
1617
import Data.Maybe (isJust)
1718
import Data.Text (Text)
1819
import Data.Text qualified as T
20+
import Data.Text.IO qualified as T
1921
import Data.Yaml (ParseException, prettyPrintParseException)
22+
import Swarm.DocGen (EditorType (..))
23+
import Swarm.DocGen qualified as DocGen
2024
import Swarm.Game.CESK (emptyStore, initMachine)
2125
import Swarm.Game.Entity (EntityMap, loadEntities)
2226
import Swarm.Game.Robot (leText, machine, robotLog, waitingUntil)
@@ -62,6 +66,7 @@ main = do
6266
, exampleTests scenarioPrograms
6367
, scenarioTests em scenarioPaths
6468
, testScenarioSolution ci em
69+
, testEditorFiles
6570
]
6671

6772
exampleTests :: [(FilePath, String)] -> TestTree
@@ -214,3 +219,38 @@ printAllLogs g =
214219
mapM_
215220
(\r -> forM_ (r ^. robotLog) (putStrLn . T.unpack . view leText))
216221
(g ^. robotMap)
222+
223+
-- | Test that editor files are up-to-date.
224+
testEditorFiles :: TestTree
225+
testEditorFiles =
226+
testGroup
227+
"editors"
228+
[ testGroup
229+
"VS Code"
230+
[ testTextInVSCode "operators" (const DocGen.operatorNames)
231+
, testTextInVSCode "commands" DocGen.keywordsCommands
232+
, testTextInVSCode "directions" DocGen.keywordsDirections
233+
]
234+
, testGroup
235+
"Emacs"
236+
[ testTextInEmacs "builtin" (const DocGen.builtinCommandsListEmacs)
237+
, testTextInEmacs "commands" DocGen.keywordsCommands
238+
, testTextInEmacs "directions" DocGen.keywordsDirections
239+
]
240+
]
241+
where
242+
testTextInVSCode name tf = testTextInFile False name (tf VSCode) "editors/vscode/syntaxes/swarm.tmLanguage.json"
243+
testTextInEmacs name tf = testTextInFile True name (tf Emacs) "editors/emacs/swarm-mode.el"
244+
testTextInFile :: Bool -> String -> Text -> FilePath -> TestTree
245+
testTextInFile whitespace name t fp = testCase name $ do
246+
let removeLW' = T.unlines . map (T.dropWhile isSpace) . T.lines
247+
removeLW = if whitespace then removeLW' else id
248+
f <- T.readFile fp
249+
assertBool
250+
( "EDITOR FILE IS NOT UP TO DATE!\n"
251+
<> "I could not find the text:\n"
252+
<> T.unpack t
253+
<> "\nin file "
254+
<> fp
255+
)
256+
(removeLW t `T.isInfixOf` removeLW f)

0 commit comments

Comments
 (0)