├── input ├── Welcome.hs ├── after │ ├── OK.hs │ ├── TopLevelSignatureProvider.hs │ ├── ImportProviderConstructor.hs │ ├── TypeHoleProvider.hs │ ├── QualifiedImportProvider.hs │ ├── TypeWildcardProvider.hs │ ├── ExtensionProvider.hs │ ├── UnusedImportProvider.hs │ ├── OrganizeExtensionProvider.hs │ ├── ImportProvider.hs │ └── OrganizeImportProvider.hs └── before │ ├── OK.hs │ ├── TopLevelSignatureProvider.hs │ ├── ImportProviderConstructor.hs │ ├── TypeHoleProvider.hs │ ├── QualifiedImportProvider.hs │ ├── ExtensionProvider.hs │ ├── TypeWildcardProvider.hs │ ├── OrganizeExtensionProvider.hs │ ├── UnusedImportProvider.hs │ ├── ImportProvider.hs │ └── OrganizeImportProvider.hs ├── .gitignore ├── images ├── AddImport_sm.gif ├── Haskutil-logo.png ├── AddExtension_sm.gif ├── AddSignature_sm.gif ├── SortImports_sm.gif ├── FillTypedHole_sm.gif ├── OrganizeImports_sm.gif ├── OrganizeExtensions_sm.gif ├── RemoveUnusedImports_sm.gif └── Haskutil-logo.svg ├── bin └── vscode-ghc-simple-0.1.23.vsix ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── .vscodeignore ├── tslint.json ├── .github └── workflows │ ├── branch.yml │ └── master.yml ├── tsconfig.json ├── src ├── test │ ├── index.ts │ ├── runTest.ts │ ├── providers.test.ts │ └── utils.ts ├── features │ ├── extensionProvider │ │ └── extensionDeclaration.ts │ ├── topLevelSignatureProvider.ts │ ├── typedHoleProvider.ts │ ├── importProvider.ts │ ├── qualifiedImportProvider.ts │ ├── extensionProvider.ts │ ├── typeWildcardProvider.ts │ ├── removeUnusedImportProvider.ts │ ├── importProvider │ │ ├── importProviderBase.ts │ │ └── importDeclaration.ts │ ├── organizeImportProvider.ts │ └── organizeExtensionProvider.ts ├── extension.ts └── configuration.ts ├── LICENSE ├── README.md ├── package.json └── CHANGELOG.md /input/Welcome.hs: -------------------------------------------------------------------------------- 1 | main = putStrLn "Hello World" 2 | -------------------------------------------------------------------------------- /input/after/OK.hs: -------------------------------------------------------------------------------- 1 | 2 | main :: IO () 3 | main = 4 | return () 5 | -------------------------------------------------------------------------------- /input/before/OK.hs: -------------------------------------------------------------------------------- 1 | 2 | main :: IO () 3 | main = 4 | return () 5 | -------------------------------------------------------------------------------- /input/before/TopLevelSignatureProvider.hs: -------------------------------------------------------------------------------- 1 | 2 | main = 3 | return () 4 | -------------------------------------------------------------------------------- /input/before/ImportProviderConstructor.hs: -------------------------------------------------------------------------------- 1 | 2 | proxy :: Proxy Bool 3 | proxy = Proxy 4 | -------------------------------------------------------------------------------- /input/after/TopLevelSignatureProvider.hs: -------------------------------------------------------------------------------- 1 | 2 | main :: IO () 3 | main = 4 | return () 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .nyc_output 3 | .vscode-test 4 | node_modules 5 | out 6 | /*.vsix 7 | -------------------------------------------------------------------------------- /images/AddImport_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/AddImport_sm.gif -------------------------------------------------------------------------------- /images/Haskutil-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/Haskutil-logo.png -------------------------------------------------------------------------------- /images/AddExtension_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/AddExtension_sm.gif -------------------------------------------------------------------------------- /images/AddSignature_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/AddSignature_sm.gif -------------------------------------------------------------------------------- /images/SortImports_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/SortImports_sm.gif -------------------------------------------------------------------------------- /input/before/TypeHoleProvider.hs: -------------------------------------------------------------------------------- 1 | 2 | foo :: Bool 3 | foo = _ 4 | 5 | bar :: [a] -> [a] 6 | bar xs = 7 | _ xs 8 | -------------------------------------------------------------------------------- /images/FillTypedHole_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/FillTypedHole_sm.gif -------------------------------------------------------------------------------- /images/OrganizeImports_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/OrganizeImports_sm.gif -------------------------------------------------------------------------------- /input/after/ImportProviderConstructor.hs: -------------------------------------------------------------------------------- 1 | import Data.Proxy (Proxy(..)) 2 | 3 | proxy :: Proxy Bool 4 | proxy = Proxy 5 | -------------------------------------------------------------------------------- /input/after/TypeHoleProvider.hs: -------------------------------------------------------------------------------- 1 | 2 | foo :: Bool 3 | foo = True 4 | 5 | bar :: [a] -> [a] 6 | bar xs = 7 | init xs 8 | -------------------------------------------------------------------------------- /bin/vscode-ghc-simple-0.1.23.vsix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/bin/vscode-ghc-simple-0.1.23.vsix -------------------------------------------------------------------------------- /images/OrganizeExtensions_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/OrganizeExtensions_sm.gif -------------------------------------------------------------------------------- /images/RemoveUnusedImports_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EduardSergeev/vscode-haskutil/HEAD/images/RemoveUnusedImports_sm.gif -------------------------------------------------------------------------------- /input/before/QualifiedImportProvider.hs: -------------------------------------------------------------------------------- 1 | import Data.Word 2 | 3 | foo xs = 4 | BS.pack xs 5 | 6 | bar i = 7 | Numeric.showInt i 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "eg2.tslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /input/before/ExtensionProvider.hs: -------------------------------------------------------------------------------- 1 | 2 | import Data.Kind (Type) 3 | 4 | data Nat = Ze | Su Nat 5 | 6 | data Vec :: Type -> Nat -> Type where 7 | Nil :: Vec a Ze 8 | Cons :: a -> Vec a n -> Vec a (Su n) 9 | -------------------------------------------------------------------------------- /input/before/TypeWildcardProvider.hs: -------------------------------------------------------------------------------- 1 | 2 | f0 :: (Num a, _) => a -> a 3 | f0 x = x + x 4 | 5 | f3 :: (Num a, _) => a -> Bool 6 | f3 x = read (show $ x + x) == x 7 | 8 | fe :: _ => a -> a 9 | fe x = x 10 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .github 3 | .gitignore 4 | .nyc_output 5 | .vscode 6 | images 7 | !images/Haskutil-logo.png 8 | input 9 | out/test 10 | out/**/*.map 11 | src 12 | tsconfig.json 13 | tslint.json 14 | -------------------------------------------------------------------------------- /input/after/QualifiedImportProvider.hs: -------------------------------------------------------------------------------- 1 | import qualified Data.ByteString as BS 2 | import Data.Word 3 | import qualified Numeric 4 | 5 | foo xs = 6 | BS.pack xs 7 | 8 | bar i = 9 | Numeric.showInt i 10 | -------------------------------------------------------------------------------- /input/after/TypeWildcardProvider.hs: -------------------------------------------------------------------------------- 1 | 2 | f0 :: (Num a) => a -> a 3 | f0 x = x + x 4 | 5 | f3 :: (Num a, Eq a, Read a, Show a) => a -> Bool 6 | f3 x = read (show $ x + x) == x 7 | 8 | fe :: a -> a 9 | fe x = x 10 | -------------------------------------------------------------------------------- /input/after/ExtensionProvider.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | 3 | import Data.Kind (Type) 4 | 5 | data Nat = Ze | Su Nat 6 | 7 | data Vec :: Type -> Nat -> Type where 8 | Nil :: Vec a Ze 9 | Cons :: a -> Vec a n -> Vec a (Su n) 10 | -------------------------------------------------------------------------------- /input/after/UnusedImportProvider.hs: -------------------------------------------------------------------------------- 1 | import Data.List (sort) 2 | import Data.Monoid (Product(getProduct), Sum(..)) 3 | 4 | foo :: Ord a => [a] -> [a] 5 | foo xs = 6 | sort xs 7 | 8 | sum :: Sum a -> a 9 | sum = getSum 10 | 11 | product :: Product a -> a 12 | product = getProduct 13 | -------------------------------------------------------------------------------- /input/before/OrganizeExtensionProvider.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module description 3 | -} 4 | 5 | {-# LANGUAGE MultiParamTypeClasses, 6 | FlexibleContexts, FlexibleInstances #-} 7 | {-# LANGUAGE DeriveFunctor #-} 8 | 9 | module Main where 10 | 11 | 12 | main :: IO () 13 | main = 14 | return () 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } 16 | -------------------------------------------------------------------------------- /input/after/OrganizeExtensionProvider.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module description 3 | -} 4 | 5 | {-# LANGUAGE DeriveFunctor #-} 6 | {-# LANGUAGE FlexibleContexts #-} 7 | {-# LANGUAGE FlexibleInstances #-} 8 | {-# LANGUAGE MultiParamTypeClasses #-} 9 | 10 | module Main where 11 | 12 | 13 | main :: IO () 14 | main = 15 | return () 16 | -------------------------------------------------------------------------------- /input/before/UnusedImportProvider.hs: -------------------------------------------------------------------------------- 1 | import Data.List (sort, tails) 2 | import Data.Maybe 3 | import Data.Monoid (All (..), Any(getAny), Product(getProduct), Sum(..), Any(..), First(..)) 4 | 5 | foo :: Ord a => [a] -> [a] 6 | foo xs = 7 | sort xs 8 | 9 | sum :: Sum a -> a 10 | sum = getSum 11 | 12 | product :: Product a -> a 13 | product = getProduct 14 | -------------------------------------------------------------------------------- /input/before/ImportProvider.hs: -------------------------------------------------------------------------------- 1 | import Data.Char ( ) 2 | 3 | foo :: Ord a => [a] -> Maybe [a] 4 | foo xs = 5 | listToMaybe . tails. sort $ xs 6 | 7 | bar :: (a -> b) -> (b -> c) -> (a -> c) 8 | bar f g = 9 | f >>> g 10 | 11 | escaped :: Int 12 | escaped = 13 | foldl' (+) 0 [1..10] 14 | 15 | dot :: Int 16 | dot = 17 | 42 .&. 1 18 | 19 | existing :: Char -> Bool 20 | existing = 21 | isDigit 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/out": true, 4 | "**/node_modules": true, 5 | "**/.vscode-test/": true, 6 | "**/*.vsix": true, 7 | "**/.vscode-test": true, 8 | "**/.nyc_output": true, 9 | "**/.coverage": true, 10 | "*.vsix": true 11 | }, 12 | "markdownlint.config": { 13 | "MD022": false, 14 | "MD024": false, 15 | "MD032": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /input/after/ImportProvider.hs: -------------------------------------------------------------------------------- 1 | import Control.Arrow ((>>>)) 2 | import Data.Bits ((.&.)) 3 | import Data.Char ( isDigit ) 4 | import Data.List (foldl', sort, tails) 5 | import Data.Maybe 6 | 7 | foo :: Ord a => [a] -> Maybe [a] 8 | foo xs = 9 | listToMaybe . tails. sort $ xs 10 | 11 | bar :: (a -> b) -> (b -> c) -> (a -> c) 12 | bar f g = 13 | f >>> g 14 | 15 | escaped :: Int 16 | escaped = 17 | foldl' (+) 0 [1..10] 18 | 19 | dot :: Int 20 | dot = 21 | 42 .&. 1 22 | 23 | existing :: Char -> Bool 24 | existing = 25 | isDigit 26 | -------------------------------------------------------------------------------- /input/before/OrganizeImportProvider.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | 3 | 4 | module Main where 5 | 6 | -- Some comment 7 | import System.IO 8 | import Data.Maybe ( 9 | listToMaybe, 10 | Maybe 11 | ) 12 | import Data.List 13 | import qualified Data.ByteString.Lazy as M 14 | import Prelude ((.), Ord, ($)) 15 | import Control.Monad 16 | 17 | 18 | foo :: Ord a => [a] -> Maybe a 19 | foo xs = 20 | listToMaybe . sort $ xs 21 | 22 | bar :: M.ByteString 23 | bar = 24 | M.empty 25 | 26 | main :: IO () 27 | main = 28 | return () 29 | -------------------------------------------------------------------------------- /input/after/OrganizeImportProvider.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | 3 | 4 | module Main where 5 | 6 | -- Some comment 7 | import Control.Monad 8 | import qualified Data.ByteString.Lazy as M 9 | import Data.List 10 | import Data.Maybe ( 11 | Maybe, 12 | listToMaybe 13 | ) 14 | import Prelude (($), (.), Ord) 15 | import System.IO 16 | 17 | 18 | foo :: Ord a => [a] -> Maybe a 19 | foo xs = 20 | listToMaybe . sort $ xs 21 | 22 | bar :: M.ByteString 23 | bar = 24 | M.empty 25 | 26 | main :: IO () 27 | main = 28 | return () 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": "build" 13 | }, 14 | { 15 | "type": "npm", 16 | "script": "compile", 17 | "problemMatcher": "$tsc", 18 | "presentation": { 19 | "reveal": "never" 20 | }, 21 | "runOptions": { 22 | "runOn": "folderOpen" 23 | }, 24 | "group": { 25 | "kind": "build", 26 | "isDefault": true 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/branch.yml: -------------------------------------------------------------------------------- 1 | name: branch 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | env: { CODE_VERSION: 'stable', DISPLAY: ':99.0' } 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up npm 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | 18 | - name: Set up GHC environment 19 | run: stack setup 20 | 21 | - run: npm install 22 | 23 | - name: Run npm test 24 | uses: coactions/setup-xvfb@v1 25 | with: 26 | run: npm test 27 | 28 | - name: Add GHC extension output (on failure) 29 | if: failure() 30 | run: find .vscode-test/udd/logs -name *GHC* -exec cat {} \; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es2019" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | /* Strict Type-Checking Option */ 12 | "strict": false, /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | "noUnusedLocals": false /* Report errors on unused locals. */ 15 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 16 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 17 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | ".vscode-test" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | 6 | export function run(): Promise { 7 | // Create the mocha test 8 | const mocha = new Mocha({ 9 | ui: 'tdd', 10 | timeout: 30000, 11 | color: true 12 | }); 13 | 14 | const testsRoot = __dirname; 15 | 16 | return new Promise((c, e) => { 17 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 18 | if (err) { 19 | return e(err); 20 | } 21 | 22 | // Add files to the test suite 23 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 24 | 25 | try { 26 | // Run the mocha test 27 | mocha.run(failures => { 28 | if (failures > 0) { 29 | e(new Error(`${failures} tests failed.`)); 30 | } else { 31 | c(); 32 | } 33 | }); 34 | } catch (err) { 35 | console.error(err); 36 | e(err); 37 | } 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outFiles": [ 16 | "${workspaceRoot}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: compile" 19 | }, 20 | { 21 | "name": "Launch Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceRoot}", 27 | "--extensionTestsPath=${workspaceRoot}/out/test" 28 | ], 29 | "stopOnEntry": false, 30 | "sourceMaps": true, 31 | "outFiles": [ 32 | "${workspaceRoot}/out/**/*.js" 33 | ], 34 | "preLaunchTask": "npm: compile" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Eduard Sergeev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/features/extensionProvider/extensionDeclaration.ts: -------------------------------------------------------------------------------- 1 | import { Range, TextDocument } from "vscode"; 2 | 3 | export default class ExtensionDeclaration { 4 | private _extensions?: string[] = []; 5 | private _header: string = ""; 6 | 7 | constructor(header: string, extensions: string, public offset?: number, public length?: number) { 8 | this.extensions = extensions; 9 | this._header = header; 10 | } 11 | 12 | public get extensions() { 13 | return this._extensions.join(','); 14 | } 15 | 16 | public set extensions(extensionsString: string) { 17 | this._extensions = extensionsString ? extensionsString.split(',') : []; 18 | } 19 | 20 | public get header() { 21 | return this._header; 22 | } 23 | 24 | public get extensionNames() { 25 | return this._extensions.map(e => e.trim()); 26 | } 27 | 28 | public get text() { 29 | return `{-# ${this.header} ${this.extensions}#-}`; 30 | } 31 | 32 | public getRange(document: TextDocument): Range { 33 | return new Range( 34 | document.positionAt(this.offset), 35 | document.positionAt(this.offset + this.length)); 36 | } 37 | 38 | public static get extensionRegex(): RegExp { 39 | return /^{-#\s+(LANGUAGE)\s+([^#]+)#-}/gmi; 40 | } 41 | 42 | public static getExtensions(text: string): ExtensionDeclaration[] { 43 | const imports = []; 44 | for (let match, regex = ExtensionDeclaration.extensionRegex; match = regex.exec(text);) { 45 | imports.push(new ExtensionDeclaration(match[1], match[2], match.index, match[0].length)); 46 | } 47 | return imports; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/features/topLevelSignatureProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CodeActionProvider, Disposable, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, WorkspaceEdit, CodeActionKind } from 'vscode'; 4 | import * as vscode from 'vscode'; 5 | 6 | 7 | export default class TopLevelSignatureProvider implements CodeActionProvider { 8 | private static commandId: string = 'haskell.addTopLevelSignature'; 9 | 10 | public activate(subscriptions: Disposable[]) { 11 | const command = vscode.commands.registerCommand(TopLevelSignatureProvider.commandId, this.runCodeAction, this); 12 | subscriptions.push(command); 13 | } 14 | 15 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 16 | const pattern = /Top-level binding with no type signature:\s+([^]+)/; 17 | const codeActions = []; 18 | for (const diagnostic of context.diagnostics) { 19 | const match = pattern.exec(diagnostic.message); 20 | if (match === null) { 21 | continue; 22 | } 23 | 24 | const signature = match[1].trim(); 25 | const title = `Add: ${signature}`; 26 | const codeAction = new CodeAction(title, CodeActionKind.QuickFix); 27 | codeAction.command = { 28 | title: title, 29 | command: TopLevelSignatureProvider.commandId, 30 | arguments: [ 31 | document, 32 | signature, 33 | diagnostic.range 34 | ] 35 | }; 36 | codeAction.diagnostics = [diagnostic]; 37 | codeActions.push(codeAction); 38 | } 39 | return codeActions; 40 | } 41 | 42 | private runCodeAction(document: TextDocument, signature: string, range: Range): Thenable { 43 | const edit = new WorkspaceEdit(); 44 | edit.insert(document.uri, range.start, `${signature}\n`); 45 | return vscode.workspace.applyEdit(edit); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/features/typedHoleProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CodeActionProvider, Disposable, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, WorkspaceEdit, CodeActionKind } from 'vscode'; 4 | import * as vscode from 'vscode'; 5 | 6 | 7 | export default class TypedHoleProvider implements CodeActionProvider { 8 | private static commandId: string = 'haskell.fillTypeHoleSignature'; 9 | 10 | public activate(subscriptions: Disposable[]) { 11 | const command = vscode.commands.registerCommand(TypedHoleProvider.commandId, this.runCodeAction, this); 12 | subscriptions.push(command); 13 | } 14 | 15 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 16 | const errorPattern = / Found hole: ([^\s]+?) ::/; 17 | const fillPattern = /^\s+([^\s]+)\s::/gm; 18 | const codeActions = []; 19 | for (const diagnostic of context.diagnostics) { 20 | const match = errorPattern.exec(diagnostic.message); 21 | if (match === null) { 22 | continue; 23 | } 24 | const hole = match[1]; 25 | 26 | for (let fillMatch; fillMatch = fillPattern.exec(diagnostic.message);) { 27 | const fill = fillMatch[1]; 28 | const title = `Fill \`${hole}' with: \`${fill}'`; 29 | const codeAction = new CodeAction(title, CodeActionKind.QuickFix); 30 | codeAction.command = { 31 | title: title, 32 | command: TypedHoleProvider.commandId, 33 | arguments: [ 34 | document, 35 | fill, 36 | diagnostic.range 37 | ] 38 | }; 39 | codeAction.diagnostics = [diagnostic]; 40 | codeActions.push(codeAction); 41 | } 42 | } 43 | return codeActions; 44 | } 45 | 46 | private runCodeAction(document: TextDocument, fill: string, range: Range): Thenable { 47 | const edit = new WorkspaceEdit(); 48 | edit.replace(document.uri, range, fill); 49 | return vscode.workspace.applyEdit(edit); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as path from 'path'; 3 | import { env } from 'process'; 4 | 5 | import { 6 | runTests, 7 | downloadAndUnzipVSCode, 8 | resolveCliPathFromVSCodeExecutablePath 9 | } from '@vscode/test-electron'; 10 | 11 | async function main(): Promise { 12 | try { 13 | // The folder containing the Extension Manifest package.json 14 | // Passed to `--extensionDevelopmentPath` 15 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 16 | 17 | // The path to the extension test runner script 18 | // Passed to --extensionTestsPath 19 | const extensionTestsPath = __dirname; 20 | const vscodeVersion = env['CODE_VERSION']; 21 | const vscodeExecutablePath = await downloadAndUnzipVSCode(vscodeVersion); 22 | const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath); 23 | 24 | // Install dependent extensions 25 | const dependencies = [ 26 | 'jcanero.hoogle-vscode', 27 | 'dramforever.vscode-ghc-simple', 28 | ]; 29 | 30 | const extensionsDir = path.resolve(path.dirname(cliPath), '..', 'extensions'); 31 | const userDataDir = path.resolve(extensionsDir, '..', '..', 'udd'); 32 | 33 | for(const extension of dependencies) { 34 | cp.spawnSync(cliPath, ['--extensions-dir', extensionsDir, '--user-data-dir', userDataDir, '--install-extension', extension], { 35 | shell: process.platform === 'win32' ? true : undefined, 36 | encoding: 'utf-8', 37 | stdio: 'inherit' 38 | }); 39 | } 40 | 41 | // This is required for Mocha tests to report non-zero exit code in case of test failure 42 | process.removeAllListeners('exit'); 43 | 44 | // Download VS Code, unzip it and run all integration tests 45 | return await runTests({ 46 | vscodeExecutablePath, 47 | extensionDevelopmentPath, 48 | extensionTestsPath, 49 | launchArgs: [ 50 | '--user-data-dir', userDataDir, 51 | '--extensions-dir', extensionsDir, 52 | '--new-window', 53 | '--disable-gpu', 54 | '--disable-updates', 55 | '--logExtensionHostCommunication', 56 | '--skip-getting-started', 57 | '--skip-welcome', 58 | '--skip-release-notes', 59 | '--disable-keytar', 60 | '--disable-restore-windows', 61 | '--disable-telemetry', 62 | '--disable-workspace-trust', 63 | '--wait', 64 | ] 65 | }); 66 | } catch (err) { 67 | console.error(err); 68 | console.error('Failed to run tests'); 69 | process.exit(1); 70 | } 71 | } 72 | 73 | main(); 74 | -------------------------------------------------------------------------------- /src/features/importProvider.ts: -------------------------------------------------------------------------------- 1 | import { CodeActionProvider, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, CodeActionKind, DiagnosticSeverity, Diagnostic } from 'vscode'; 2 | import ImportProviderBase, { SearchResult } from './importProvider/importProviderBase'; 3 | 4 | 5 | export default class ImportProvider extends ImportProviderBase implements CodeActionProvider { 6 | constructor() { 7 | super('haskell.addImport'); 8 | } 9 | 10 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 11 | const patterns = [ 12 | /Variable not in scope:\s+(\S+)/, 13 | /Not in scope: type constructor or class [`‘](\S+)['’]/ 14 | ]; 15 | const codeActions = await Promise.all(context.diagnostics 16 | .filter(d => d.severity === DiagnosticSeverity.Error) 17 | .flatMap(diagnostic => 18 | patterns.map(pattern => pattern.exec(diagnostic.message)) 19 | .filter(match => match && !/\w\./.test(match[1])) 20 | .flatMap(async ([, name]) => 21 | this.addImportForVariable(document, diagnostic, name, await this.search(name)) 22 | ) 23 | ) 24 | ); 25 | return codeActions.flat(); 26 | } 27 | 28 | private addImportForVariable(document: TextDocument, diagnostic: Diagnostic, variableName: string, searchResults: SearchResult[]): CodeAction[] { 29 | const codeActions = new Map(); 30 | for (const result of searchResults) { 31 | let title = `Add: "import ${result.module}"`; 32 | let codeAction = new CodeAction(title, CodeActionKind.QuickFix); 33 | codeAction.command = { 34 | title: title, 35 | command: this.commandId, 36 | arguments: [ 37 | document, 38 | result.module, 39 | ], 40 | }; 41 | codeAction.diagnostics = [diagnostic]; 42 | codeActions.set(title, codeAction); 43 | 44 | const element = variableName[0] === variableName[0].toLowerCase() ? variableName : `${variableName}(..)`; 45 | title = `Add: "import ${result.module} (${element})"`; 46 | codeAction = new CodeAction(title, CodeActionKind.QuickFix); 47 | codeAction.command = { 48 | title: title, 49 | command: this.commandId, 50 | arguments: [ 51 | document, 52 | result.module, 53 | { 54 | elementName: element 55 | } 56 | ] 57 | }; 58 | codeAction.diagnostics = [diagnostic]; 59 | codeActions.set(title, codeAction); 60 | } 61 | return [...codeActions.values()]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/features/qualifiedImportProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CodeActionProvider, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, CodeActionKind } from 'vscode'; 4 | import * as vscode from 'vscode'; 5 | import ImportProviderBase, { SearchResult } from './importProvider/importProviderBase'; 6 | 7 | 8 | export default class QualifiedImportProvider extends ImportProviderBase implements CodeActionProvider { 9 | constructor() { 10 | super('haskell.addQualifiedImport'); 11 | } 12 | 13 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 14 | const patterns = [ 15 | /Not in scope:[^`]*[`‘]([^.]+)\.([^'’]+)['’]/, 16 | /Variable not in scope:\s+(?:(\S+)\.)?(\S+)/, 17 | ]; 18 | let codeActions = []; 19 | const diagnostics = context.diagnostics.filter(d => d.severity === vscode.DiagnosticSeverity.Error); 20 | for (let diagnostic of diagnostics) { 21 | for (const pattern of patterns) { 22 | const match = pattern.exec(diagnostic.message); 23 | if (match === null) { 24 | continue; 25 | } 26 | 27 | let [, alias, name] = match; 28 | 29 | if (!alias) { 30 | const expressionMatch = /(\S+)\.(\S+)/.exec(document.getText(diagnostic.range)); 31 | if (expressionMatch) { 32 | alias = expressionMatch[1]; 33 | } else { 34 | continue; 35 | } 36 | } 37 | 38 | const results = await this.search(name); 39 | codeActions = codeActions.concat(this.addImportForVariable(document, alias, results)); 40 | codeActions.forEach(action => { 41 | action.diagnostics = [diagnostic]; 42 | }); 43 | } 44 | } 45 | return codeActions; 46 | } 47 | 48 | private addImportForVariable(document: TextDocument, alias: string, searchResults: SearchResult[]): CodeAction[] { 49 | const codeActions = new Map(); 50 | for (const result of searchResults) { 51 | const title = `Add: "import qualified ${result.module}${result.module !== alias ? ` as ${alias}` : ''}"`; 52 | const codeAction = new CodeAction(title, CodeActionKind.QuickFix); 53 | codeAction.command = { 54 | title: title, 55 | command: this.commandId, 56 | arguments: [ 57 | document, 58 | result.module, 59 | { 60 | qualified: true, 61 | alias: result.module !== alias ? ` as ${alias}` : null 62 | } 63 | ] 64 | }; 65 | codeActions.set(title, codeAction); 66 | } 67 | return [...codeActions.values()]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/features/extensionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { CodeActionProvider, Disposable, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, WorkspaceEdit, CodeActionKind } from 'vscode'; 3 | import OrganizeExtensionProvider from './organizeExtensionProvider'; 4 | import Configuration from '../configuration'; 5 | 6 | 7 | export default class ExtensionProvider implements CodeActionProvider { 8 | private static commandId: string = 'haskell.addExtension'; 9 | 10 | public static get extensionPattern() { 11 | return /^{-#\s+LANGUAGE\s+([^#]+)#-}/gm; 12 | } 13 | 14 | public activate(subscriptions: Disposable[]) { 15 | const command = vscode.commands.registerCommand(ExtensionProvider.commandId, this.runCodeAction, this); 16 | subscriptions.push(command); 17 | } 18 | 19 | public provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): CodeAction[] { 20 | const codeActions = []; 21 | for (const diagnostic of context.diagnostics) { 22 | for (const extension of Configuration.supportedExtensions) { 23 | if (!diagnostic.message.includes(extension)) { 24 | continue; 25 | } 26 | 27 | const line = `{-# LANGUAGE ${extension} #-}`; 28 | const title = `Add: ${line}`; 29 | if (codeActions.findIndex(a => a.title === title) !== -1) { 30 | continue; 31 | } 32 | 33 | const codeAction = new CodeAction(title, CodeActionKind.QuickFix); 34 | codeAction.command = { 35 | title: title, 36 | command: ExtensionProvider.commandId, 37 | arguments: [ 38 | document, 39 | extension, 40 | line 41 | ] 42 | }; 43 | codeAction.diagnostics = [diagnostic]; 44 | codeActions.push(codeAction); 45 | } 46 | } 47 | return codeActions; 48 | } 49 | 50 | private async runCodeAction(document: TextDocument, newExtension: string, extensionLine: string) { 51 | function afterMatch(offset) { 52 | const position = document.positionAt(offset); 53 | return document.offsetAt(position.with(position.line + 1, 0)); 54 | } 55 | 56 | const text = document.getText(); 57 | let position = 0; 58 | 59 | for (let match, pattern = ExtensionProvider.extensionPattern; match = pattern.exec(text);) { 60 | const oldExtension = match[1]; 61 | if (oldExtension > newExtension) { 62 | position = match.index; 63 | break; 64 | } 65 | position = afterMatch(match.index + match[0].length); 66 | } 67 | 68 | const edit = new WorkspaceEdit(); 69 | edit.insert(document.uri, document.positionAt(position), extensionLine + "\n"); 70 | await vscode.workspace.applyEdit(edit); 71 | 72 | if (Configuration.shouldOrganiseExtensionOnInsert) { 73 | await vscode.commands.executeCommand(OrganizeExtensionProvider.commandId, document); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import Configuration from './configuration'; 3 | import ImportProvider from './features/importProvider'; 4 | import QualifiedImportProvider from './features/qualifiedImportProvider'; 5 | import OrganizeImportProvider from './features/organizeImportProvider'; 6 | import ExtensionProvider from './features/extensionProvider'; 7 | import OrganizeExtensionProvider from './features/organizeExtensionProvider'; 8 | import TopLevelSignatureProvider from './features/topLevelSignatureProvider'; 9 | import TypedHoleProvider from './features/typedHoleProvider'; 10 | import TypeWildcardProvider from './features/typeWildcardProvider'; 11 | import RemoveUnusedImportProvider from './features/removeUnusedImportProvider'; 12 | 13 | 14 | export function activate(context: vscode.ExtensionContext) { 15 | const features = { 16 | addImport: new ImportProvider(), 17 | addQualifiedImport: new QualifiedImportProvider(), 18 | organizeImports: new OrganizeImportProvider(), 19 | addExtension: new ExtensionProvider(), 20 | organizeExtensions: new OrganizeExtensionProvider(), 21 | addSignature: new TopLevelSignatureProvider(), 22 | fillTypeHole: new TypedHoleProvider(), 23 | fillTypeWildcard: new TypeWildcardProvider(), 24 | removeUnusedImport: new RemoveUnusedImportProvider(), 25 | }; 26 | 27 | if (Configuration.checkDiagnosticsExtension) { 28 | checkDependencies(); 29 | } 30 | 31 | for (const feature in features) { 32 | if (Configuration.root.feature[feature]) { 33 | const provider = features[feature]; 34 | provider.activate(context.subscriptions); 35 | vscode.languages.registerCodeActionsProvider('haskell', provider); 36 | } 37 | } 38 | } 39 | 40 | function checkDependencies() { 41 | const dependencies = Configuration.supportedDependencies; 42 | if(!dependencies.find(extension => vscode.extensions.getExtension(extension.id))) { 43 | const toLink = ({id, name}) => `[${name}](${vscode.Uri.parse(`command:workbench.extensions.search?["@id:${id}"]`)})`; 44 | const items = dependencies.map(toLink); 45 | const warningSetting = `${Configuration.rootSection}.${Configuration.checkDiagnosticsExtensionSection}`; 46 | const warningLink = `[${Configuration.checkDiagnosticsExtensionSection}](${vscode.Uri.parse(`command:workbench.action.openSettings?["${warningSetting}"]`)})`; 47 | const listSetting = `${Configuration.rootSection}.${Configuration.supportedDependenciesSection}`; 48 | const listLink = `[${Configuration.supportedDependenciesSection}](${vscode.Uri.parse(`command:workbench.action.openSettings?["${listSetting}"]`)})`; 49 | vscode.window.showWarningMessage(`No supported Haskell diagnostics extension was found. 50 | To get QuickFix provided by ${toLink({ id: 'edka.haskutil', name: 'Haskutil' })} 51 | install one of the recommended extension: ${items.slice(0, -1).join(', ')} or ${items.pop()}. 52 | Otherwise to supress this warning either add any 3rd party Haskell diagnostics extension to ${listLink} or disable it in ${warningLink}.`); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/features/typeWildcardProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CodeActionProvider, Disposable, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, WorkspaceEdit, CodeActionKind } from 'vscode'; 4 | import * as vscode from 'vscode'; 5 | 6 | 7 | export default class TypeWildcardProvider implements CodeActionProvider { 8 | private static commandId: string = 'haskell.fillTypeWildcard'; 9 | 10 | public activate(subscriptions: Disposable[]) { 11 | const command = vscode.commands.registerCommand(TypeWildcardProvider.commandId, this.runCodeAction, this); 12 | subscriptions.push(command); 13 | } 14 | 15 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 16 | const errorPattern = /Found .* wildcard/; 17 | const widlcardPattern = /wildcard [`‘](.+?)['’]/; 18 | const fillPattern = /standing for\n?.*[`‘](.+?)['’]/; 19 | const codeActions = []; 20 | for (const diagnostic of context.diagnostics) { 21 | const match = errorPattern.exec(diagnostic.message); 22 | if (match === null) { 23 | continue; 24 | } 25 | 26 | const wildcardMatch = widlcardPattern.exec(diagnostic.message); 27 | let wildcard = wildcardMatch ? wildcardMatch[1] : '_'; 28 | 29 | let fillMatch = fillPattern.exec(diagnostic.message); 30 | if (fillMatch) { 31 | let fill = fillMatch[1]; 32 | let offset = document.offsetAt(diagnostic.range.end); 33 | while (document.getText(new Range(document.positionAt(offset), document.positionAt(offset + 1))).match(/\s/)) { 34 | offset++; 35 | } 36 | 37 | let next = document.getText(new Range(document.positionAt(offset), document.positionAt(offset + 2))); 38 | if (fill.indexOf(" -> ") > -1 && next === "->") { 39 | fill = `(${fill})`; 40 | } 41 | 42 | const line = document.lineAt(diagnostic.range.start); 43 | if(fill === "()" || fill == "() :: Constraint") { 44 | fill = ""; 45 | const wilcardMatch = line.text.match(/_\s*=>\s*/) || line.text.match(/,\s*_\s*/) || line.text.match(/\s*_\s*,/); 46 | if(wilcardMatch) { 47 | wildcard = wilcardMatch[0]; 48 | } 49 | } 50 | if(line.text.match(/\(.*_.*\)/) && fill.match(/^\(.*\)$/)) { 51 | fill = fill.slice(1, -1); 52 | } 53 | 54 | const title = `Replace \`${wildcard}' with: \`${fill}'`; 55 | const codeAction = new CodeAction(title, CodeActionKind.QuickFix); 56 | const wildcardPosition = document.positionAt(document.offsetAt(line.range.start) + line.text.indexOf(wildcard)); 57 | const range = new Range(wildcardPosition, wildcardPosition.with({ character: wildcardPosition.character + wildcard.length})); 58 | codeAction.command = { 59 | title: title, 60 | command: TypeWildcardProvider.commandId, 61 | arguments: [ 62 | document, 63 | fill, 64 | range 65 | ] 66 | }; 67 | codeAction.diagnostics = [diagnostic]; 68 | codeActions.push(codeAction); 69 | } 70 | } 71 | return codeActions; 72 | } 73 | 74 | private runCodeAction(document: TextDocument, fill: string, range: Range): Thenable { 75 | const edit = new WorkspaceEdit(); 76 | edit.replace(document.uri, range, fill); 77 | return vscode.workspace.applyEdit(edit); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/providers.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { didChangeDiagnostics, runQuickfixTest } from './utils'; 3 | import { DiagnosticSeverity } from 'vscode'; 4 | import path = require('path'); 5 | 6 | const configs = { 7 | 'telemetry.enableTelemetry': false, 8 | 'ghcSimple.replCommand': 'stack exec ghci', 9 | 'ghcSimple.replScope': 'file', 10 | }; 11 | 12 | suite('', () => { 13 | suiteSetup(async () => { 14 | const config = vscode.workspace.getConfiguration(); 15 | for (const setting in configs) { 16 | await config.update(setting, configs[setting], true); 17 | } 18 | 19 | // Temporary hack to fix intermittent (but quite persistent) test failures 20 | const welcome = path.join(__dirname, '../../input/Welcome.hs'); 21 | const doc = await didChangeDiagnostics(welcome, [DiagnosticSeverity.Warning, 1], async () => { 22 | const doc = await vscode.workspace.openTextDocument(welcome); 23 | await vscode.window.showTextDocument(doc); 24 | // Not sure why by VSCode aborts the subsequent `vscode.executeCodeActionProvider` command 25 | // with `Cancelled` error if we do not give VSCode or our extensions some time to initialise 26 | // Could not find a proper event to wait on so we have to `sleep` for 3s instead 27 | await require('util').promisify(setTimeout)(3000); 28 | return doc; 29 | }); 30 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 31 | }); 32 | 33 | test('Add missing import', () => { 34 | return runQuickfixTest('ImportProvider.hs', 7, 35 | 'Add: "import Control.Arrow ((>>>))"', 36 | 'Add: "import Data.Maybe"', 37 | 'Add: "import Data.List (tails)"', 38 | 'Add: "import Data.List (sort)"', 39 | 'Add: "import Data.List (foldl\')"', 40 | 'Add: "import Data.Bits ((.&.))"', 41 | 'Add: "import Data.Char (isDigit)"' 42 | ); 43 | }); 44 | 45 | test('Add missing import qualified', () => { 46 | return runQuickfixTest('QualifiedImportProvider.hs', [DiagnosticSeverity.Error, 2], 47 | 'Add: "import qualified Data.ByteString as BS"', 48 | 'Add: "import qualified Numeric"' 49 | ); 50 | }); 51 | 52 | test('Add missing constructor import', () => { 53 | return runQuickfixTest('ImportProviderConstructor.hs', [DiagnosticSeverity.Error, 1], 54 | 'Add: "import Data.Proxy (Proxy(..))"' 55 | ); 56 | }); 57 | 58 | test('Organize imports', () => { 59 | return runQuickfixTest('OrganizeImportProvider.hs', 0); 60 | }); 61 | 62 | test('Remove unused imports', () => { 63 | return runQuickfixTest('UnusedImportProvider.hs', 3); 64 | }); 65 | 66 | test('Add missing extension', () => { 67 | return runQuickfixTest('ExtensionProvider.hs', 2); 68 | }); 69 | 70 | test('Organize extensions', () => { 71 | return runQuickfixTest('OrganizeExtensionProvider.hs', 0); 72 | }); 73 | 74 | test('Replace wildcard with suggested type', () => { 75 | return runQuickfixTest('TypeWildcardProvider.hs', 3); 76 | }); 77 | 78 | test('Replace type hole with suggested type', () => { 79 | return runQuickfixTest('TypeHoleProvider.hs', 2, 80 | "Fill `_' with: `True'", 81 | "Fill `_' with: `init'" 82 | ); 83 | }); 84 | 85 | test('Add top-level signature', () => { 86 | return runQuickfixTest('TopLevelSignatureProvider.hs', 1); 87 | }); 88 | 89 | suiteTeardown(async () => { 90 | const config = vscode.workspace.getConfiguration(); 91 | for (const setting in configs) { 92 | await config.update(setting, undefined, true); 93 | } 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /images/Haskutil-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 40 | 43 | 44 | 48 | 55 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VSCode Haskutil 2 | [![Build Status](https://github.com/EduardSergeev/vscode-haskutil/workflows/master/badge.svg)](https://github.com/EduardSergeev/vscode-haskutil/actions?query=workflow%3Amaster+branch%3Amaster) 3 | [![Coverage Status](https://coveralls.io/repos/github/EduardSergeev/vscode-haskutil/badge.svg?branch=master)](https://coveralls.io/github/EduardSergeev/vscode-haskutil?branch=master) 4 | [![Haskutil on Visual Studio Marketplace](https://img.shields.io/vscode-marketplace/v/edka.haskutil.svg)](https://marketplace.visualstudio.com/items?itemName=Edka.haskutil) 5 | [![Installs](https://img.shields.io/visual-studio-marketplace/i/Edka.haskutil)](https://marketplace.visualstudio.com/items?itemName=Edka.haskutil) 6 | 7 | 'QuickFix' actions for Haskell editor 8 | 9 | ## Requirements 10 | This extension uses diagnostics (errors and warnings) from `PROBLEMS` tab which is populated by other Haskell extensions such as [Simple GHC](https://marketplace.visualstudio.com/items?itemName=dramforever.vscode-ghc-simple), [Haskero](https://marketplace.visualstudio.com/items?itemName=Vans.haskero), [ghcid](https://marketplace.visualstudio.com/items?itemName=ndmitchell.haskell-ghcid) or [ghcide](https://marketplace.visualstudio.com/items?itemName=DigitalAssetHoldingsLLC.ghcide). Please install one of them along with this extension. 11 | 12 | ## Features 13 | * [Add missing import](#add-missing-import) 14 | * [Organize imports](#organize-imports) 15 | * [Remove unused imports](#remove-unused-imports) 16 | * [Add missing LANGUAGE extension](#add-missing-language-extension) 17 | * [Organize LANGUAGE extensions](#organize-language-extensions) 18 | * [Add top-level signature](#add-top-level-signature) 19 | * [Fill typed hole](#fill-typed-hole) 20 | 21 | Individual features can be turned on and off via `haskutil.feature.[feature]` flags 22 | 23 | ### Add missing import 24 | ![Add missing import](/images/AddImport_sm.gif "Add missing import") 25 | Uses [Hoogle](https://www.haskell.org/hoogle/) to search for matching modules. Configurable via [hoogle-vscode](https://marketplace.visualstudio.com/items?itemName=jcanero.hoogle-vscode) extension configuration (can be configured to use local instance of hoogle for example). 26 | 27 | ### Organize imports 28 | ![Organize imports](/images/OrganizeImports_sm.gif "Organize imports") 29 | Sorting and alignment are configurable via `haskutil.sortImports` and `haskutil.alignImports` respectively. Imports can also be organized on file save with `haskutil.organiseImportsOnSave: true` (dafault is `false`). 30 | 31 | ### Remove unused imports 32 | ![Remove unused imports](/images/RemoveUnusedImports_sm.gif "Remove unused imports") 33 | Removes unused imports identified by warnings. `haskutil.alignImports` controls if remaning imports are also aligned. 34 | 35 | ### Add missing LANGUAGE extension 36 | ![Add extension](/images/AddExtension_sm.gif "Add extension") 37 | Adds `LANGUAGE` pragma suggested by error. List of supported pragmas is defined by `haskutil.supportedExtensions`. 38 | 39 | ### Organize LANGUAGE extensions 40 | ![Organize extensions](/images/OrganizeExtensions_sm.gif "Organize extensions") 41 | Splitting, sorting and alignment of extensions are configurable via `haskutil.splitExtensions`, `haskutil.sortExtensions` and `haskutil.alignExtensions` respectively. Extensions can also be organized on file save with `haskutil.organiseExtensionOnSave: true` (dafault is `false`). 42 | 43 | ### Add top-level signature 44 | ![Add top-level signature](/images/AddSignature_sm.gif "Add top-level signature") 45 | Adds signature suggested by warning. 46 | 47 | ### Fill typed hole 48 | ![Fill typed hole](/images/FillTypedHole_sm.gif "Fill types hole") 49 | Replaces [typed hole](https://downloads.haskell.org/~ghc/8.6.4/docs/html/users_guide/glasgow_exts.html#typed-holes) with one of the `valid hole fits` from the warning. 50 | Similarly replaces [type wildcard](https://downloads.haskell.org/~ghc/8.6.4/docs/html/users_guide/glasgow_exts.html#type-wildcards) with inferred type. 51 | 52 | ## Dependencies 53 | 54 | * Automatic dependency (auto install) [hoogle-vscode](https://marketplace.visualstudio.com/items?itemName=jcanero.hoogle-vscode) 55 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '[0-9]+.[0-9]+.[0-9]+' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - ubuntu-latest 20 | - windows-latest 21 | - macos-latest 22 | ghc: 23 | - 8.10.7 24 | - 9.0.2 25 | - 9.4.5 26 | env: 27 | - { CODE_VERSION: 1.66.2, DISPLAY: ':99.0' } 28 | - { CODE_VERSION: 'stable', DISPLAY: ':99.0' } 29 | runs-on: ${{ matrix.os }} 30 | env: ${{ matrix.env }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Set up npm 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | 39 | - name: Install Haskell Stack 40 | if: runner.os == 'macOS' 41 | run: | 42 | brew install llvm@12 43 | echo "/opt/homebrew/opt/llvm@12/bin" >> $GITHUB_PATH 44 | brew install haskell-stack 45 | 46 | - name: Set up GHC ${{ matrix.ghc }} environment 47 | run: | 48 | echo "resolver: ghc-${{ matrix.ghc }}" > stack.yaml 49 | echo "packages: []" >> stack.yaml 50 | stack setup 51 | 52 | - run: npm install 53 | 54 | - name: Run npm test 55 | uses: coactions/setup-xvfb@v1 56 | with: 57 | run: npm test 58 | 59 | - name: Add GHC extension output (on failure on Linux or MacOS) 60 | if: failure() && runner.os != 'Windows' 61 | run: find .vscode-test/udd/logs -name *GHC* -exec cat {} \; 62 | 63 | - name: Add GHC extension output (on failure on Windows) 64 | if: failure() && runner.os == 'Windows' 65 | run: Get-ChildItem -Path .vscode-test -Include *GHC.log -File -Recurse | Get-Content 66 | 67 | metrics: 68 | runs-on: ubuntu-latest 69 | env: { CODE_VERSION: 'stable', DISPLAY: ':99.0', GHC: 9.4.5 } 70 | steps: 71 | - uses: actions/checkout@v4 72 | 73 | - name: Set up npm 74 | uses: actions/setup-node@v4 75 | with: 76 | node-version: 20 77 | 78 | - name: Set up GHC ${{ env.GHC }} environment 79 | run: | 80 | echo "resolver: ghc-${{ env.GHC }}" > stack.yaml 81 | echo "packages: []" >> stack.yaml 82 | stack setup 83 | 84 | - run: npm install 85 | 86 | - name: Run tests with coverage 87 | uses: coactions/setup-xvfb@v1 88 | with: 89 | run: npm run coverage 90 | 91 | - name: Publish coverage on Coveralls.io 92 | uses: coverallsapp/github-action@v2 93 | with: 94 | github-token: ${{ secrets.GITHUB_TOKEN }} 95 | path-to-lcov: .coverage/lcov.info 96 | 97 | release: 98 | needs: 99 | - build 100 | - metrics 101 | runs-on: ubuntu-latest 102 | env: 103 | RELEASE_BODY_FILE: ${{ github.workspace }}/release-description.md 104 | steps: 105 | - uses: actions/checkout@v4 106 | 107 | - name: Set up npm 108 | uses: actions/setup-node@v4 109 | with: 110 | node-version: 20 111 | 112 | - name: Build package 113 | run: | 114 | npm install 115 | npm run package 116 | 117 | - name: Create release notes 118 | run: | 119 | echo "RELEASE=$(git show -q --format=format:%s)" >> $GITHUB_ENV 120 | git show -q --format=format:%b > ${{ env.RELEASE_BODY_FILE }} 121 | 122 | - name: Create release 123 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 124 | uses: ncipollo/release-action@v1 125 | with: 126 | commit: ${{ github.sha }} 127 | name: ${{ env.RELEASE }} 128 | bodyFile: ${{ env.RELEASE_BODY_FILE }} 129 | artifacts: haskutil-*.vsix 130 | makeLatest: ${{ startsWith(github.event.base_ref, 'refs/heads/master') }} 131 | prerelease: ${{ !startsWith(github.event.base_ref, 'refs/heads/master') }} 132 | 133 | - name: Upload new version to MS Marketplace 134 | if: github.event_name == 'push' && startsWith(github.event.base_ref, 'refs/heads/master') 135 | run: npm run upload 136 | env: 137 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 138 | -------------------------------------------------------------------------------- /src/test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { Range, Position, CodeAction, TextDocument, Disposable, DiagnosticSeverity } from 'vscode'; 5 | import { assert } from 'chai'; 6 | import { promisify } from 'util'; 7 | 8 | export type DocFun = (doc: TextDocument) => Thenable; 9 | 10 | export function runQuickfixTest(file: string, diagsCount: number|[DiagnosticSeverity, number], ...titles: string[]): Promise { 11 | const [severety, count] = typeof diagsCount === 'number' ? [DiagnosticSeverity.Warning, diagsCount] : diagsCount; 12 | return withTestDocument(file, [severety, count], async doc => { 13 | const diagnostics = vscode.languages.getDiagnostics(doc.uri); 14 | const available = await getQuickFixes(doc); 15 | const applicable = available.filter(qf => !titles.length || titles.includes(qf.title)); 16 | assert.isNotEmpty(applicable, ` 17 | Could not find any applicable QuickFixes. 18 | Available: '${available.map(qf => qf.title).join(', ')}' 19 | Requested: '${titles.join(', ')}' 20 | Diagnostics: '${diagnostics.map(d => d.message).join('\n')}' 21 | `); 22 | await runQuickFixes(applicable); 23 | }); 24 | } 25 | 26 | export async function withTestDocument(file: string, diagnosticCount: [DiagnosticSeverity, number], test: DocFun, cleanup?: DocFun): Promise { 27 | const before = path.join(__dirname, '../../input/before/', file); 28 | const after = path.join(__dirname, '../../input/after', file); 29 | const doc = await didChangeDiagnostics(before, diagnosticCount, async () => { 30 | const doc = await vscode.workspace.openTextDocument(before); 31 | await vscode.window.showTextDocument(doc); 32 | return doc; 33 | }); 34 | try { 35 | await test(doc); 36 | const expected = await promisify(fs.readFile)(after, { encoding: 'utf8' }); 37 | assert.strictEqual(doc.getText(), expected); 38 | } finally { 39 | await cleanup?.(doc); 40 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 41 | } 42 | } 43 | 44 | export async function getQuickFixes(doc : TextDocument): Promise { 45 | return await vscode.commands.executeCommand( 46 | 'vscode.executeCodeActionProvider', 47 | doc.uri, 48 | new Range(new Position(0, 0), new Position(doc.lineCount - 1, 0)) 49 | ); 50 | } 51 | 52 | export async function runQuickFixes(quickFixes: CodeAction[]) { 53 | for(const quickFix of quickFixes) { 54 | console.log(` 55 | Executing: '${quickFix.title}'` 56 | ); 57 | await vscode.commands.executeCommand( 58 | quickFix.command.command, 59 | ...quickFix.command.arguments 60 | ); 61 | } 62 | } 63 | 64 | export async function didChangeDiagnostics(fsPath: string, [severety, count]: [DiagnosticSeverity, number], action: () => Thenable) { 65 | return didEvent( 66 | vscode.languages.onDidChangeDiagnostics, 67 | e => { 68 | const uri = e.uris.find(uri => uri.fsPath === fsPath); 69 | const diags = vscode.languages.getDiagnostics(uri).filter(d => d.severity <= severety); 70 | assert.isAtMost(diags.length, count); 71 | return uri && diags.length === count; 72 | }, 73 | action, 74 | ); 75 | } 76 | 77 | export async function didEvent( 78 | subscribe: (arg: (event: TEvent) => void) => Disposable, 79 | predicate: (event: TEvent) => Boolean, 80 | action: () => Thenable): Promise { 81 | return new Promise(async (resolve, _) => { 82 | const result = action(); 83 | const disposable = subscribe(async e => { 84 | if(predicate(e)) { 85 | disposable.dispose(); 86 | resolve(await result); 87 | } 88 | }); 89 | }); 90 | } 91 | 92 | export async function outputGHCiLog() { 93 | vscode.window.onDidChangeVisibleTextEditors(editors => { 94 | for (const editor of editors) { 95 | if (editor.document.fileName.startsWith('extension-output')) { 96 | const firstLine = editor.document.lineAt(0).text; 97 | if (!firstLine || firstLine.startsWith('Starting GHCi with')) { 98 | console.log(`\nGHCi Output:\n\n${editor.document.getText()}`); 99 | } 100 | } 101 | } 102 | }, this); 103 | await vscode.commands.executeCommand('vscode-ghc-simple.openOutput'); 104 | } 105 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { WorkspaceConfiguration } from 'vscode'; 3 | 4 | 5 | const rootSection = "haskutil"; 6 | const organiseImportsOnInsertSection = "organiseImportsOnInsert"; 7 | const organiseExtensionOnInsertSection = "organiseExtensionOnInsert"; 8 | const supportedExtensionsSection = "supportedExtensions"; 9 | const splitExtensionsSection = "splitExtensions"; 10 | const alignExtensionsSection = "alignExtensions"; 11 | const sortExtensionsSection = "sortExtensions"; 12 | const organizeExtensionsOnSaveSection = "organiseExtensionOnSave"; 13 | const alignImportsSection = "alignImports"; 14 | const alwaysPadImportsSection = "alwaysPadImports"; 15 | const sortImportsSection = "sortImports"; 16 | const sortImportedElementListsSection = "sortImportedElementLists"; 17 | const placeOperatorsAfterFunctionsSection = "placeOperatorsAfterFunctions"; 18 | const organizeImportsOnSaveSection = "organiseImportsOnSave"; 19 | const checkDiagnosticsExtensionSection = "checkDiagnosticsExtension"; 20 | const supportedDependenciesSection = "supportedDependencies"; 21 | 22 | function root(): WorkspaceConfiguration { 23 | return vscode.workspace.getConfiguration(rootSection); 24 | } 25 | 26 | function get(section: string): T { 27 | return root().get(section); 28 | } 29 | 30 | 31 | export default class Configuration { 32 | public static rootSection = rootSection; 33 | public static organiseImportsOnInsertSection = organiseImportsOnInsertSection; 34 | public static organiseExtensionOnInsertSection = organiseExtensionOnInsertSection; 35 | public static supportedExtensionsSection = supportedExtensionsSection; 36 | public static splitExtensionsSection = splitExtensionsSection; 37 | public static alignExtensionsSection = alignExtensionsSection; 38 | public static sortExtensionsSection = sortExtensionsSection; 39 | public static organizeExtensionsOnSaveSection = organizeExtensionsOnSaveSection; 40 | public static alignImportsSection = alignImportsSection; 41 | public static alwaysPadImportsSection = alwaysPadImportsSection; 42 | public static sortImportsSection = sortImportsSection; 43 | public static sortImportedElementListsSection = sortImportedElementListsSection; 44 | public static placeOperatorsAfterFunctionsSection = placeOperatorsAfterFunctionsSection; 45 | public static organizeImportsOnSaveSection = organizeImportsOnSaveSection; 46 | public static checkDiagnosticsExtensionSection = checkDiagnosticsExtensionSection; 47 | public static supportedDependenciesSection = supportedDependenciesSection; 48 | 49 | public static get root(): WorkspaceConfiguration { 50 | return root(); 51 | } 52 | 53 | public static get shouldOrganiseImportsOnInsert(): boolean { 54 | return get(organiseImportsOnInsertSection); 55 | } 56 | 57 | public static get shouldOrganiseExtensionOnInsert(): boolean { 58 | return get(organiseExtensionOnInsertSection); 59 | } 60 | 61 | public static get supportedExtensions(): string[] { 62 | return get(supportedExtensionsSection); 63 | } 64 | 65 | public static get shouldSplitExtensions(): boolean { 66 | return get(splitExtensionsSection); 67 | } 68 | 69 | public static get shouldAlignExtensions(): boolean { 70 | return get(alignExtensionsSection); 71 | } 72 | 73 | public static get shouldSortExtensions(): boolean { 74 | return get(sortExtensionsSection); 75 | } 76 | 77 | public static get shouldOrganizeExtensionsOnSave(): boolean { 78 | return get(organizeExtensionsOnSaveSection); 79 | } 80 | 81 | public static get shouldAlignImports(): boolean { 82 | return get(alignImportsSection); 83 | } 84 | 85 | public static get shouldPadImports(): boolean { 86 | return get(alwaysPadImportsSection); 87 | } 88 | 89 | public static get shouldSortImports(): boolean { 90 | return get(sortImportsSection); 91 | } 92 | 93 | public static get shouldSortImportedElementLists(): boolean { 94 | return get(sortImportedElementListsSection); 95 | } 96 | 97 | public static get shouldplaceOperatorsAfterFunctions(): boolean { 98 | return get(placeOperatorsAfterFunctionsSection); 99 | } 100 | 101 | public static get shouldOrganizeImportsOnSave(): boolean { 102 | return get(organizeImportsOnSaveSection); 103 | } 104 | 105 | public static get checkDiagnosticsExtension(): boolean { 106 | return get(checkDiagnosticsExtensionSection); 107 | } 108 | 109 | public static get supportedDependencies(): [{ id: string, name: string }] { 110 | return get(supportedDependenciesSection); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/features/removeUnusedImportProvider.ts: -------------------------------------------------------------------------------- 1 | import { CodeActionProvider, Disposable, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, WorkspaceEdit, CodeActionKind, Diagnostic, DiagnosticSeverity, DiagnosticChangeEvent, Uri } from 'vscode'; 2 | import * as vscode from 'vscode'; 3 | import ImportDeclaration from './importProvider/importDeclaration'; 4 | import OrganizeImportProvider from './organizeImportProvider'; 5 | import Configuration from '../configuration'; 6 | 7 | 8 | export default class RemoveUnusedImportProvider implements CodeActionProvider { 9 | public static commandId: string = 'haskell.removeUnusedImports'; 10 | private diagnosticCollection: vscode.DiagnosticCollection; 11 | private static diagnosticCode: string = "haskutil.unusedImports"; 12 | 13 | public activate(subscriptions: Disposable[]) { 14 | const command = vscode.commands.registerCommand(RemoveUnusedImportProvider.commandId, this.runCodeAction, this); 15 | subscriptions.push(command); 16 | this.diagnosticCollection = vscode.languages.createDiagnosticCollection(); 17 | subscriptions.push(this.diagnosticCollection); 18 | vscode.languages.onDidChangeDiagnostics(this.didChangeDiagnostics, this, subscriptions); 19 | } 20 | 21 | private async didChangeDiagnostics(e: DiagnosticChangeEvent) { 22 | for (const uri of e.uris) { 23 | const unusedImports = this.getUnusedImports(uri); 24 | if (unusedImports.length) { 25 | const document = await vscode.workspace.openTextDocument(uri); 26 | const imports = ImportDeclaration.getImports(document.getText()); 27 | const lastImport = imports[imports.length - 1]; 28 | const range = new Range( 29 | document.positionAt(imports[0].offset), 30 | document.positionAt(lastImport.offset + lastImport.length)); 31 | const message = "There are unused imports which can be removed"; 32 | const diagnostic = new Diagnostic(range, message, DiagnosticSeverity.Hint); 33 | diagnostic.code = RemoveUnusedImportProvider.diagnosticCode; 34 | if(!this.diagnosticCollection.has(document.uri) || !this.diagnosticCollection.get(document.uri)[0].range.isEqual(diagnostic.range)) { 35 | this.diagnosticCollection.set(document.uri, [diagnostic]); 36 | } 37 | } 38 | else if (!unusedImports.length && this.diagnosticCollection.has(uri)) { 39 | this.diagnosticCollection.delete(uri); 40 | } 41 | } 42 | } 43 | 44 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 45 | let codeActions = []; 46 | const diagnostics = context.diagnostics.filter(d => d.code === RemoveUnusedImportProvider.diagnosticCode); 47 | for (let diagnostic of diagnostics) { 48 | let title = "Remove unused imports"; 49 | let codeAction = new CodeAction(title, CodeActionKind.QuickFix); 50 | codeAction.command = { 51 | title: title, 52 | command: RemoveUnusedImportProvider.commandId, 53 | arguments: [ 54 | document 55 | ] 56 | }; 57 | codeAction.diagnostics = [diagnostic]; 58 | codeActions.push(codeAction); 59 | } 60 | return codeActions; 61 | } 62 | 63 | private runCodeAction(document: TextDocument): Thenable { 64 | const edit = new WorkspaceEdit(); 65 | let imports = ImportDeclaration.getImports(document.getText()); 66 | const toBeDeleted = []; 67 | for (const [range,,, list] of this.getUnusedImports(document.uri)) { 68 | const start = document.offsetAt(range.start); 69 | const oldImportIndex = imports.findIndex(i => i.offset <= start && i.offset + i.length >= start); 70 | const oldImport = imports[oldImportIndex]; 71 | if(list) { 72 | list.split(",").forEach(e => oldImport.removeElement(e.trim())); 73 | } else { 74 | imports.splice(oldImportIndex, 1); 75 | toBeDeleted.push(range); 76 | } 77 | } 78 | if (Configuration.shouldAlignImports) { 79 | imports = OrganizeImportProvider.alignImports(imports); 80 | } 81 | for (const imp of imports) { 82 | edit.delete(document.uri, imp.getRange(document)); 83 | edit.insert(document.uri, imp.getRange(document).start, imp.text); 84 | } 85 | for(const range of toBeDeleted) { 86 | edit.delete(document.uri, range.with({ end: range.end.with(range.end.line + 1, 0) })); 87 | } 88 | return vscode.workspace.applyEdit(edit); 89 | } 90 | 91 | private getUnusedImports(uri: Uri): [Range, ...string[]][] { 92 | const diagnostics = vscode.languages.getDiagnostics(uri); 93 | return diagnostics 94 | .map(d => [ 95 | d.range, 96 | d.message.match(/The (qualified )?import of (?:[`‘]([\s\S]+?)['’]\s+from module )?[`‘](.+?)['’] is redundant/m) 97 | ] as const) 98 | .filter(([,m]) => m) 99 | .map(([range, match]) => [ 100 | range, 101 | ...match 102 | ]); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/features/importProvider/importProviderBase.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Disposable, TextDocument, Range, WorkspaceEdit } from 'vscode'; 3 | import ExtensionProvider from '../extensionProvider'; 4 | import ImportDeclaration from './importDeclaration'; 5 | import OrganizeImportProvider from '../organizeImportProvider'; 6 | import Configuration from '../../configuration'; 7 | 8 | 9 | export interface SearchResult { 10 | package: string; 11 | module: string; 12 | result: string; 13 | } 14 | 15 | export default class ImportProviderBase { 16 | private static modulePattern = /^module(.|\n)+?where/m; 17 | private hoogleSearch: (name: string, resultCallback: HoogleSearchCallback) => void; 18 | 19 | constructor(protected commandId: string) { 20 | } 21 | 22 | public activate(subscriptions: Disposable[]) { 23 | const command = vscode.commands.registerCommand(this.commandId, this.runCodeAction, this); 24 | subscriptions.push(command); 25 | 26 | const hoogle = vscode.extensions.getExtension('jcanero.hoogle-vscode'); 27 | const hoogleApi = hoogle.exports; 28 | this.hoogleSearch = hoogleApi.search; 29 | } 30 | 31 | protected search(name: string): Promise { 32 | const result = new Promise(resolve => { 33 | this.hoogleSearch(name, searchResult => { 34 | resolve(searchResult.results 35 | .filter(result => { 36 | if (!result.isModule() && !result.isPackage()) { 37 | const r = this.decodeHtmlEntity(result.getQueryResult()); 38 | const i = r.indexOf(name); 39 | const j = i + name.length; 40 | return (i >= 0) && 41 | (i === 0 || r[i - 1] === " " || r[i - 1] === '(') && 42 | (j === r.length || r[j] === " " || r[j] === ")"); 43 | } 44 | else { 45 | return false; 46 | } 47 | }).map(result => { 48 | return { 49 | package: result.getPackageName(), 50 | module: result.getModuleName().replace(/-/g, '.'), 51 | result: result.getQueryResult() 52 | }; 53 | }) 54 | ); 55 | }); 56 | }); 57 | return result; 58 | } 59 | 60 | private decodeHtmlEntity(str: string): string { 61 | return str.replace(/&#(\d+);/g, (_, dec) => 62 | String.fromCharCode(dec)); 63 | } 64 | 65 | private async runCodeAction(document: TextDocument, moduleName: string, options: { qualified?: Boolean, alias?: string, elementName?: string } = {}): Promise { 66 | function afterMatch(offset) { 67 | const position = document.positionAt(offset); 68 | return document.offsetAt(position.with(position.line + 1, 0)); 69 | } 70 | 71 | const text = document.getText(); 72 | let position = 0; 73 | 74 | for (let match, pattern = ExtensionProvider.extensionPattern; match = pattern.exec(text);) { 75 | position = afterMatch(match.index + match[0].length); 76 | } 77 | 78 | const match = ImportProviderBase.modulePattern.exec(text); 79 | if (match !== null) { 80 | position = afterMatch(match.index + match[0].length); 81 | } 82 | 83 | const edit = new WorkspaceEdit(); 84 | 85 | const oldImports = ImportDeclaration.getImports(text); 86 | const oldImport = 87 | oldImports.find(decl => decl.module === moduleName && decl.importList !== null) || 88 | oldImports.find(decl => decl.module === moduleName); 89 | if (oldImport && options.elementName) { 90 | oldImport.addImportElement(options.elementName); 91 | const oldRange = new Range( 92 | document.positionAt(oldImport.offset), 93 | document.positionAt(oldImport.offset + oldImport.length)); 94 | edit.replace(document.uri, oldRange, oldImport.text); 95 | } 96 | else { 97 | for (const importInfo of oldImports) { 98 | if (importInfo.module + (importInfo.importList || "") > moduleName) { 99 | position = importInfo.offset; 100 | break; 101 | } 102 | position = afterMatch(importInfo.offset + importInfo.length); 103 | } 104 | const importDeclaration = new ImportDeclaration(moduleName); 105 | if (options.qualified) { 106 | importDeclaration.qualified = " qualified "; 107 | if (options.alias) { 108 | importDeclaration.alias = options.alias; 109 | } 110 | } 111 | if (options.elementName) { 112 | importDeclaration.addImportElement(options.elementName); 113 | } 114 | 115 | // Align import if necessary 116 | OrganizeImportProvider.ensureAligned(importDeclaration, oldImports); 117 | 118 | edit.insert(document.uri, document.positionAt(position), importDeclaration.text + "\n"); 119 | } 120 | 121 | await vscode.workspace.applyEdit(edit); 122 | if(Configuration.shouldOrganiseImportsOnInsert) { 123 | await vscode.commands.executeCommand(OrganizeImportProvider.commandId, document); 124 | } 125 | } 126 | } 127 | 128 | interface HoogleSearchCallback { 129 | (result: { results: HoogleResult[] }): void; 130 | } 131 | 132 | interface HoogleResult { 133 | isModule(): boolean; 134 | isPackage(): boolean; 135 | getPackageName(): string; 136 | getModuleName(): string; 137 | getQueryResult(): string; 138 | } 139 | -------------------------------------------------------------------------------- /src/features/importProvider/importDeclaration.ts: -------------------------------------------------------------------------------- 1 | import { Range, TextDocument } from "vscode"; 2 | import Configuration from "../../configuration"; 3 | 4 | export default class ImportDeclaration { 5 | private _before: string = ''; 6 | private _importElements: string[] = []; 7 | private _importSeparators: string[] = []; 8 | private _after: string = ''; 9 | qualified: string = " "; 10 | alias?: string; 11 | importList?: string; 12 | hidingList?: string; 13 | offset?: number; 14 | length?: number; 15 | 16 | constructor( 17 | public module: string, 18 | optional?: { 19 | qualified?: string, 20 | alias?: string, 21 | importList?: string, 22 | importElements?: string, 23 | hidingList?: string, 24 | offset?: number, 25 | length?: number 26 | }) { 27 | if (optional) { 28 | this.qualified = optional.qualified || ""; 29 | this.alias = optional.alias; 30 | this.importList = optional.importList; 31 | this.importElements = optional.importElements; 32 | this.hidingList = optional.hidingList; 33 | this.offset = optional.offset; 34 | this.length = optional.length; 35 | } 36 | } 37 | 38 | private get separator() { 39 | return ', ' 40 | }; 41 | 42 | public get importElements() { 43 | const separators = this._importSeparators.concat(''); 44 | const list = this._importElements.flatMap((elem, i) => [elem, separators[i]]); 45 | return [this._before, ...list, this._after].join(''); 46 | } 47 | 48 | public set importElements(elementsString: string) { 49 | const input = elementsString ?? ''; 50 | const empty = /^\s*$/g; 51 | const before = /^\s*/g; 52 | const after = /\s*$/g; 53 | const separators = /(?<=\S)\s*,\s*(?=\S)/g; 54 | if (empty.test(input)) { 55 | const middle = input.length / 2; 56 | this._before = input.slice(0, middle); 57 | this._after = input.slice(middle) 58 | } else { 59 | this._before = input.match(before)[0]; 60 | this._after = input.match(after)[0]; 61 | } 62 | const matches = [...input.matchAll(separators)].map(m => [m.index, m[0]] as const); 63 | this._importSeparators = matches.map(m => m[1]); 64 | const indices = matches.map(m => [m[0], m[0] + m[1].length] as const); 65 | const starts = [this._before.length].concat(indices.map(ixs => ixs[1])); 66 | const ends = indices.map(ixs => ixs[0]).concat(input.length - this._after.length); 67 | this._importElements = starts.map((ix, i) => input.slice(ix, ends[i])).filter(e => e !== ''); 68 | } 69 | 70 | public addImportElement(newElem: string) { 71 | let before = `(${this.importElements})`; 72 | if (!this.importList) { 73 | this.importList = " ()"; 74 | before = "()"; 75 | } 76 | 77 | let index = this._importElements.findIndex(oldElem => this.compareImportElements(newElem, oldElem) < 0); 78 | index = index === -1 ? this._importElements.length : index; 79 | if (this._importElements.length > 0) { 80 | if (index === this._importElements.length) { 81 | this._importSeparators.push(this.separator); 82 | } else { 83 | this._importSeparators.splice(index, 0, this.separator); 84 | } 85 | } 86 | this._importElements.splice(index, 0, newElem); 87 | 88 | this.importList = this.importList.replace(before, `(${this.importElements})`); 89 | } 90 | 91 | public removeElement(elem: string) { 92 | const before = this.importElements; 93 | 94 | const index = this._importElements.findIndex(oldElem => oldElem === elem || oldElem.replace(' ', '') == `${elem}(..)`); 95 | if (index !== -1) { 96 | if (this._importElements.length > 1) { 97 | if (index === this._importElements.length - 1) { 98 | this._importSeparators.pop(); 99 | } else { 100 | this._importSeparators.splice(index, 1); 101 | } 102 | } 103 | this._importElements.splice(index, 1); 104 | 105 | this.importList = this.importList.replace(before, this.importElements); 106 | } 107 | } 108 | 109 | public get importElementsSorted() { 110 | return [...this._importElements] 111 | .sort(this.compareImportElements) 112 | .every((elem, i) => this._importElements[i] === elem); 113 | } 114 | 115 | public sortImportElements() { 116 | if (this._importElements.length > 0) { 117 | const before = this.importElements; 118 | this._importElements.sort(this.compareImportElements); 119 | this.importList = this.importList.replace(before, this.importElements); 120 | } 121 | } 122 | 123 | private compareImportElements(left: string, right: string) { 124 | const toSortable = (elem: string) => elem.replace('(', '~'); 125 | if (Configuration.shouldplaceOperatorsAfterFunctions) { 126 | left = toSortable(left); 127 | right = toSortable(right); 128 | } 129 | return left < right ? -1 : left > right ? 1 : 0; 130 | } 131 | 132 | public get text() { 133 | return `import${this.qualified || ""}${this.module}${this.alias || ""}${this.importList || ""}${this.hidingList || ""}`; 134 | } 135 | 136 | public getRange(document: TextDocument): Range { 137 | return new Range( 138 | document.positionAt(this.offset), 139 | document.positionAt(this.offset + this.length)); 140 | } 141 | 142 | public static getImports(text: string): ImportDeclaration[] { 143 | const importPattern = /^import((?:\s+qualified\s+)|\s+)(\S+)(\s+as\s+(\S+))?(\s*?\(((?:(?:\(.*?\))|.|\r?\n)*?)\))?(\s+hiding\s+\(((?:(?:\(.*?\))|.|\r?\n)*?)\))?/gm; 144 | const imports = []; 145 | for (let match; match = importPattern.exec(text);) { 146 | imports.push(new ImportDeclaration( 147 | match[2], 148 | { 149 | qualified: match[1], 150 | alias: match[3], 151 | importList: match[5], 152 | importElements: match[6], 153 | hidingList: match[7], 154 | offset: match.index, 155 | length: match[0].length 156 | })); 157 | } 158 | return imports; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/features/organizeImportProvider.ts: -------------------------------------------------------------------------------- 1 | import { CodeActionProvider, Disposable, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, WorkspaceEdit, CodeActionKind, Diagnostic, DiagnosticSeverity, WorkspaceConfiguration, TextDocumentWillSaveEvent } from 'vscode'; 2 | import * as vscode from 'vscode'; 3 | import ImportDeclaration from './importProvider/importDeclaration'; 4 | import Configuration from '../configuration'; 5 | 6 | 7 | export default class OrganizeImportProvider implements CodeActionProvider { 8 | public static commandId: string = 'haskell.organizeImports'; 9 | private diagnosticCollection: vscode.DiagnosticCollection; 10 | private static diagnosticCode: string = "haskutil.unorganizedImports"; 11 | 12 | public activate(subscriptions: Disposable[]) { 13 | const command = vscode.commands.registerCommand(OrganizeImportProvider.commandId, this.runCodeAction, this); 14 | subscriptions.push(command); 15 | 16 | this.diagnosticCollection = vscode.languages.createDiagnosticCollection(); 17 | subscriptions.push(this.diagnosticCollection); 18 | vscode.workspace.onDidOpenTextDocument(this.checkImports, this, subscriptions); 19 | vscode.workspace.onDidCloseTextDocument(doc => this.diagnosticCollection.delete(doc.uri), null, subscriptions); 20 | vscode.workspace.onDidSaveTextDocument(this.checkImports, this, subscriptions); 21 | vscode.workspace.onWillSaveTextDocument(this.ensureOrganized, this, subscriptions); 22 | vscode.workspace.textDocuments.filter(d => d.languageId === 'haskell').forEach(this.checkImports, this); 23 | } 24 | 25 | private checkImports(document: TextDocument) { 26 | // We subscribe to multiple events which can be fired for any language, not just `Haskell` 27 | if (document.languageId !== 'haskell') { 28 | return; 29 | } 30 | const imports = ImportDeclaration.getImports(document.getText()); 31 | let messages = []; 32 | 33 | const aligned = 34 | imports.length === 0 || ( 35 | (Configuration.shouldPadImports || 36 | imports.some(imp => imp.qualified.trim() === "qualified") 37 | ) && 38 | imports.every(imp => imp.qualified.length === " qualified ".length) 39 | ) || ( 40 | !Configuration.shouldPadImports && 41 | imports.every(imp => imp.qualified.trim() === "") && 42 | imports.every(imp => imp.qualified.length === " ".length) 43 | ); 44 | if (!aligned && Configuration.shouldAlignImports) { 45 | messages = ["not aligned"]; 46 | } 47 | 48 | let pred = ""; 49 | if (Configuration.shouldSortImports) { 50 | for (const imp of imports) { 51 | const curr = imp.module + (imp.importList || ""); 52 | if (curr < pred || Configuration.shouldSortImportedElementLists && !imp.importElementsSorted) { 53 | messages.unshift("unsorted"); 54 | break; 55 | } 56 | pred = curr; 57 | } 58 | } 59 | 60 | if (messages.length > 0) { 61 | const lastImport = imports[imports.length - 1]; 62 | const range = new Range( 63 | document.positionAt(imports[0].offset), 64 | document.positionAt(lastImport.offset + lastImport.length)); 65 | const message = `Imports are ${messages.join(" and ")}`; 66 | const diagnostic = new Diagnostic(range, message, DiagnosticSeverity.Hint); 67 | diagnostic.code = OrganizeImportProvider.diagnosticCode; 68 | this.diagnosticCollection.set(document.uri, [diagnostic]); 69 | } 70 | else { 71 | this.diagnosticCollection.delete(document.uri); 72 | } 73 | } 74 | 75 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 76 | let codeActions = []; 77 | const diagnostics = context.diagnostics.filter(d => d.code === OrganizeImportProvider.diagnosticCode); 78 | for (let diagnostic of diagnostics) { 79 | let title = "Organize imports"; 80 | let codeAction = new CodeAction(title, CodeActionKind.QuickFix); 81 | codeAction.command = { 82 | title: title, 83 | command: OrganizeImportProvider.commandId, 84 | arguments: [ 85 | document 86 | ] 87 | }; 88 | codeAction.diagnostics = [diagnostic]; 89 | codeActions.push(codeAction); 90 | } 91 | return codeActions; 92 | } 93 | 94 | private runCodeAction(document: TextDocument): Thenable { 95 | const oldImports = ImportDeclaration.getImports(document.getText()); 96 | let newImports = oldImports.map(i => i); 97 | if (Configuration.shouldSortImports) { 98 | if (Configuration.shouldSortImportedElementLists) { 99 | newImports.forEach(imp => imp.importElementsSorted || imp.sortImportElements()); 100 | } 101 | newImports.sort((l, r) => { 102 | const ls = l.module + (l.importList || ""); 103 | const rs = r.module + (r.importList || ""); 104 | return ls < rs ? -1 : (ls === rs ? 0 : 1); 105 | }); 106 | } 107 | if (Configuration.shouldAlignImports) { 108 | newImports = OrganizeImportProvider.alignImports(newImports); 109 | } 110 | 111 | var edit = new WorkspaceEdit(); 112 | for (let i = oldImports.length - 1; i >= 0; i--) { 113 | edit.delete(document.uri, oldImports[i].getRange(document)); 114 | edit.insert(document.uri, oldImports[i].getRange(document).start, newImports[i].text); 115 | } 116 | return vscode.workspace.applyEdit(edit); 117 | } 118 | 119 | private ensureOrganized(event: TextDocumentWillSaveEvent) { 120 | if (Configuration.shouldOrganizeImportsOnSave) { 121 | event.waitUntil(this.runCodeAction(event.document)); 122 | } 123 | } 124 | 125 | public static ensureAligned(newImport: ImportDeclaration, existingImports: ImportDeclaration[]): ImportDeclaration { 126 | if (Configuration.shouldAlignImports) { 127 | let allImports = existingImports.map(imp => imp); 128 | allImports.unshift(newImport); 129 | OrganizeImportProvider.alignImports(allImports); 130 | } 131 | return newImport; 132 | } 133 | 134 | public static alignImports(imports: ImportDeclaration[]): ImportDeclaration[] { 135 | const isQualified = imp => imp.qualified.trim() === "qualified"; 136 | 137 | return Configuration.shouldPadImports || imports.some(isQualified) ? 138 | imports.map(imp => { 139 | if (isQualified(imp)) { 140 | imp.qualified = " qualified "; 141 | } 142 | else { 143 | imp.qualified = " "; 144 | } 145 | return imp; 146 | }) : 147 | imports.map(imp => { 148 | imp.qualified = " "; 149 | return imp; 150 | }); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/features/organizeExtensionProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CodeActionProvider, Disposable, TextDocument, Range, CodeActionContext, CancellationToken, CodeAction, WorkspaceEdit, CodeActionKind, Diagnostic, DiagnosticSeverity, WorkspaceConfiguration, TextDocumentWillSaveEvent } from 'vscode'; 4 | import * as vscode from 'vscode'; 5 | import ExtensionDeclaration from './extensionProvider/extensionDeclaration'; 6 | import Configuration from '../configuration'; 7 | 8 | 9 | export default class OrganizeExtensionProvider implements CodeActionProvider { 10 | public static commandId: string = 'haskell.organizeExtensions'; 11 | private diagnosticCollection: vscode.DiagnosticCollection; 12 | private static diagnosticCode: string = "haskutil.unorganizedExtensions"; 13 | 14 | public activate(subscriptions: Disposable[]) { 15 | const command = vscode.commands.registerCommand(OrganizeExtensionProvider.commandId, this.runCodeAction, this); 16 | subscriptions.push(command); 17 | 18 | this.diagnosticCollection = vscode.languages.createDiagnosticCollection(); 19 | subscriptions.push(this.diagnosticCollection); 20 | vscode.workspace.onDidOpenTextDocument(this.checkExtensions, this, subscriptions); 21 | vscode.workspace.onDidCloseTextDocument(doc => this.diagnosticCollection.delete(doc.uri), null, subscriptions); 22 | vscode.workspace.onDidSaveTextDocument(this.checkExtensions, this, subscriptions); 23 | vscode.workspace.onWillSaveTextDocument(this.ensureOrganized, this, subscriptions); 24 | vscode.workspace.textDocuments.forEach(this.checkExtensions, this); 25 | } 26 | 27 | private checkExtensions(document: TextDocument) { 28 | const extensions = ExtensionDeclaration.getExtensions(document.getText()); 29 | 30 | let unorganized = 31 | extensions.some(extension => extension.extensionNames.length > 1); 32 | 33 | const aligned = 34 | extensions.length === 0 || 35 | extensions.every(extension => extension.extensions.length === extensions[0].extensions.length) && 36 | Math.max(...extensions.map(extension => extension.extensions.trimEnd().length + 1)) === extensions[0].extensions.length; 37 | unorganized = unorganized || Configuration.shouldAlignExtensions && !aligned; 38 | 39 | let pred = ""; 40 | if (Configuration.shouldSortExtensions) { 41 | for (const extension of extensions) { 42 | const curr = extension.extensions; 43 | if (curr < pred) { 44 | unorganized = true; 45 | break; 46 | } 47 | pred = curr; 48 | } 49 | } 50 | 51 | if (unorganized) { 52 | const lastExtension = extensions[extensions.length - 1]; 53 | const range = new Range( 54 | document.positionAt(extensions[0].offset), 55 | document.positionAt(lastExtension.offset + lastExtension.length)); 56 | const message = `Extension are unorganised`; 57 | const diagnostic = new Diagnostic(range, message, DiagnosticSeverity.Hint); 58 | diagnostic.code = OrganizeExtensionProvider.diagnosticCode; 59 | this.diagnosticCollection.set(document.uri, [diagnostic]); 60 | } 61 | else { 62 | this.diagnosticCollection.delete(document.uri); 63 | } 64 | } 65 | 66 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { 67 | let codeActions = []; 68 | const diagnostics = context.diagnostics.filter(d => d.code === OrganizeExtensionProvider.diagnosticCode); 69 | for (let diagnostic of diagnostics) { 70 | let title = "Organize extensions"; 71 | let codeAction = new CodeAction(title, CodeActionKind.QuickFix); 72 | codeAction.command = { 73 | title: title, 74 | command: OrganizeExtensionProvider.commandId, 75 | arguments: [ 76 | document 77 | ] 78 | }; 79 | codeAction.diagnostics = [diagnostic]; 80 | codeActions.push(codeAction); 81 | } 82 | return codeActions; 83 | } 84 | 85 | private async runCodeAction(document: TextDocument) { 86 | if (Configuration.shouldSplitExtensions) { 87 | await this.splitExtensions(document); 88 | } 89 | if (Configuration.shouldAlignExtensions) { 90 | await this.alignExtensions(document); 91 | } 92 | if (Configuration.shouldSortExtensions) { 93 | await this.sortExtensions(document); 94 | } 95 | } 96 | 97 | private ensureOrganized(event: TextDocumentWillSaveEvent) { 98 | if (Configuration.shouldOrganizeExtensionsOnSave) { 99 | event.waitUntil(this.runCodeAction(event.document)); 100 | } 101 | } 102 | 103 | private async splitExtensions(document: TextDocument) { 104 | for (const extension of ExtensionDeclaration.getExtensions(document.getText()).reverse()) { 105 | await this.splitExtension(extension, document); 106 | } 107 | } 108 | 109 | private async splitExtension(extension: ExtensionDeclaration, document: TextDocument) { 110 | if (extension.extensionNames.length > 1) { 111 | const edit = new WorkspaceEdit(); 112 | const range = extension.getRange(document); 113 | edit.delete(document.uri, range.with({ end: range.end.with(range.end.line + 1, 0) })); 114 | 115 | const extensions = extension.extensionNames.map(name => 116 | new ExtensionDeclaration(extension.header, `${name} `)); 117 | extensions.sort((l, r) => r.text.localeCompare(l.text)); 118 | for (const newExtension of extensions) { 119 | edit.insert(document.uri, range.start, newExtension.text + "\n"); 120 | } 121 | await vscode.workspace.applyEdit(edit); 122 | } 123 | } 124 | 125 | private async alignExtensions(document: TextDocument) { 126 | const oldExtensions = ExtensionDeclaration.getExtensions(document.getText()); 127 | const length = Math.max(...oldExtensions.map(extension => extension.extensions.trimEnd().length)); 128 | const newExtensions = oldExtensions.map(extension => { 129 | const extensionsTrimmed = extension.extensions.trimEnd(); 130 | return new ExtensionDeclaration( 131 | extension.header, 132 | extensionsTrimmed.concat(" ".repeat(length - extensionsTrimmed.length + 1))); 133 | }); 134 | 135 | var edit = new WorkspaceEdit(); 136 | for (let i = oldExtensions.length - 1; i >= 0; i--) { 137 | edit.delete(document.uri, oldExtensions[i].getRange(document)); 138 | edit.insert(document.uri, oldExtensions[i].getRange(document).start, newExtensions[i].text); 139 | } 140 | await vscode.workspace.applyEdit(edit); 141 | } 142 | 143 | private async sortExtensions(document: TextDocument) { 144 | const oldExtensions = ExtensionDeclaration.getExtensions(document.getText()); 145 | let newExtensions = oldExtensions.map(i => i); 146 | newExtensions.sort((l, r) => l.text.localeCompare(r.text)); 147 | 148 | var edit = new WorkspaceEdit(); 149 | for (let i = oldExtensions.length - 1; i >= 0; i--) { 150 | edit.delete(document.uri, oldExtensions[i].getRange(document)); 151 | edit.insert(document.uri, oldExtensions[i].getRange(document).start, newExtensions[i].text); 152 | } 153 | await vscode.workspace.applyEdit(edit); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haskutil", 3 | "displayName": "Haskutil", 4 | "description": "'QuickFix' actions for Haskell editor", 5 | "version": "0.14.0", 6 | "publisher": "Edka", 7 | "icon": "images/Haskutil-logo.png", 8 | "repository": { 9 | "url": "https://github.com/EduardSergeev/vscode-haskutil" 10 | }, 11 | "engines": { 12 | "vscode": "^1.48.0" 13 | }, 14 | "categories": [ 15 | "Other" 16 | ], 17 | "license": "MIT", 18 | "galleryBanner": { 19 | "theme": "dark", 20 | "color": "#444" 21 | }, 22 | "main": "./out/extension", 23 | "contributes": { 24 | "languages": [ 25 | { 26 | "id": "haskell", 27 | "aliases": [ 28 | "Haskell", 29 | "haskell" 30 | ], 31 | "extensions": [ 32 | ".hs", 33 | ".lhs" 34 | ] 35 | } 36 | ], 37 | "configuration": { 38 | "type": "object", 39 | "title": "Haskutil configuration settings", 40 | "properties": { 41 | "haskutil.feature.addImport": { 42 | "type": "boolean", 43 | "default": true, 44 | "scope": "resource", 45 | "description": "Enable `Add import` feature" 46 | }, 47 | "haskutil.feature.addQualifiedImport": { 48 | "type": "boolean", 49 | "default": true, 50 | "scope": "resource", 51 | "description": "Enable `Add qualified import` feature" 52 | }, 53 | "haskutil.feature.organizeImports": { 54 | "type": "boolean", 55 | "default": true, 56 | "scope": "resource", 57 | "description": "Enable `Organize import` feature" 58 | }, 59 | "haskutil.feature.addExtension": { 60 | "type": "boolean", 61 | "default": true, 62 | "scope": "resource", 63 | "description": "Enable `Add extension` feature" 64 | }, 65 | "haskutil.feature.organizeExtensions": { 66 | "type": "boolean", 67 | "default": true, 68 | "scope": "resource", 69 | "description": "Enable `Organize extensions` feature" 70 | }, 71 | "haskutil.feature.addSignature": { 72 | "type": "boolean", 73 | "default": true, 74 | "scope": "resource", 75 | "description": "Enable `Add top-level signature` feature" 76 | }, 77 | "haskutil.feature.fillTypeHole": { 78 | "type": "boolean", 79 | "default": true, 80 | "scope": "resource", 81 | "description": "Enable `Fill type hole` feature" 82 | }, 83 | "haskutil.feature.fillTypeWildcard": { 84 | "type": "boolean", 85 | "default": true, 86 | "scope": "resource", 87 | "description": "Enable `Fill type wildcard` feature" 88 | }, 89 | "haskutil.alignImports": { 90 | "type": "boolean", 91 | "description": "Align imports when organizing imports", 92 | "default": true 93 | }, 94 | "haskutil.alwaysPadImports": { 95 | "type": "boolean", 96 | "description": "Always pad after `import` regardless if there is `qualified` import or not", 97 | "default": false 98 | }, 99 | "haskutil.sortImports": { 100 | "type": "boolean", 101 | "description": "Sort imports when organizing imports", 102 | "default": true 103 | }, 104 | "haskutil.placeOperatorsAfterFunctions": { 105 | "type": "boolean", 106 | "description": "Place operators after other elements in imported element lists in import declarations", 107 | "default": false 108 | }, 109 | "haskutil.sortImportedElementLists": { 110 | "type": "boolean", 111 | "description": "Sort imported elements lists in import declarations when organizing imports", 112 | "default": true 113 | }, 114 | "haskutil.organiseImportsOnSave": { 115 | "type": "boolean", 116 | "description": "Organize imports on save", 117 | "default": false 118 | }, 119 | "haskutil.organiseImportsOnInsert": { 120 | "type": "boolean", 121 | "description": "Organize imports when adding new import", 122 | "default": true 123 | }, 124 | "haskutil.feature.removeUnusedImport": { 125 | "type": "boolean", 126 | "default": true, 127 | "scope": "resource", 128 | "description": "Enable `Remove unused imports` feature" 129 | }, 130 | "haskutil.splitExtensions": { 131 | "type": "boolean", 132 | "description": "Make sure there is one extension per LANGUAGE pragma when organizing extensions", 133 | "default": true 134 | }, 135 | "haskutil.alignExtensions": { 136 | "type": "boolean", 137 | "description": "Make sure of LANGUAGE extension pragma are of the same length when organizing extensions", 138 | "default": true 139 | }, 140 | "haskutil.sortExtensions": { 141 | "type": "boolean", 142 | "description": "Make sure LANGUAGE extension pragmas are sorted when organizing imports", 143 | "default": true 144 | }, 145 | "haskutil.organiseExtensionOnSave": { 146 | "type": "boolean", 147 | "description": "Organize extensions on save", 148 | "default": false 149 | }, 150 | "haskutil.organiseExtensionOnInsert": { 151 | "type": "boolean", 152 | "description": "Organize extensions when adding new extensions", 153 | "default": true 154 | }, 155 | "haskutil.supportedExtensions": { 156 | "type": "array", 157 | "description": "Haskell LANGUAGE extensions specified in GHC's error messages", 158 | "default": [ 159 | "BangPatterns", 160 | "DataKinds", 161 | "DefaultSignatures", 162 | "DeriveFunctor", 163 | "DeriveGeneric", 164 | "FlexibleContexts", 165 | "FlexibleInstances", 166 | "FunctionalDependencies", 167 | "GADTs", 168 | "GeneralizedNewtypeDeriving", 169 | "KindSignatures", 170 | "MultiParamTypeClasses", 171 | "NamedFieldPuns", 172 | "RankNTypes", 173 | "RecordWildCards", 174 | "StandaloneDeriving", 175 | "TemplateHaskell", 176 | "TupleSections", 177 | "TypeFamilies", 178 | "TypeSynonymInstances", 179 | "UndecidableInstances", 180 | "TypeApplications", 181 | "ViewPatterns" 182 | ] 183 | }, 184 | "haskutil.checkDiagnosticsExtension": { 185 | "type": "boolean", 186 | "description": "Check if any of recommended VSCode extensions which generate Haskell diagnostics is installed", 187 | "default": true 188 | }, 189 | "haskutil.supportedDependencies": { 190 | "type": "array", 191 | "description": "Supported Haskell diagnostic generating extensions", 192 | "items": { 193 | "type": "object", 194 | "properties": { 195 | "id": { 196 | "title": "Extension id", 197 | "type": "string" 198 | }, 199 | "name": { 200 | "title": "Extension name", 201 | "type": "string" 202 | } 203 | } 204 | }, 205 | "default": [ 206 | { 207 | "id": "dramforever.vscode-ghc-simple", 208 | "name": "GHC" 209 | }, 210 | { 211 | "id": "Vans.haskero", 212 | "name": "Haskero" 213 | }, 214 | { 215 | "id": "ndmitchell.haskell-ghcid", 216 | "name": "ghcid" 217 | }, 218 | { 219 | "id": "digitalassetholdingsllc.ghcide", 220 | "name": "ghcid" 221 | }, 222 | { 223 | "id": "taylorfausak.purple-yolk", 224 | "name": "Purple Yolk" 225 | } 226 | ] 227 | } 228 | } 229 | } 230 | }, 231 | "activationEvents": [ 232 | "onLanguage:haskell" 233 | ], 234 | "extensionDependencies": [ 235 | "jcanero.hoogle-vscode" 236 | ], 237 | "scripts": { 238 | "precompile": "rm -rf ./out", 239 | "compile": "tsc -p ./", 240 | "watch": "tsc -watch -p ./", 241 | "pretest": "npm run compile", 242 | "test": "mocha ./out/test/runTest.js", 243 | "coverage": "c8 npm test", 244 | "prepackage": "npm run compile", 245 | "package": "vsce package", 246 | "preupload": "npm run package", 247 | "upload": "vsce publish" 248 | }, 249 | "c8": { 250 | "include": [ 251 | "src/**/*.ts", 252 | "out/**/*.js" 253 | ], 254 | "exclude": [ 255 | "src/test/*", 256 | "out/test/*" 257 | ], 258 | "reporter": [ 259 | "text-summary", 260 | "html", 261 | "lcov" 262 | ], 263 | "check-coverage": false, 264 | "report-dir": ".coverage" 265 | }, 266 | "devDependencies": { 267 | "@types/chai": "4.3.16", 268 | "@types/mocha": "10.0.6", 269 | "@types/node": "20.12.12", 270 | "@types/vscode": "1.48.0", 271 | "@vscode/test-electron": "2.3.9", 272 | "@vscode/vsce": "2.23.0", 273 | "c8": "^10.1.2", 274 | "chai": "4.4.1", 275 | "mocha": "10.4.0", 276 | "source-map-support": "0.5.21", 277 | "ts-node": "10.9.2", 278 | "tslint": "6.1.3", 279 | "typescript": "4.9.5" 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "Haskutil" extension will be documented in this file. 3 | 4 | 5 | ## [0.14.0] - 2024-06-23 6 | ### Added 7 | * `Organize imports` now also sorts imported element lists ([#86](https://github.com/EduardSergeev/vscode-haskutil/issues/86)): 8 | When sorting import declaration if `"haskutil.sortImportedElementLists"` is set to `true` (default value) 9 | * An option to place operators at the end of imported element lists [#88](https://github.com/EduardSergeev/vscode-haskutil/issues/88): 10 | When inserting new elements via `Add import` or sorting imported elements via `Organize imports` operators can now be placed after other type of elements, i.e. after functions, constructors or types. 11 | This behavior is controlled via new configuration setting `"haskutil.placeOperatorsAfterFunctions"` which is set to `false` by default 12 | ### Changed 13 | * Switch to [c8](https://github.com/bcoe/c8) for code coverage (fix [#87](https://github.com/EduardSergeev/vscode-haskutil/issues/87)) 14 | 15 | 16 | ## [0.13.3] - 2024-06-19 17 | ### Fixed 18 | * `Add import` action's handling of operators containing `.` (dot)([#80](https://github.com/EduardSergeev/vscode-haskutil/issues/80)): 19 | Previously no quick fix action was emitted for such operators (e.g. `(.&.)` or `(.=)`) 20 | * `Add import` action's handling of names containing `'` (single quote) ([#81](https://github.com/EduardSergeev/vscode-haskutil/issues/81)): 21 | Previously no quick fix action was emitted for such names (e.g. `foldl'`) 22 | 23 | ## [0.13.2] - 2024-05-19 24 | ### Fixed 25 | * `Remove unused imports` action when import declaration contains elements with `(..)` ([#40](https://github.com/EduardSergeev/vscode-haskutil/issues/40)): 26 | Fix behaviour of `RemoveUnusedImportProvider` in regards to removal from import lists containing elements with export lists, e.g. `Sum(..)` or `Sum(fromSum)`; 27 | * `Replace wildcard` action under GHC 9.6.5 ([#79](https://github.com/EduardSergeev/vscode-haskutil/issues/79)): 28 | Fix `TypeWildcardProvider` behavior when running with latest GHC handling changes in corresponding GHC error message format; 29 | ### Changed 30 | * Switch `branch` CI build to run tests against the latest GHC 31 | 32 | ## [0.13.1] - 2024-05-11 33 | ### Fixed 34 | * Intermittent test failures 35 | Failures with `Cancelled` error message 36 | * `CHANELOG` formatting 37 | 38 | ## [0.13.0] - 2024-05-07 39 | ### Added 40 | * [Purple Yolk](https://github.com/tfausak/purple-yolk) extension is added to the list of `haskutil.supportedDependencies` 41 | ([#73](https://github.com/EduardSergeev/vscode-haskutil/pull/73) - thanks to [Taylor Fausak](https://github.com/tfausak) for contribution) 42 | ### Fixed 43 | * CI build on MacOS 44 | 45 | ## [0.12.3] - 2023-09-09 46 | ### Added 47 | * Extension icon 48 | 49 | ## [0.12.2] - 2023-08-31 50 | ### Changed 51 | * `Dependency not installed` warning message extension links: 52 | Instead of linking to extension's Marketplace page link to VSCode `EXTENSIONS` view 53 | Focused to linked extension via `workbench.extensions.search` command-link 54 | 55 | ## [0.12.1] - 2023-08-30 56 | ### Fixed 57 | * `CHANGELOG` formatting: 58 | Add missing spaces to `CHANGELOG.md` for proper newline rendering 59 | 60 | ## [0.12.0] - 2023-08-30 61 | ### Added 62 | * `haskutil.supportedDependencies` configuration option: 63 | Make the list of supported Haskell diagnostic generating extensions configurable 64 | 65 | ## [0.11.4] - 2023-08-27 66 | ### Fixed 67 | * `OrganizeExtensionProvider`: false positive of `Extension are unorganised` 68 | Aligned extension were incorrectly detected as being not aligned 69 | * `ImportProvider`: `Organize imports` behaviour on Windows 70 | Fix invalid parsing of imports in file with `\r\n` (Windows) line ending 71 | As a result applying `Organize imports` would previously lead to corrupted imports 72 | * Fail CI build in case of test failure: 73 | Previously build would still be green even if some of the test wer failing 74 | 75 | ## [0.11.3] - 2023-08-26 76 | ### Fixed 77 | * `ImportProvider`: 78 | Do not add `(..)` when adding import for operators 79 | 80 | ## [0.11.2] - 2023-08-26 81 | ### Fixed 82 | * `OrganizeExtensionProvider`: 83 | Make sure all `LANGUAGE` extension are aligning using minimal padding 84 | Fixes [#41](https://github.com/EduardSergeev/vscode-haskutil/issues/41) 85 | * `ImportProvider`: 86 | Suggest adding class or data type with `(..)`, i.e. with all defined constructors/members 87 | Fixes [#44](https://github.com/EduardSergeev/vscode-haskutil/issues/41) 88 | 89 | ## [0.11.0] - 2023-08-26 90 | ### Added 91 | * `haskutil.checkDiagnosticsExtension` configuration option: 92 | Check if any of recommended VSCode extensions which generate Haskell diagnostics is installed 93 | Optional, default is `true` 94 | 95 | ## [0.10.8] - 2023-08-21 96 | ### Fixed 97 | * `QualifiedImportProvider`: 98 | - Suppress `Add: "import qualified ... as undefined"` suggestions 99 | - Avoid annecessary alias in qualified import: fixes [#48](https://github.com/EduardSergeev/vscode-haskutil/issues/48) 100 | * `certificate has expired` error in tests: 101 | Switch to VSCode version `1.66.2` which does not have this problem 102 | ### Changed 103 | * Oldest supported VSCode version is now `1.48.0` 104 | 105 | ## [0.10.7] - 2023-08-20 106 | ### Fixed 107 | * `RemoveUnusedImportProvider` on GHC > `9.0.2`: 108 | Fix `Cannot read properties of undefined (reading 'removeElement')` exception 109 | * `TypeWildcardProvider`: 110 | Handle various error message formats: 111 | - old from GHC <= `9.0.2` 112 | - new from GHC > `9.0.2` 113 | * `QualifiedImportProvider` on GHC > `9.0.2`: 114 | Newer GHC uses a different error message format 115 | * `ExtensionProvider`: 116 | - Do not create duplicated QuickFix actions 117 | - Switch to `DataKinds` extension in test 118 | * Update all dependencies to the latest versions 119 | ### Added 120 | * Extend matrix build with various supported GHC versions 121 | 122 | ## [0.10.5] - 2021-03-31 123 | ### Fixed 124 | * Bump dependencies to fix security vulnerability 125 | 126 | ## [0.10.4] - 2020-09-26 127 | ### Fixed 128 | * Stop putting images into *.vsix package 129 | This fix reduces in ~20 times (oops...) and `README.md` points to Github-hosted files anyway 130 | * Better isolation for tests: 131 | Set custom `--user-data-dir` so the test would not interfere with the VSCode installed locally 132 | 133 | ## [0.10.3] - 2020-09-25 134 | ### Fixed 135 | * Fix extension publishing via `npm run publish` 136 | 137 | ## [0.10.2] - 2020-09-22 138 | ### Added 139 | * Github actions build: 140 | - Matrix builds on multiple platforms 141 | - Publish to Marketplace on version tag push 142 | ### Fixed 143 | * Test coverage 144 | * CHANGELOG.md warnings 145 | ### Removed 146 | * Travis-ci build 147 | 148 | ## [0.10.1] - 2020-09-17 149 | ### Fixed 150 | * Bump `lodash` dependency version to fix security vulnerabilitiy 151 | 152 | ## [0.10.0] - 2020-05-09 153 | ### Added 154 | * `haskutil.organiseImportsOnInsert` configuration option 155 | ### Fixed 156 | * Run multiple Hoogle requests in parallel 157 | 158 | ## [0.9.2] - 2020-05-04 159 | ### Added 160 | * Test coverage 161 | 162 | ## [0.9.1] - 2020-05-04 163 | ### Fixed 164 | * Exclude unrelated files from .vsix package 165 | * CHANGLELOG (add missing contributer) 166 | 167 | ## [0.9.0] - 2020-05-04 168 | ### Added 169 | * Add [ghcide](https://marketplace.visualstudio.com/items?itemName=DigitalAssetHoldingsLLC.ghcide) as an option for base extension populating PROBLEMS 170 | ([#29](https://github.com/EduardSergeev/vscode-haskutil/pull/29) - thanks to [ArturGajowy](https://github.com/ArturGajowy) for contribution) 171 | 172 | ## [0.8.1] - 2020-04-20 173 | ### Fixed 174 | * Untitled files re-opening themselves after closing 175 | ([#28](https://github.com/EduardSergeev/vscode-haskutil/pull/28) - thanks to [dramforever](https://github.com/dramforever) for contribution) 176 | * Haskutil complaining about non-Haskell files with unorganized imports 177 | * Organize import can produce code that the extension considers unorganized ([#26](https://github.com/EduardSergeev/vscode-haskutil/issues/26)) 178 | 179 | ## [0.8.0] - 2020-04-13 180 | ### Added 181 | * Align remaining imports when removing unused imports 182 | ### Fixed 183 | * Adjust Unused imports diagnostic range when imports are edited/moved 184 | 185 | ## [0.7.0] - 2020-04-12 186 | ### Added 187 | * Add `Remove unused imports` feature 188 | 189 | ## [0.6.0] - 2020-04-10 190 | ### Added 191 | * Add [ghcid](https://marketplace.visualstudio.com/items?itemName=ndmitchell.haskell-ghcid) as an option for base extension populating `PROBLEMS` 192 | 193 | ## [0.5.3] - 2020-04-09 194 | ### Fixed 195 | * Background color of Marketplace banner 196 | 197 | ## [0.5.2] - 2020-04-09 198 | ### Fixed 199 | * Fix detection of missing qualified import on Linux 200 | 201 | ## [0.5.1] - 2020-04-09 202 | ### Fixed 203 | * Fix wildcard replacement under GHC 8.8 204 | * Better handling of wildcards 205 | (nested parenthesis and `()` as replacement) 206 | 207 | ## [0.5.0] - 2020-04-01 208 | ### Added 209 | * Settings for turning on/off individual features 210 | ### Fixed 211 | * Fix 'Not in scope' error detection (different platforms) 212 | ([#16](https://github.com/EduardSergeev/vscode-haskutil/pull/16) - thanks to [serras](https://github.com/serras) for contribution) 213 | * Make LANGUAGE pragma handling case insensitive 214 | 215 | ## [0.4.5] - 2019-07-26 216 | ### Fixed 217 | * Fix type wildcard error detection on Linux 218 | 219 | ## [0.4.4] - 2019-07-26 220 | ### Fixed 221 | * Fix type hole error detection on Linux 222 | 223 | ## [0.4.3] - 2019-05-21 224 | ### Fixed 225 | * Upgrade dependent packages to fix security vulnerabilities 226 | 227 | ## [0.4.2] - 2019-03-27 228 | ### Fixed 229 | * Rolling back version 0.4.1. This version is identical to 0.4.0 230 | 231 | ## [0.4.0] - 2019-03-26 232 | ### Added 233 | * Replace type wildcard with GHC suggestion 234 | 235 | ## [0.3.1] - 2019-03-25 236 | ### Changed 237 | * Minor fix: remove redundant workaround 238 | 239 | ## [0.3.0] - 2019-03-24 240 | ### Added 241 | * Fill typed hole with GHC suggestion 242 | 243 | ## [0.2.2] - 2019-03-10 244 | ### Fixed 245 | * Bump dependencies to fix security warning in `node.extend` 246 | 247 | ## [0.2.1] - 2018-09-30 248 | ### Added 249 | * Check if dependent extension is installed 250 | 251 | ## [0.2.0] - 2018-09-30 252 | ### Added 253 | * Organize LANGUAGE extensions (split, align and sort) 254 | * Configuration of the padding of `import` in `qualified` area 255 | (should we always pad with 9 spaces even if there is no `qualified` import) 256 | 257 | ## [0.1.0] - 2018-09-20 258 | ### Changed 259 | * Organize imports (sort and align) 260 | * Add top-level signature (documented) 261 | 262 | ## [0.0.4] - 2018-09-19 263 | ### Added 264 | * Sort `import` statements 265 | * Add top-level signature (undocumented) 266 | 267 | ## [0.0.3] - 2018-09-15 268 | ### Added 269 | * Additional supported `LANGUAGE` extensions 270 | ### Changed 271 | * Removed dependency on `Haskero` 272 | ### Fixed 273 | * Broken ordering on `LANGUAGE` pragma insert 274 | 275 | ## [0.0.2] - 2018-09-14 276 | ### Fixed 277 | * Insert imports after all `LANGUAGE` pragmas 278 | * Show only exactly matched suggestion in `Add import` 279 | (previously `runErrorT`'s module would be included for `runE` variable) 280 | 281 | ## [0.0.1] - 2018-09-13 282 | ### Added 283 | * Add missing `import` statement 284 | * Add missing `LANGUAGE` pragma 285 | --------------------------------------------------------------------------------