Skip to content

Commit 9cc8c62

Browse files
VenInffendorChristophHochrainerVeryMilkyJoe
authored
Cabal go to module's definition (#4380)
If you click go-to definition on the field under `exposed-module` or `other-module` it will open the file where this module was defined. The go-to definition function compares the highlighted text with modules in the cabal file. If there is a match, it takes the respective build target and tries to fetch their `hsSourceDirs` from the `PackageDescription`. (by looking at all `buildInfos` with matching names). After finding them, it constructs a path using directory where the cabal file is located, the info from `hsSourceDirs` and a name of the module converted to a path. If the file exists it returns the `Definition` with the acquired location. --------- Co-authored-by: fendor <[email protected]> Co-authored-by: Chrizzl <[email protected]> Co-authored-by: VeryMilkyJoe <[email protected]>
1 parent 2253752 commit 9cc8c62

File tree

17 files changed

+585
-101
lines changed

17 files changed

+585
-101
lines changed

haskell-language-server.cabal

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ library hls-cabal-plugin
242242
Ide.Plugin.Cabal.Completion.Completions
243243
Ide.Plugin.Cabal.Completion.Data
244244
Ide.Plugin.Cabal.Completion.Types
245+
Ide.Plugin.Cabal.Definition
245246
Ide.Plugin.Cabal.FieldSuggest
246247
Ide.Plugin.Cabal.LicenseSuggest
247248
Ide.Plugin.Cabal.CabalAdd
@@ -287,11 +288,12 @@ test-suite hls-cabal-plugin-tests
287288
hs-source-dirs: plugins/hls-cabal-plugin/test
288289
main-is: Main.hs
289290
other-modules:
291+
CabalAdd
290292
Completer
291293
Context
292-
Utils
294+
Definition
293295
Outline
294-
CabalAdd
296+
Utils
295297
build-depends:
296298
, base
297299
, bytestring

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ import qualified Data.ByteString as BS
1717
import Data.Hashable
1818
import Data.HashMap.Strict (HashMap)
1919
import qualified Data.HashMap.Strict as HashMap
20-
import Data.List (find)
2120
import qualified Data.List.NonEmpty as NE
2221
import qualified Data.Maybe as Maybe
2322
import qualified Data.Text as T
2423
import qualified Data.Text.Encoding as Encoding
2524
import Data.Typeable
2625
import Development.IDE as D
27-
import Development.IDE.Core.PluginUtils
2826
import Development.IDE.Core.Shake (restartShakeSession)
2927
import qualified Development.IDE.Core.Shake as Shake
3028
import Development.IDE.Graph (Key, alwaysRerun)
@@ -33,20 +31,19 @@ import Development.IDE.Types.Shake (toKey)
3331
import qualified Distribution.Fields as Syntax
3432
import qualified Distribution.Parsec.Position as Syntax
3533
import GHC.Generics
36-
import Ide.Plugin.Cabal.Completion.CabalFields as CabalFields
3734
import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes
3835
import qualified Ide.Plugin.Cabal.Completion.Completions as Completions
3936
import Ide.Plugin.Cabal.Completion.Types (ParseCabalCommonSections (ParseCabalCommonSections),
4037
ParseCabalFields (..),
4138
ParseCabalFile (..))
4239
import qualified Ide.Plugin.Cabal.Completion.Types as Types
40+
import Ide.Plugin.Cabal.Definition (gotoDefinition)
4341
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
4442
import qualified Ide.Plugin.Cabal.FieldSuggest as FieldSuggest
4543
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
4644
import Ide.Plugin.Cabal.Orphans ()
4745
import Ide.Plugin.Cabal.Outline
4846
import qualified Ide.Plugin.Cabal.Parse as Parse
49-
import Ide.Plugin.Error
5047
import Ide.Types
5148
import qualified Language.LSP.Protocol.Lens as JL
5249
import qualified Language.LSP.Protocol.Message as LSP
@@ -305,32 +302,6 @@ fieldSuggestCodeAction recorder ide _ (CodeActionParams _ _ (TextDocumentIdentif
305302
let completionTexts = fmap (^. JL.label) completions
306303
pure $ FieldSuggest.fieldErrorAction uri fieldName completionTexts _range
307304

308-
-- | CodeActions for going to definitions.
309-
--
310-
-- Provides a CodeAction for going to a definition when clicking on an identifier.
311-
-- The definition is found by traversing the sections and comparing their name to
312-
-- the clicked identifier.
313-
--
314-
-- TODO: Support more definitions than sections.
315-
gotoDefinition :: PluginMethodHandler IdeState LSP.Method_TextDocumentDefinition
316-
gotoDefinition ideState _ msgParam = do
317-
nfp <- getNormalizedFilePathE uri
318-
cabalFields <- runActionE "cabal-plugin.commonSections" ideState $ useE ParseCabalFields nfp
319-
case CabalFields.findTextWord cursor cabalFields of
320-
Nothing ->
321-
pure $ InR $ InR Null
322-
Just cursorText -> do
323-
commonSections <- runActionE "cabal-plugin.commonSections" ideState $ useE ParseCabalCommonSections nfp
324-
case find (isSectionArgName cursorText) commonSections of
325-
Nothing ->
326-
pure $ InR $ InR Null
327-
Just commonSection -> do
328-
pure $ InL $ Definition $ InL $ Location uri $ CabalFields.getFieldLSPRange commonSection
329-
where
330-
cursor = Types.lspPositionToCabalPosition (msgParam ^. JL.position)
331-
uri = msgParam ^. JL.textDocument . JL.uri
332-
isSectionArgName name (Syntax.Section _ sectionArgName _) = name == CabalFields.onelineSectionArgs sectionArgName
333-
isSectionArgName _ _ = False
334305

335306
cabalAddCodeAction :: PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction
336307
cabalAddCodeAction state plId (CodeActionParams _ _ (TextDocumentIdentifier uri) _ CodeActionContext{_diagnostics=diags}) = do

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
module Ide.Plugin.Cabal.Completion.CabalFields
2-
( findStanzaForColumn,
3-
findFieldSection,
4-
findTextWord,
5-
findFieldLine,
6-
getOptionalSectionName,
7-
getAnnotation,
8-
getFieldName,
9-
onelineSectionArgs,
10-
getFieldEndPosition,
11-
getSectionArgEndPosition,
12-
getNameEndPosition,
13-
getFieldLineEndPosition,
14-
getFieldLSPRange
15-
) where
2+
( findStanzaForColumn
3+
, getModulesNames
4+
, getFieldLSPRange
5+
, findFieldSection
6+
, findTextWord
7+
, findFieldLine
8+
, getOptionalSectionName
9+
, getAnnotation
10+
, getFieldName
11+
, onelineSectionArgs
12+
, getFieldEndPosition
13+
, getSectionArgEndPosition
14+
, getNameEndPosition
15+
, getFieldLineEndPosition
16+
)
17+
where
1618

1719
import qualified Data.ByteString as BS
1820
import Data.List (find)
21+
import Data.List.Extra (groupSort)
1922
import Data.List.NonEmpty (NonEmpty)
2023
import qualified Data.List.NonEmpty as NE
2124
import qualified Data.Text as T
2225
import qualified Data.Text.Encoding as T
26+
import Data.Tuple (swap)
2327
import qualified Distribution.Fields as Syntax
2428
import qualified Distribution.Parsec.Position as Syntax
2529
import Ide.Plugin.Cabal.Completion.Types
@@ -138,6 +142,9 @@ getFieldName :: Syntax.Field ann -> FieldName
138142
getFieldName (Syntax.Field (Syntax.Name _ fn) _) = T.decodeUtf8 fn
139143
getFieldName (Syntax.Section (Syntax.Name _ fn) _ _) = T.decodeUtf8 fn
140144

145+
getFieldLineName :: Syntax.FieldLine ann -> FieldName
146+
getFieldLineName (Syntax.FieldLine _ fn) = T.decodeUtf8 fn
147+
141148
-- | Returns the name of a section if it has a name.
142149
--
143150
-- This assumes that the given section args belong to named stanza
@@ -148,6 +155,107 @@ getOptionalSectionName (x:xs) = case x of
148155
Syntax.SecArgName _ name -> Just (T.decodeUtf8 name)
149156
_ -> getOptionalSectionName xs
150157

158+
type BuildTargetName = T.Text
159+
type ModuleName = T.Text
160+
161+
-- | Given a cabal AST returns pairs of all respective target names
162+
-- and the module name bound to them. If a target is a main library gives
163+
-- @Nothing@, otherwise @Just target-name@
164+
--
165+
-- Examples of input cabal files and the outputs:
166+
--
167+
-- * Target is a main library module:
168+
--
169+
-- > library
170+
-- > exposed-modules:
171+
-- > MyLib
172+
--
173+
-- * @getModulesNames@ output:
174+
--
175+
-- > [([Nothing], "MyLib")]
176+
--
177+
-- * Same module names in different targets:
178+
--
179+
-- > test-suite first-target
180+
-- > other-modules:
181+
-- > Config
182+
-- > test-suite second-target
183+
-- > other-modules:
184+
-- > Config
185+
--
186+
-- * @getModulesNames@ output:
187+
--
188+
-- > [([Just "first-target", Just "second-target"], "Config")]
189+
getModulesNames :: [Syntax.Field any] -> [([Maybe BuildTargetName], ModuleName)]
190+
getModulesNames fields = map swap $ groupSort rawModuleTargetPairs
191+
where
192+
rawModuleTargetPairs = concatMap getSectionModuleNames sections
193+
sections = getSectionsWithModules fields
194+
195+
getSectionModuleNames :: Syntax.Field any -> [(ModuleName, Maybe BuildTargetName)]
196+
getSectionModuleNames (Syntax.Section _ secArgs fields) = map (, getArgsName secArgs) $ concatMap getFieldModuleNames fields
197+
getSectionModuleNames _ = []
198+
199+
getArgsName [Syntax.SecArgName _ name] = Just $ T.decodeUtf8 name
200+
getArgsName _ = Nothing -- Can be only a main library, that has no name
201+
-- since it's impossible to have multiple names for a build target
202+
203+
getFieldModuleNames field@(Syntax.Field _ modules) = if getFieldName field == T.pack "exposed-modules" ||
204+
getFieldName field == T.pack "other-modules"
205+
then map getFieldLineName modules
206+
else []
207+
getFieldModuleNames _ = []
208+
209+
-- | Trims a given cabal AST leaving only targets and their
210+
-- @exposed-modules@ and @other-modules@ sections.
211+
--
212+
-- For example:
213+
--
214+
-- * Given a cabal file like this:
215+
--
216+
-- > library
217+
-- > import: extra
218+
-- > hs-source-dirs: source/directory
219+
-- > ...
220+
-- > exposed-modules:
221+
-- > Important.Exposed.Module
222+
-- > other-modules:
223+
-- > Important.Other.Module
224+
-- >
225+
-- > test-suite tests
226+
-- > type: type
227+
-- > build-tool-depends: tool
228+
-- > other-modules:
229+
-- > Important.Other.Module
230+
--
231+
-- * @getSectionsWithModules@ gives output:
232+
--
233+
-- > library
234+
-- > exposed-modules:
235+
-- > Important.Exposed.Module
236+
-- > other-modules:
237+
-- > Important.Other.Module
238+
-- > test-suite tests
239+
-- > other-modules:
240+
-- > Important.Other.Module
241+
getSectionsWithModules :: [Syntax.Field any] -> [Syntax.Field any]
242+
getSectionsWithModules fields = concatMap go fields
243+
where
244+
go :: Syntax.Field any -> [Syntax.Field any]
245+
go (Syntax.Field _ _) = []
246+
go section@(Syntax.Section _ _ fields) = concatMap onlySectionsWithModules (section:fields)
247+
248+
onlySectionsWithModules :: Syntax.Field any -> [Syntax.Field any]
249+
onlySectionsWithModules (Syntax.Field _ _) = []
250+
onlySectionsWithModules (Syntax.Section name secArgs fields)
251+
| (not . null) newFields = [Syntax.Section name secArgs newFields]
252+
| otherwise = []
253+
where newFields = filter subfieldHasModule fields
254+
255+
subfieldHasModule :: Syntax.Field any -> Bool
256+
subfieldHasModule field@(Syntax.Field _ _) = getFieldName field == T.pack "exposed-modules" ||
257+
getFieldName field == T.pack "other-modules"
258+
subfieldHasModule (Syntax.Section _ _ _) = False
151259

152260
-- | Makes a single text line out of multiple
153261
-- @SectionArg@s. Allows to display conditions,
@@ -165,7 +273,6 @@ onelineSectionArgs sectionArgs = joinedName
165273
getName (Syntax.SecArgStr _ quotedString) = T.decodeUtf8 quotedString
166274
getName (Syntax.SecArgOther _ string) = T.decodeUtf8 string
167275

168-
169276
-- | Returns the end position of a provided field
170277
getFieldEndPosition :: Syntax.Field Syntax.Position -> Syntax.Position
171278
getFieldEndPosition (Syntax.Field name []) = getNameEndPosition name

0 commit comments

Comments
 (0)