Skip to content

Commit ba1154c

Browse files
committed
Settings for VSCode extension
1 parent 5320eb3 commit ba1154c

File tree

23 files changed

+222
-122
lines changed

23 files changed

+222
-122
lines changed

dist/bundle.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ description="PureScript linter, extensible, with configurable rules, and one-off
1010

1111
for file in $ROOT/dist/npm/package.json $ROOT/dist/vscode-extension/package.json; do
1212
sed -i "s/\"version\": \".*\"/\"version\": \"$version\"/g" $file
13-
sed -i "s/\"description\": \".*\"/\"description\": \"$description\"/g" $file
13+
sed -i "s/\n \"description\": \".*\"/\n \"description\": \"$description\"/g" $file
1414
done
1515

1616
cp $ROOT/LICENSE.txt $ROOT/dist/vscode-extension

dist/npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "whine",
3-
"version": "0.0.13",
3+
"version": "0.0.15",
44
"description": "PureScript linter, extensible, with configurable rules, and one-off escape hatches",
55
"keywords": ["purescript", "lint"],
66
"author": "Fyodor Soikin <name.fa@gmail.com>",

dist/vscode-extension/package.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "collegevine",
44
"displayName": "Whine at PureScript",
55
"description": "PureScript linter, extensible, with configurable rules, and one-off escape hatches",
6-
"version": "0.0.13",
6+
"version": "0.0.15",
77
"repository": "https://github.com/collegevine/purescript-whine",
88
"engines": {
99
"vscode": "^1.95.0"
@@ -12,5 +12,25 @@
1212
"Other"
1313
],
1414
"activationEvents": ["onLanguage:purescript"],
15-
"main": "./extension.js"
15+
"main": "./extension.js",
16+
"contributes": {
17+
"configuration": {
18+
"title": "Whine at PureScript",
19+
"properties": {
20+
"whine.languageServerStartCommand": {
21+
"type": "string",
22+
"scope": "resource",
23+
"default": "npx -y whine language-server --quiet",
24+
"description": "Command to start the Whine language server."
25+
},
26+
"whine.checkOn": {
27+
"type": "string",
28+
"enum": ["save", "change"],
29+
"scope": "resource",
30+
"default": "save",
31+
"description": "When to check the file for violations. Choosing 'change' may put extra load on your system as Whine will recheck the file on every single keystroke."
32+
}
33+
}
34+
}
35+
}
1636
}

spago.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ package:
4343
main: Test.Main
4444
dependencies: []
4545
publish:
46-
version: 0.0.13
46+
version: 0.0.15
4747
license: MIT
4848
location:
4949
githubOwner: collegevine

src/Whine/Prelude.purs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Control.Monad.Error.Class (try) as Reexport
1010
import Control.Monad.Except (mapExcept, mapExceptT, runExcept, runExceptT) as Reexport
1111
import Control.Monad.Writer (class MonadWriter, WriterT, execWriterT, mapWriterT, tell) as Reexport
1212

13-
import Data.Array ((:), (..), nub, null, cons, drop, filter, length, catMaybes, mapMaybe, last, take, zip, zipWith) as Reexport
13+
import Data.Array ((:), (..), (\\), nub, null, cons, drop, elem, filter, length, catMaybes, mapMaybe, last, take, uncons, zip, zipWith) as Reexport
1414
import Data.Array.NonEmpty (NonEmptyArray) as Reexport
1515
import Data.Bifunctor (bimap, lmap, rmap) as Reexport
1616
import Data.Either (Either(..), fromRight, either, hush, note) as Reexport
@@ -22,6 +22,7 @@ import Data.Newtype (wrap, unwrap, un) as Reexport
2222
import Data.Nullable (Nullable) as Reexport
2323
import Data.Profunctor (dimap) as Reexport
2424
import Data.String (Pattern(..), joinWith) as Reexport
25+
import Data.String.NonEmpty (NonEmptyString) as Reexport
2526
import Data.Traversable (any, fold, for, for_, intercalate, traverse, traverse_, sequence, sequence_) as Reexport
2627
import Data.Tuple (fst, snd) as Reexport
2728
import Data.Tuple.Nested (type (/\), (/\)) as Reexport

src/Whine/Runner/Cli.purs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ type Args =
1212
, quiet :: Boolean
1313
}
1414

15-
data Command = JustWhine | LanguageServer
15+
data Command = JustWhine | LanguageServer CheckFileWhen
16+
17+
data CheckFileWhen = CheckOnSave | CheckOnChange
1618

1719
parseCliArgs :: m. MonadEffect m => m Args
1820
parseCliArgs = liftEffect $
@@ -26,14 +28,17 @@ parseCliArgs = liftEffect $
2628
commandParser :: O.Parser Command
2729
commandParser =
2830
O.subparser $ O.command "language-server" $
29-
O.info (ignoreLspOptions $> LanguageServer) (O.progDesc "Start Whine in Language Server mode")
31+
O.info
32+
(LanguageServer <$> languageServerArgsParser)
33+
(O.progDesc "Start Whine in Language Server mode")
3034

31-
ignoreLspOptions :: O.Parser Unit
32-
ignoreLspOptions = ado
35+
languageServerArgsParser :: O.Parser CheckFileWhen
36+
languageServerArgsParser = ado
3337
_ <- O.switch $ O.long "stdio"
3438
_ <- O.switch $ O.long "node-ipc"
3539
_ <- OT.optional $ O.strOption $ O.long "socket"
36-
in unit
40+
checkWhen <- checkWhenOption
41+
in checkWhen # fromMaybe CheckOnSave
3742

3843
argsParser :: O.Parser Args
3944
argsParser =
@@ -80,3 +85,23 @@ determineLogLevel args =
8085
if args.debug then LogDebug
8186
else if args.quiet then LogError
8287
else LogInfo
88+
89+
checkWhenOption :: O.Parser (Maybe CheckFileWhen)
90+
checkWhenOption =
91+
OT.optional $ O.option checkWhen $ fold
92+
[ O.long "check-on"
93+
, O.help $ fold
94+
[ "When to check files for violations. Possible values are "
95+
, "'", saveOption, "' to check when a file is saved or "
96+
, "'", changeOption, "' to check on every change. "
97+
, "Default is '", saveOption, "'."
98+
]
99+
]
100+
where
101+
saveOption = "save"
102+
changeOption = "change"
103+
104+
checkWhen = O.eitherReader \s -> do
105+
if s == saveOption then Right CheckOnSave
106+
else if s == changeOption then Right CheckOnChange
107+
else Left $ "Invalid value: " <> s

src/Whine/Runner/Config.purs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,42 @@ import Whine.Runner.Prelude
55
import Codec.JSON.DecodeError as DecodeError
66
import Control.Monad.Error.Class (throwError)
77
import Control.Monad.Writer (runWriter)
8-
import Data.Array.NonEmpty as NEA
98
import Data.Codec as Codec
109
import Data.Codec.JSON as CJ
1110
import Data.Codec.JSON.Common as CJ.Common
1211
import Data.Codec.JSON.Strict as CJS
1312
import Data.Map as Map
13+
import Data.String.NonEmpty as NES
1414
import Effect.Exception as Err
1515
import JSON as JSON
1616
import Node.FS.Sync (readTextFile)
1717
import Record (merge)
18-
import Whine.Types (RuleFactories, RuleSet, Violations, WithFile, WithRule)
18+
import Whine.Runner.Glob (Globs)
1919
import Whine.Runner.Yaml (parseYaml)
20+
import Whine.Types (RuleFactories, RuleId, Violations, WithFile, WithRule, Rule)
2021

2122
data PackageSpec
2223
= JustPackage
2324
| PackageVersion String
2425
| LocalPackage { path :: FilePath, module :: Maybe String }
2526

26-
type Config =
27+
type Config m =
28+
{ rules :: RuleSet m
29+
, files :: Globs
30+
}
31+
32+
-- | For every rule ID we have a rule implementation `Rule m` and some common
33+
-- | config values that can be applied to any rule and are handled by the
34+
-- | framework.
35+
type RuleSet m = Map RuleId
36+
{ rule :: Rule m
37+
, globs :: Globs
38+
}
39+
40+
type ConfigJson =
2741
{ rulePackages :: Map { package :: String } PackageSpec
42+
, include :: Maybe (Array NonEmptyString)
43+
, exclude :: Maybe (Array NonEmptyString)
2844
, rules :: Maybe (Map String JSON)
2945
}
3046

@@ -54,8 +70,8 @@ type Config =
5470
-- | The whole thing runs in a writer monad to which it can report any errors
5571
-- | while parsing the config. The errors doesn't stop the parsing, but
5672
-- | erroneous rules do not make it into the resulting map.
57-
parseConfig :: m n. MonadWriter (Violations (WithRule + ())) m => RuleFactories n -> Map String JSON -> m (RuleSet n)
58-
parseConfig factories config =
73+
parseRuleConfigs :: m n. MonadWriter (Violations (WithRule + ())) m => RuleFactories n -> Map String JSON -> m (RuleSet n)
74+
parseRuleConfigs factories config =
5975
Map.fromFoldable <$> catMaybes <$>
6076
for factories \(ruleId /\ factory) -> do
6177

@@ -77,17 +93,17 @@ parseConfig factories config =
7793
Just value ->
7894
value # CJ.decode codec # lmap DecodeError.print # orFail ("Malformed '" <> name <> "'")
7995

80-
include <- commonProp "include" (CJ.array CJ.string)
81-
exclude <- commonProp "exclude" (CJ.array CJ.string)
96+
include <- commonProp "include" (CJ.array CJ.Common.nonEmptyString)
97+
exclude <- commonProp "exclude" (CJ.array CJ.Common.nonEmptyString)
8298
enabled <- commonProp "enabled" CJ.boolean <#> fromMaybe true
8399

84100
if enabled then
85101
factory (fromMaybe JSON.null ruleConfig)
86102
<#> (\rule -> ruleId /\
87103
{ rule
88104
, globs:
89-
{ include: include >>= NEA.fromArray
90-
, exclude: exclude >>= NEA.fromArray
105+
{ include: include # fromMaybe []
106+
, exclude: exclude # fromMaybe []
91107
}
92108
}
93109
)
@@ -96,7 +112,7 @@ parseConfig factories config =
96112
pure Nothing
97113

98114
-- | Reads config from a given file and parses it.
99-
readConfig :: m n. MonadEffect m => MonadWriter (Violations (WithRule + WithFile + ())) m => RuleFactories n -> FilePath -> m (RuleSet n)
115+
readConfig :: m n. MonadEffect m => MonadWriter (Violations (WithRule + WithFile + ())) m => RuleFactories n -> FilePath -> m (Config n)
100116
readConfig factories configFile = do
101117
text <- lmap Err.message <$> liftEffect (try $ readTextFile UTF8 configFile)
102118

@@ -114,14 +130,22 @@ readConfig factories configFile = do
114130
Right c ->
115131
pure c
116132

117-
let res /\ violations = runWriter $ parseConfig factories (config.rules # fromMaybe Map.empty)
133+
let res /\ violations = runWriter $ parseRuleConfigs factories (config.rules # fromMaybe Map.empty)
118134

119135
tell $ violations <#> merge { file: { path: configFile, lines: Nothing } }
120-
pure res
121-
122-
configCodec :: CJ.Codec Config
136+
pure
137+
{ rules: res
138+
, files:
139+
{ include: config.include # fromMaybe (NES.fromString `mapMaybe` ["src/**/*.purs", "test/**/*.purs"])
140+
, exclude: config.exclude # fromMaybe []
141+
}
142+
}
143+
144+
configCodec :: CJ.Codec ConfigJson
123145
configCodec = CJS.objectStrict $ CJS.record
124146
# CJS.recordProp @"rulePackages" packagesCodec
147+
# CJS.recordPropOptional @"include" (CJ.array CJ.Common.nonEmptyString)
148+
# CJS.recordPropOptional @"exclude" (CJ.array CJ.Common.nonEmptyString)
125149
# CJS.recordPropOptional @"rules" (CJ.Common.strMap CJ.json)
126150

127151
packagesCodec :: CJ.Codec (Map { package :: String } PackageSpec)
@@ -157,8 +181,10 @@ packagesCodec = dimap Map.toUnfoldable Map.fromFoldable $ CJ.array packageCodec
157181
# CJS.recordProp @"local" CJ.string
158182
# CJS.recordPropOptional @"module" CJ.string
159183

160-
defaultConfig :: Config
184+
defaultConfig :: ConfigJson
161185
defaultConfig =
162186
{ rulePackages: Map.singleton { package: "whine-core" } JustPackage
187+
, include: Nothing
188+
, exclude: Nothing
163189
, rules: Nothing
164190
}

src/Whine/Runner/Glob.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import mm from 'micromatch'
2-
export { globSync } from 'glob'
2+
import { globSync } from 'glob'
33

4-
export const test = patterns => file => mm.isMatch(file, patterns)
4+
export const test = ({ include, exclude }) => file => mm.isMatch(file, [...include, ...exclude.map(e => `!${e}`)])
5+
6+
export const glob = ({ include, exclude }) => () => globSync(include, { ignore: exclude })

src/Whine/Runner/Glob.purs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
module Whine.Runner.Glob (glob, test) where
1+
module Whine.Runner.Glob where
22

33
import Whine.Runner.Prelude
4-
import Effect.Uncurried (EffectFn1, runEffectFn1)
54

6-
glob :: String -> Effect (Array String)
7-
glob = runEffectFn1 globSync
5+
type Globs = { include :: Array NonEmptyString, exclude :: Array NonEmptyString }
86

9-
foreign import globSync :: EffectFn1 String (Array String)
7+
foreign import glob :: Globs -> Effect (Array FilePath)
108

11-
foreign import test :: Array String -> FilePath -> Boolean
9+
foreign import test :: Globs -> FilePath -> Boolean
10+
11+
isEmptyGlobs :: Globs -> Boolean
12+
isEmptyGlobs { include, exclude } = null include && null exclude

src/Whine/Runner/LanguageServer.purs

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,70 @@ import Data.String as String
77
import Node.Path as NodePath
88
import Record (merge)
99
import Untagged.Castable (cast)
10+
import Vscode.Events (on)
1011
import Vscode.Server.Capabilities (textDocumentSyncKind)
1112
import Vscode.Server.Connection as Conn
1213
import Vscode.Server.Diagnostic (diagnosticSeverity)
13-
import Vscode.Events (on)
1414
import Vscode.Server.TextDocuments as Doc
1515
import Whine (checkModule)
16+
import Whine.Runner.Cli (CheckFileWhen(..))
1617
import Whine.Runner.Config (readConfig)
18+
import Whine.Runner.Glob as Glob
1719
import Whine.Types (RuleFactories, Violations, mapViolation)
1820

1921
startLanguageServer ::
2022
{ factories :: RuleFactories (WriterT (Violations ()) RunnerM)
2123
, configFile :: FilePath
24+
, checkWhen :: CheckFileWhen
2225
}
2326
-> RunnerM Unit
24-
startLanguageServer { factories, configFile } = do
27+
startLanguageServer { factories, configFile, checkWhen } = do
2528
conn <- Conn.createConnection
2629

2730
conn # on Conn.initialize \{} ->
2831
pure { capabilities: { textDocumentSync: textDocumentSyncKind.incremental } }
2932

3033
-- TODO: report config errors
3134
-- TODO: reload config on change
32-
ruleSet /\ _configErrors <-
35+
config /\ _configErrors <-
3336
readConfig factories configFile
3437
# mapViolation (merge { muted: false })
3538
# runWriterT
3639

37-
onDidSave <- unliftRunnerM \document -> do
40+
checkDocument <- unliftRunnerM \document -> do
3841
let uri = Doc.uri document
3942
path = uri # String.stripPrefix (Pattern "file://") <#> NodePath.relative (NodePath.dirname configFile) # fromMaybe ""
4043

41-
logDebug $ "Checking file: URI=" <> Doc.uri document <> ", path=" <> path
44+
if Glob.test config.files path then do
45+
logDebug $ "Checking file: URI=" <> Doc.uri document <> ", path=" <> path
4246

43-
text <- Doc.getText document
44-
violations <- execWriterT $ checkModule ruleSet { path, text }
45-
let liveViolations = violations # filter (not _.muted)
47+
text <- Doc.getText document
48+
violations <- execWriterT $ checkModule config.rules { path, text }
49+
let liveViolations = violations # filter (not _.muted)
4650

47-
logDebug $ fold
48-
[ "Found violations in ", path, ": "
49-
, show (length liveViolations), " live, "
50-
, show (length violations - length liveViolations), " muted"
51-
]
51+
logDebug $ fold
52+
[ "Found violations in ", path, ": "
53+
, show (length liveViolations), " live, "
54+
, show (length violations - length liveViolations), " muted"
55+
]
5256

53-
unless (null liveViolations) do
54-
conn # Conn.sendDiagnostics
55-
{ uri
56-
, diagnostics: liveViolations <#> \v -> cast
57-
{ range: fromMaybe firstLineRange $ v.source <#> \r -> { start: vsCodePos r.start, end: vsCodePos r.end }
58-
, code: v.rule
59-
, source: "purescript-whine"
60-
, message: v.message
61-
, severity: diagnosticSeverity.information
62-
}
63-
}
57+
unless (null liveViolations) do
58+
conn # Conn.sendDiagnostics
59+
{ uri
60+
, diagnostics: liveViolations <#> \v -> cast
61+
{ range: fromMaybe firstLineRange $ v.source <#> \r -> { start: vsCodePos r.start, end: vsCodePos r.end }
62+
, code: v.rule
63+
, source: "purescript-whine"
64+
, message: v.message
65+
, severity: diagnosticSeverity.information
66+
}
67+
}
68+
else
69+
logDebug $ "Skipping " <> path <> " because it doesn't match globs in the config"
6470

6571
docs <- Doc.create
66-
docs # on Doc.didSave \{ document } -> launchAff_ $ onDidSave document
67-
docs # on Doc.didOpen \{ document } -> launchAff_ $ onDidSave document
72+
docs # on Doc.didOpen \{ document } -> launchAff_ $ checkDocument document
73+
docs # on checkEvent \{ document } -> launchAff_ $ checkDocument document
6874

6975
Doc.listen docs conn
7076
Conn.listen conn
@@ -74,3 +80,7 @@ startLanguageServer { factories, configFile } = do
7480
where
7581
firstLineRange = { start: { line: 0, character: 0 }, end: { line: 1, character: 0 } }
7682
vsCodePos p = { line: p.line, character: p.column }
83+
84+
checkEvent = case checkWhen of
85+
CheckOnSave -> Doc.didSave
86+
CheckOnChange -> Doc.didChangeContent

0 commit comments

Comments
 (0)