Skip to content

Commit 42bcf92

Browse files
sloorushKobayashi
and
Kobayashi
authored
Feat: Folding Ranges (#3058)
* save some progress: add basic starter code for folding ranges * save some progress: add function to traverse through coderange and form folding ranges * save some progress: add parsing of folding ranges * fix: maybe issue with foldingRanges * add: generate folding ranges from coderange * add: plugin request method instance for folding ranges * ref: alter function and var names * post review: cleanup crk to frk & fix typo * fix: find folding ranges function * format: run formatter and add comments * fix: return all response results of folding range request * Revert "format: run formatter and add comments" This reverts commit e6a2b5c. * add: removed comments after revert * fix: formatting * docs: add folding range to features section and cabal file * refactor: use destructuring for createFoldingRange function and use characters * test: add basic unit test for findFoldingRanges function * test: add tests for children and code kind * test: add more test cases * test: add test for createFoldingRange * test: add integration test for folding ranges * fix: duplicate start line foldingranges and remove single line foldingranges * refactor: duplicate folding range functionality * fix: formatting in code range plugin * added more descriptive comments and encorporate code review suggestions * revert: automatic formatting for selection range test case file * fix: ignoring children if root fails to provide folding ranges * remove: redundant match on crkToFrk * revert: filtering same line foldings and multiple foldings on the same line as it can be handled by clients * revert: formatting change to selection range test file * fix: entire file folding because of root node Co-authored-by: Kobayashi <contact@zelinf.net>
1 parent bd1d0a1 commit 42bcf92

File tree

11 files changed

+265
-27
lines changed

11 files changed

+265
-27
lines changed

docs/features.md

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This table gives a summary of the features that HLS supports.
44
Many of these are standard LSP features, but a lot of special features are provided as [code actions](#code-actions) and [code lenses](#code-lenses).
55

66
| Feature | [LSP method](./what-is-hls.md#lsp-terminology) |
7-
|-----------------------------------------------------|---------------------------------------------------------------------------------------------------|
7+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
88
| [Diagnostics](#diagnostics) | `textDocument/publishDiagnostics` |
99
| [Hovers](#hovers) | `textDocument/hover` |
1010
| [Jump to definition](#jump-to-definition) | `textDocument/definition` |
@@ -100,7 +100,7 @@ Completions for language pragmas.
100100
Format your code with various Haskell code formatters.
101101

102102
| Formatter | Provided by |
103-
|-----------------|------------------------------|
103+
| --------------- | ---------------------------- |
104104
| Brittany | `hls-brittany-plugin` |
105105
| Floskell | `hls-floskell-plugin` |
106106
| Fourmolu | `hls-fourmolu-plugin` |
@@ -261,6 +261,7 @@ Change/Update a type signature to match implementation.
261261
Status: Until GHC 9.4, the implementation is ad-hoc and relies on GHC error messages to create a new signature. Not all GHC error messages are supported.
262262

263263
Known Limitations:
264+
264265
- Not all GHC error messages are supported
265266
- Top-level and Function-local bindings with the same names can cause issues, such as incorrect signature changes or no code actions available.
266267

@@ -337,6 +338,16 @@ support.
337338

338339
![Selection range demo](https://user-images.githubusercontent.com/16440269/177240833-7dc8fe39-b446-477e-b5b1-7fc303608d4f.gif)
339340

341+
## Folding range
342+
343+
Provided by: `hls-code-range-plugin`
344+
345+
Provides haskell specific
346+
[Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding)
347+
support.
348+
349+
![Folding range demo](https://user-images.githubusercontent.com/54478821/184468510-7c0d5182-c684-48ef-9b39-3866dc2309df.gif)
350+
340351
## Rename
341352

342353
Provided by: `hls-rename-plugin`
@@ -354,15 +365,14 @@ Known limitations:
354365
The following features are supported by the LSP specification but not implemented in HLS.
355366
Contributions welcome!
356367

357-
| Feature | Status | [LSP method](./what-is-hls.md#lsp-terminology) |
358-
|------------------------|------------------------------------------------------------------------------------------|-----------------------------------------------------|
359-
| Signature help | Unimplemented | `textDocument/signatureHelp` |
360-
| Jump to declaration | Unclear if useful | `textDocument/declaration` |
361-
| Jump to implementation | Unclear if useful | `textDocument/implementation` |
362-
| Folding | Unimplemented | `textDocument/foldingRange` |
363-
| Semantic tokens | Unimplemented | `textDocument/semanticTokens` |
364-
| Linked editing | Unimplemented | `textDocument/linkedEditingRange` |
365-
| Document links | Unimplemented | `textDocument/documentLink` |
366-
| Document color | Unclear if useful | `textDocument/documentColor` |
367-
| Color presentation | Unclear if useful | `textDocument/colorPresentation` |
368-
| Monikers | Unclear if useful | `textDocument/moniker` |
368+
| Feature | Status | [LSP method](./what-is-hls.md#lsp-terminology) |
369+
| ---------------------- | ----------------- | ---------------------------------------------- |
370+
| Signature help | Unimplemented | `textDocument/signatureHelp` |
371+
| Jump to declaration | Unclear if useful | `textDocument/declaration` |
372+
| Jump to implementation | Unclear if useful | `textDocument/implementation` |
373+
| Semantic tokens | Unimplemented | `textDocument/semanticTokens` |
374+
| Linked editing | Unimplemented | `textDocument/linkedEditingRange` |
375+
| Document links | Unimplemented | `textDocument/documentLink` |
376+
| Document color | Unclear if useful | `textDocument/documentColor` |
377+
| Color presentation | Unclear if useful | `textDocument/colorPresentation` |
378+
| Monikers | Unclear if useful | `textDocument/moniker` |

hls-plugin-api/src/Ide/Plugin/Config.hs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ data PluginConfig =
110110
, plcCompletionOn :: !Bool
111111
, plcRenameOn :: !Bool
112112
, plcSelectionRangeOn :: !Bool
113+
, plcFoldingRangeOn :: !Bool
113114
, plcConfig :: !A.Object
114115
} deriving (Show,Eq)
115116

@@ -125,11 +126,12 @@ instance Default PluginConfig where
125126
, plcCompletionOn = True
126127
, plcRenameOn = True
127128
, plcSelectionRangeOn = True
129+
, plcFoldingRangeOn = True
128130
, plcConfig = mempty
129131
}
130132

131133
instance A.ToJSON PluginConfig where
132-
toJSON (PluginConfig g ch ca cl d h s c rn sr cfg) = r
134+
toJSON (PluginConfig g ch ca cl d h s c rn sr fr cfg) = r
133135
where
134136
r = object [ "globalOn" .= g
135137
, "callHierarchyOn" .= ch
@@ -141,6 +143,7 @@ instance A.ToJSON PluginConfig where
141143
, "completionOn" .= c
142144
, "renameOn" .= rn
143145
, "selectionRangeOn" .= sr
146+
, "foldingRangeOn" .= fr
144147
, "config" .= cfg
145148
]
146149

@@ -156,6 +159,7 @@ instance A.FromJSON PluginConfig where
156159
<*> o .:? "completionOn" .!= plcCompletionOn def
157160
<*> o .:? "renameOn" .!= plcRenameOn def
158161
<*> o .:? "selectionRangeOn" .!= plcSelectionRangeOn def
162+
<*> o .:? "foldingRangeOn" .!= plcFoldingRangeOn def
159163
<*> o .:? "config" .!= plcConfig def
160164

161165
-- ---------------------------------------------------------------------

hls-plugin-api/src/Ide/Types.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,13 @@ instance PluginMethod Request TextDocumentSelectionRange where
429429
uri = msgParams ^. J.textDocument . J.uri
430430
pid = pluginId pluginDesc
431431

432+
instance PluginMethod Request TextDocumentFoldingRange where
433+
pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc
434+
&& pluginEnabledConfig plcFoldingRangeOn pid conf
435+
where
436+
uri = msgParams ^. J.textDocument . J.uri
437+
pid = pluginId pluginDesc
438+
432439
instance PluginMethod Request CallHierarchyIncomingCalls where
433440
-- This method has no URI parameter, thus no call to 'pluginResponsible'
434441
pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn pid conf
@@ -529,6 +536,9 @@ instance PluginRequestMethod TextDocumentPrepareCallHierarchy where
529536
instance PluginRequestMethod TextDocumentSelectionRange where
530537
combineResponses _ _ _ _ (x :| _) = x
531538

539+
instance PluginRequestMethod TextDocumentFoldingRange where
540+
combineResponses _ _ _ _ x = sconcat x
541+
532542
instance PluginRequestMethod CallHierarchyIncomingCalls where
533543

534544
instance PluginRequestMethod CallHierarchyOutgoingCalls where

plugins/hls-code-range-plugin/hls-code-range-plugin.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ cabal-version: 2.4
22
name: hls-code-range-plugin
33
version: 1.0.0.0
44
synopsis:
5-
HLS Plugin to support smart selection range
5+
HLS Plugin to support smart selection range and Folding range
66

77
description:
88
Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>

plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange.hs

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
{-# LANGUAGE OverloadedStrings #-}
2-
{-# LANGUAGE RecordWildCards #-}
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE RecordWildCards #-}
3+
{-# LANGUAGE ScopedTypeVariables #-}
34

45
module Ide.Plugin.CodeRange (
56
descriptor
67
, Log
78

89
-- * Internal
910
, findPosition
11+
, findFoldingRanges
12+
, createFoldingRange
1013
) where
1114

1215
import Control.Monad.Except (ExceptT (ExceptT),
@@ -33,7 +36,7 @@ import Development.IDE.Core.PositionMapping (PositionMapping,
3336
import Development.IDE.Types.Logger (Pretty (..))
3437
import Ide.Plugin.CodeRange.Rules (CodeRange (..),
3538
GetCodeRange (..),
36-
codeRangeRule)
39+
codeRangeRule, crkToFrk)
3740
import qualified Ide.Plugin.CodeRange.Rules as Rules (Log)
3841
import Ide.PluginUtils (pluginResponse,
3942
positionInRange)
@@ -42,12 +45,14 @@ import Ide.Types (PluginDescriptor (pluginH
4245
defaultPluginDescriptor,
4346
mkPluginHandler)
4447
import Language.LSP.Server (LspM)
45-
import Language.LSP.Types (List (List),
48+
import Language.LSP.Types (FoldingRange (..),
49+
FoldingRangeParams (..),
50+
List (List),
4651
NormalizedFilePath,
4752
Position (..),
4853
Range (_start),
4954
ResponseError,
50-
SMethod (STextDocumentSelectionRange),
55+
SMethod (STextDocumentFoldingRange, STextDocumentSelectionRange),
5156
SelectionRange (..),
5257
SelectionRangeParams (..),
5358
TextDocumentIdentifier (TextDocumentIdentifier),
@@ -57,8 +62,7 @@ import Prelude hiding (log, span)
5762
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
5863
descriptor recorder plId = (defaultPluginDescriptor plId)
5964
{ pluginHandlers = mkPluginHandler STextDocumentSelectionRange selectionRangeHandler
60-
-- TODO @sloorush add folding range
61-
-- <> mkPluginHandler STextDocumentFoldingRange foldingRangeHandler
65+
<> mkPluginHandler STextDocumentFoldingRange foldingRangeHandler
6266
, pluginRules = codeRangeRule (cmapWithPrio LogRules recorder)
6367
}
6468

@@ -68,6 +72,25 @@ instance Pretty Log where
6872
pretty log = case log of
6973
LogRules codeRangeLog -> pretty codeRangeLog
7074

75+
foldingRangeHandler :: IdeState -> PluginId -> FoldingRangeParams -> LspM c (Either ResponseError (List FoldingRange))
76+
foldingRangeHandler ide _ FoldingRangeParams{..} = do
77+
pluginResponse $ do
78+
filePath <- ExceptT . pure . maybeToEither "fail to convert uri to file path" $
79+
toNormalizedFilePath' <$> uriToFilePath' uri
80+
foldingRanges <- ExceptT . liftIO . runIdeAction "FoldingRange" (shakeExtras ide) . runExceptT $
81+
getFoldingRanges filePath
82+
pure . List $ foldingRanges
83+
where
84+
uri :: Uri
85+
TextDocumentIdentifier uri = _textDocument
86+
87+
getFoldingRanges :: NormalizedFilePath -> ExceptT String IdeAction [FoldingRange]
88+
getFoldingRanges file = do
89+
(codeRange, _) <- maybeToExceptT "fail to get code range" $ useE GetCodeRange file
90+
91+
-- removing first node because it folds the entire file
92+
pure $ drop 1 $ findFoldingRanges codeRange
93+
7194
selectionRangeHandler :: IdeState -> PluginId -> SelectionRangeParams -> LspM c (Either ResponseError (List SelectionRange))
7295
selectionRangeHandler ide _ SelectionRangeParams{..} = do
7396
pluginResponse $ do
@@ -126,6 +149,39 @@ findPosition pos root = go Nothing root
126149
startOfRight <- _start . _codeRange_range <$> V.headM right
127150
if pos < startOfRight then binarySearchPos left else binarySearchPos right
128151

152+
-- | Traverses through the code range and it children to a folding ranges.
153+
--
154+
-- It starts with the root node, converts that into a folding range then moves towards the children.
155+
-- It converts each child of each root node and parses it to folding range and moves to its children.
156+
--
157+
-- Two cases to that are assumed to be taken care on the client side are:
158+
--
159+
-- 1. When a folding range starts and ends on the same line, it is upto the client if it wants to
160+
-- fold a single line folding or not.
161+
--
162+
-- 2. As we are converting nodes of the ast into folding ranges, there are multiple nodes starting from a single line.
163+
-- A single line of code doesn't mean a single node in AST, so this function removes all the nodes that have a duplicate
164+
-- start line, ie. they start from the same line.
165+
-- Eg. A multi-line function that also has a multi-line if statement starting from the same line should have the folding
166+
-- according to the function.
167+
--
168+
-- We think the client can handle this, if not we could change to remove these in future
169+
--
170+
-- Discussion reference: https://github.com/haskell/haskell-language-server/pull/3058#discussion_r973737211
171+
findFoldingRanges :: CodeRange -> [FoldingRange]
172+
findFoldingRanges r@(CodeRange _ children _) =
173+
let frChildren :: [FoldingRange] = concat $ V.toList $ fmap findFoldingRanges children
174+
in case createFoldingRange r of
175+
Just x -> x:frChildren
176+
Nothing -> frChildren
177+
178+
-- | Parses code range to folding range
179+
createFoldingRange :: CodeRange -> Maybe FoldingRange
180+
createFoldingRange (CodeRange (Range (Position lineStart charStart) (Position lineEnd charEnd)) _ ck) = do
181+
-- Type conversion of codeRangeKind to FoldingRangeKind
182+
let frk = crkToFrk ck
183+
Just (FoldingRange lineStart (Just charStart) lineEnd (Just charEnd) (Just frk))
184+
129185
-- | Likes 'toCurrentPosition', but works on 'SelectionRange'
130186
toCurrentSelectionRange :: PositionMapping -> SelectionRange -> Maybe SelectionRange
131187
toCurrentSelectionRange positionMapping SelectionRange{..} = do

plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/Rules.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module Ide.Plugin.CodeRange.Rules
2121
-- * Internal
2222
, removeInterleaving
2323
, simplify
24+
, crkToFrk
2425
) where
2526

2627
import Control.DeepSeq (NFData)
@@ -52,6 +53,7 @@ import Ide.Plugin.CodeRange.ASTPreProcess (CustomNodeType (..),
5253
PreProcessEnv (..),
5354
isCustomNode,
5455
preProcessAST)
56+
import Language.LSP.Types (FoldingRangeKind (FoldingRangeComment, FoldingRangeImports, FoldingRangeRegion))
5557
import Language.LSP.Types.Lens (HasEnd (end),
5658
HasStart (start))
5759
import Prelude hiding (log)
@@ -89,7 +91,7 @@ data CodeRangeKind =
8991
| CodeKindImports
9092
-- | a comment
9193
| CodeKindComment
92-
deriving (Show, Generic, NFData)
94+
deriving (Show, Eq, Generic, NFData)
9395

9496
Lens.makeLenses ''CodeRange
9597

@@ -189,3 +191,10 @@ handleError recorder action' = do
189191
logWith recorder Error msg
190192
pure $ toIdeResult (Left [])
191193
Right value -> pure $ toIdeResult (Right value)
194+
195+
-- | Maps type CodeRangeKind to FoldingRangeKind
196+
crkToFrk :: CodeRangeKind -> FoldingRangeKind
197+
crkToFrk crk = case crk of
198+
CodeKindComment -> FoldingRangeComment
199+
CodeKindImports -> FoldingRangeImports
200+
CodeKindRegion -> FoldingRangeRegion

0 commit comments

Comments
 (0)