diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs index 83e809fb0f..fd052f5c13 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs @@ -34,7 +34,7 @@ contextToCompleter :: Context -> Completer contextToCompleter (TopLevel, None) = snippetCompleter <> ( constantCompleter $ - Map.keys (cabalVersionKeyword <> cabalKeywords) ++ Map.keys stanzaKeywordMap + Map.keys cabalKeywords ++ Map.keys stanzaKeywordMap ) -- if we are in a keyword context in the top level, -- we look up that keyword in the top level context and can complete its possible values @@ -55,6 +55,11 @@ contextToCompleter (Stanza s _, KeyWord kw) = Just m -> case Map.lookup kw m of Nothing -> errorNoopCompleter (LogUnknownKeyWordInContextError kw) Just l -> l +contextToCompleter (FirstLine, None) = constantCompleter ["cabal-version:"] +contextToCompleter (FirstLine, KeyWord kw) = + case Map.lookup kw (cabalVersionKeyword <> cabalKeywords) of + Nothing -> errorNoopCompleter (LogUnknownKeyWordInContextError kw) + Just l -> l -- | Takes prefix info about the previously written text -- and a rope (representing a file), returns the corresponding context. @@ -135,10 +140,13 @@ findCursorContext :: Context findCursorContext cursor parentHistory prefixText fields = case findFieldSection cursor fields of - Nothing -> (snd $ NE.head parentHistory, None) + Nothing + | cursorRow == 1 -> (FirstLine, None) + | otherwise -> (snd $ NE.head parentHistory, None) -- We found the most likely section. Now, are we starting a new section or are we completing an existing one? Just field@(Syntax.Field _ _) -> classifyFieldContext parentHistory cursor field Just section@(Syntax.Section _ args sectionFields) + | cursorRow == 1 -> (FirstLine, None) | inSameLineAsSectionName section -> (stanzaCtx, None) -- TODO: test whether keyword in same line is parsed correctly | getFieldName section `elem` conditionalKeywords -> findCursorContext cursor parentHistory prefixText sectionFields -- Ignore if conditionals, they are not real sections | otherwise -> @@ -150,6 +158,8 @@ findCursorContext cursor parentHistory prefixText fields = stanzaCtx = snd $ NE.head parentHistory conditionalKeywords = ["if", "elif", "else"] + cursorRow = Syntax.positionRow cursor + -- | Finds the cursor's context, where the cursor is already found to be in a specific field -- -- Due to the way the field context is recognised for incomplete cabal files, @@ -159,11 +169,11 @@ classifyFieldContext :: NonEmpty (Int, StanzaContext) -> Syntax.Position -> Synt classifyFieldContext ctx cursor field -- the cursor is not indented enough to be within the field -- but still indented enough to be within the stanza - | cursorColumn <= fieldColumn && minIndent <= cursorColumn = (stanzaCtx, None) + | cursorColumn <= fieldColumn && minIndent <= cursorColumn = (firstLineOrStanzaCtx, None) -- the cursor is not in the current stanza's context as it is not indented enough | cursorColumn < minIndent = findStanzaForColumn cursorColumn ctx - | cursorIsInFieldName = (stanzaCtx, None) - | cursorIsBeforeFieldName = (stanzaCtx, None) + | cursorIsInFieldName = (firstLineOrStanzaCtx, None) + | cursorIsBeforeFieldName = (firstLineOrStanzaCtx, None) | otherwise = (stanzaCtx, KeyWord (getFieldName field <> ":")) where (minIndent, stanzaCtx) = NE.head ctx @@ -179,3 +189,5 @@ classifyFieldContext ctx cursor field cursorColumn = Syntax.positionCol cursor fieldColumn = Syntax.positionCol (getAnnotation field) + + firstLineOrStanzaCtx = if Syntax.positionRow cursor == 1 then FirstLine else stanzaCtx diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs index 59796afe2b..894fd04700 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs @@ -77,7 +77,9 @@ type Context = (StanzaContext, FieldContext) -- | Context inside a cabal file. -- Used to decide which keywords to suggest. data StanzaContext - = -- | Top level context in a cabal file such as 'author' + = -- | First line in a cabal file. Only cabal-version is valid here + FirstLine + | -- | Top level context in a cabal file such as 'author' TopLevel | -- | Nested context in a cabal file, such as 'library'. -- @@ -88,6 +90,7 @@ data StanzaContext deriving (Eq, Show, Read) instance Pretty StanzaContext where + pretty FirstLine = "FirstLine" pretty TopLevel = "TopLevel" pretty (Stanza t ms) = "Stanza" <+> pretty t <+> (maybe mempty pretty ms) diff --git a/plugins/hls-cabal-plugin/test/Completer.hs b/plugins/hls-cabal-plugin/test/Completer.hs index c050c4a85e..bcd62e750c 100644 --- a/plugins/hls-cabal-plugin/test/Completer.hs +++ b/plugins/hls-cabal-plugin/test/Completer.hs @@ -77,6 +77,31 @@ basicCompleterTests = let complTexts = getTextEditTexts compls liftIO $ assertBool "suggests f2" $ "f2.hs" `elem` complTexts liftIO $ assertBool "does not suggest" $ "Content.hs" `notElem` complTexts + , runCabalTestCaseSession "only suggest cabal-version at start of file" "" $ do + doc <- openDoc "empty.cabal" "cabal" + compls <- getCompletions doc (Position 0 0) + let complTexts = getTextEditTexts compls + liftIO $ complTexts @?= ["cabal-version:"] + , runCabalTestCaseSession "only suggest cabal-version at start of file with other fields" "" $ do + doc <- openDoc "cabal-version-missing.cabal" "cabal" + compls <- getCompletions doc (Position 0 0) + let complTexts = getTextEditTexts compls + liftIO $ complTexts @?= ["cabal-version:"] + , runCabalTestCaseSession "only suggest cabal-version at start of file with some input" "" $ do + doc <- openDoc "start-cabal-version.cabal" "cabal" + compls <- getCompletions doc (Position 0 2) + let complTexts = getTextEditTexts compls + liftIO $ complTexts @?= ["cabal-version:"] + , runCabalTestCaseSession "suggest field value completion in first line for fields other than cabal-version" "" $ do + doc <- openDoc "cabal-version-not-in-first-line.cabal" "cabal" + compls <- getCompletions doc (Position 0 8) + let complTexts = getTextEditTexts compls + liftIO $ complTexts @?= [] + , runCabalTestCaseSession "Don't suggest cabal-version at lines after the first line" "" $ do + doc <- openDoc "cabal-version-missing.cabal" "cabal" + compls <- getCompletions doc (Position 2 0) + let complTexts = getTextEditTexts compls + liftIO . assertBool "cabal-version is not suggested after first line" . not $ "cabal-version:" `elem` complTexts , parameterisedCursorTestM "extensions completion" libraryStanzaData [ \_ actual -> assertBool "suggests FieldSelectors" $ "FieldSelectors" `elem` actual , \_ actual -> assertBool "suggests OverloadedStrings" $ "OverloadedStrings" `elem` actual diff --git a/plugins/hls-cabal-plugin/test/Context.hs b/plugins/hls-cabal-plugin/test/Context.hs index 00d13b08f8..f2272df3f2 100644 --- a/plugins/hls-cabal-plugin/test/Context.hs +++ b/plugins/hls-cabal-plugin/test/Context.hs @@ -13,7 +13,7 @@ import Ide.Plugin.Cabal.Completion.Completer.Paths import Ide.Plugin.Cabal.Completion.Completions import Ide.Plugin.Cabal.Completion.Types (Context, FieldContext (KeyWord, None), - StanzaContext (Stanza, TopLevel)) + StanzaContext (FirstLine, Stanza, TopLevel)) import qualified Ide.Plugin.Cabal.Parse as Parse import Test.Hls import Utils as T @@ -65,7 +65,7 @@ getContextTests = -- for a completely empty file, the context needs to -- be top level without a specified keyword ctx <- callGetContext (Position 0 0) "" "" - ctx @?= (TopLevel, None) + ctx @?= (FirstLine, None) , testCase "Cabal version keyword - no value, no space after :" $ do -- on a file, where the keyword is already written -- the context should still be toplevel but the keyword should be recognized @@ -76,7 +76,7 @@ getContextTests = -- but the cursor is in the middle of the keyword, -- we are not in a keyword context ctx <- callGetContext (Position 0 5) "cabal" "cabal-version:\n" - ctx @?= (TopLevel, None) + ctx @?= (FirstLine, None) , testCase "Cabal version keyword - no value, many spaces" $ do -- on a file, where the "cabal-version:" keyword is already written -- the context should still be top level but the keyword should be recognized @@ -87,7 +87,7 @@ getContextTests = -- has not been written completely, the keyword context -- should still be None ctx <- callGetContext (Position 0 5) "cabal" "cabal" - ctx @?= (TopLevel, None) + ctx @?= (FirstLine, None) , testCase "Cabal version keyword - value partly written" $ do -- in the first line of the file, if the keyword -- has not been written completely, the keyword context @@ -182,7 +182,7 @@ getContextTests = ctx <- callGetContext (Position 8 14) "" multiLineOptsData ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:") , parameterisedCursorTest "Contexts in large testfile" multiPositionTestData - [ (TopLevel, None) + [ (FirstLine, None) , (TopLevel, KeyWord "cabal-version:") , (TopLevel, None) , (TopLevel, KeyWord "description:") diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-version-missing.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-version-missing.cabal new file mode 100644 index 0000000000..534e9dda4e --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-version-missing.cabal @@ -0,0 +1,2 @@ + +name: test-hls diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-version-not-in-first-line.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-version-not-in-first-line.cabal new file mode 100644 index 0000000000..36e20b5a1c --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-version-not-in-first-line.cabal @@ -0,0 +1 @@ +lisence: diff --git a/plugins/hls-cabal-plugin/test/testdata/empty.cabal b/plugins/hls-cabal-plugin/test/testdata/empty.cabal new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/hls-cabal-plugin/test/testdata/start-cabal-version.cabal b/plugins/hls-cabal-plugin/test/testdata/start-cabal-version.cabal new file mode 100644 index 0000000000..16fc6795f6 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/start-cabal-version.cabal @@ -0,0 +1 @@ +ca