From 615882d676bcecb735ac88cdfcb64e313c2cc45d Mon Sep 17 00:00:00 2001 From: Florian Engel Date: Fri, 9 Oct 2026 16:02:08 +0200 Subject: [PATCH 1/5] Only complete to cabal-version at first line The field cabal-version has to be in the first line. So any other field completion doesn't make sense. --- .../Plugin/Cabal/Completion/Completions.hs | 34 +++++++++++++------ .../src/Ide/Plugin/Cabal/Completion/Types.hs | 7 ++++ plugins/hls-cabal-plugin/test/Completer.hs | 12 +++++++ plugins/hls-cabal-plugin/test/Context.hs | 6 ++-- .../test/testdata/empty.cabal | 0 .../test/testdata/start-cabal-version.cabal | 1 + 6 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 plugins/hls-cabal-plugin/test/testdata/empty.cabal create mode 100644 plugins/hls-cabal-plugin/test/testdata/start-cabal-version.cabal 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..bc82bd0719 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 @@ -55,6 +55,9 @@ contextToCompleter (Stanza s _, KeyWord kw) = Just m -> case Map.lookup kw m of Nothing -> errorNoopCompleter (LogUnknownKeyWordInContextError kw) Just l -> l +contextToCompleter (TopLevel, CabalVersion) = constantCompleter ["cabal-version:"] +-- If we are at the start of a file, we should be in the toplevel stanza +contextToCompleter (Stanza _ _, CabalVersion) = errorNoopCompleter LogNotInTopLevelStanzaAtStartOfFirstLine -- | Takes prefix info about the previously written text -- and a rope (representing a file), returns the corresponding context. @@ -134,21 +137,30 @@ findCursorContext :: -- ^ The fields to traverse Context findCursorContext cursor parentHistory prefixText fields = - case findFieldSection cursor fields of - Nothing -> (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) - | 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 -> - findCursorContext cursor - (NE.cons (Syntax.positionCol (getAnnotation section) + 1, Stanza (getFieldName section) (getOptionalSectionName args)) parentHistory) - prefixText sectionFields + case (cursor, parentHistory) of + (Syntax.Position 1 _, (_, TopLevel) NE.:| []) + | not (any isCabalVersion fields) -> (TopLevel, CabalVersion) + _ -> case findFieldSection cursor fields of + Nothing -> (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) + | 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 -> + findCursorContext + cursor + (NE.cons (Syntax.positionCol (getAnnotation section) + 1, Stanza (getFieldName section) (getOptionalSectionName args)) parentHistory) + prefixText + sectionFields where inSameLineAsSectionName section = Syntax.positionRow (getAnnotation section) == Syntax.positionRow cursor stanzaCtx = snd $ NE.head parentHistory conditionalKeywords = ["if", "elif", "else"] + isCabalVersion field = + case field of + Syntax.Field (Syntax.Name _ "cabal-version") _ -> True + _ -> False -- | Finds the cursor's context, where the cursor is already found to be in a specific field -- 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..c5cfe6eade 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 @@ -21,6 +21,8 @@ data Log LogUnknownKeyWordInContextError KeyWordName | -- | This should never occur since we extract the word to lookup from the same map we look it up in. LogUnknownStanzaNameInContextError StanzaName + | -- | This should never occur since we should be at the toplevel if we at the first character in the first line + LogNotInTopLevelStanzaAtStartOfFirstLine | LogFilePathCompleterIOError FilePath IOError | LogUseWithStaleFastNoResult | LogMapLookUpOfKnownKeyFailed T.Text @@ -34,6 +36,8 @@ instance Pretty Log where "Lookup of key word failed for:" <+> viaShow kw LogUnknownStanzaNameInContextError sn -> "Lookup of stanza name failed for:" <+> viaShow sn + LogNotInTopLevelStanzaAtStartOfFirstLine -> + "Not in toplevel stanza, but at start of first line" LogFilePathCompleterIOError fp ioErr -> "When trying to complete the file path:" <+> pretty fp <+> "the following unexpected IO error occurred" <+> viaShow ioErr LogUseWithStaleFastNoResult -> "Package description couldn't be read" @@ -99,6 +103,8 @@ data FieldContext -- occurs right before the current word -- to be completed KeyWord !KeyWordName + | -- | Point is in the first line of the file and cabal version is missing + CabalVersion | -- | Keyword context where no keyword occurs -- right before the current word to be completed None @@ -106,6 +112,7 @@ data FieldContext instance Pretty FieldContext where pretty (KeyWord kw) = "KeyWord" <+> pretty kw + pretty CabalVersion = "CabalVersion" pretty None = "No Keyword" type KeyWordName = T.Text diff --git a/plugins/hls-cabal-plugin/test/Completer.hs b/plugins/hls-cabal-plugin/test/Completer.hs index c050c4a85e..6bf51dc904 100644 --- a/plugins/hls-cabal-plugin/test/Completer.hs +++ b/plugins/hls-cabal-plugin/test/Completer.hs @@ -77,6 +77,18 @@ 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:"] + pure () + , 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:"] + pure () , 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..fa38c3af7d 100644 --- a/plugins/hls-cabal-plugin/test/Context.hs +++ b/plugins/hls-cabal-plugin/test/Context.hs @@ -12,7 +12,7 @@ import Ide.Plugin.Cabal import Ide.Plugin.Cabal.Completion.Completer.Paths import Ide.Plugin.Cabal.Completion.Completions import Ide.Plugin.Cabal.Completion.Types (Context, - FieldContext (KeyWord, None), + FieldContext (CabalVersion, KeyWord, None), StanzaContext (Stanza, TopLevel)) import qualified Ide.Plugin.Cabal.Parse as Parse import Test.Hls @@ -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 @?= (TopLevel, CabalVersion) , 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 @@ -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 @?= (TopLevel, CabalVersion) , 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 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 From 143123cdc1aabcdb9b48c880189080c55cc29673 Mon Sep 17 00:00:00 2001 From: Florian Engel Date: Sat, 10 Oct 2026 11:16:18 +0200 Subject: [PATCH 2/5] Don't suggest cabal-version after first line It is only valid in first line --- .../src/Ide/Plugin/Cabal/Completion/Completions.hs | 2 +- plugins/hls-cabal-plugin/test/Completer.hs | 6 ++++++ .../test/testdata/cabal-version-missing.cabal | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 plugins/hls-cabal-plugin/test/testdata/cabal-version-missing.cabal 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 bc82bd0719..7e00041192 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 diff --git a/plugins/hls-cabal-plugin/test/Completer.hs b/plugins/hls-cabal-plugin/test/Completer.hs index 6bf51dc904..9392e817f1 100644 --- a/plugins/hls-cabal-plugin/test/Completer.hs +++ b/plugins/hls-cabal-plugin/test/Completer.hs @@ -89,6 +89,12 @@ basicCompleterTests = let complTexts = getTextEditTexts compls liftIO $ complTexts @?= ["cabal-version:"] pure () + , 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 + pure () , 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/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 From 4519dfd9a2f6a917512f8ac98f75c7e1e16086a4 Mon Sep 17 00:00:00 2001 From: Florian Engel Date: Sun, 11 Oct 2026 11:09:47 +0200 Subject: [PATCH 3/5] Remove unnecessary pures --- plugins/hls-cabal-plugin/test/Completer.hs | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/hls-cabal-plugin/test/Completer.hs b/plugins/hls-cabal-plugin/test/Completer.hs index 9392e817f1..e7b0049360 100644 --- a/plugins/hls-cabal-plugin/test/Completer.hs +++ b/plugins/hls-cabal-plugin/test/Completer.hs @@ -82,19 +82,16 @@ basicCompleterTests = compls <- getCompletions doc (Position 0 0) let complTexts = getTextEditTexts compls liftIO $ complTexts @?= ["cabal-version:"] - pure () , 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:"] - pure () , 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 - pure () , parameterisedCursorTestM "extensions completion" libraryStanzaData [ \_ actual -> assertBool "suggests FieldSelectors" $ "FieldSelectors" `elem` actual , \_ actual -> assertBool "suggests OverloadedStrings" $ "OverloadedStrings" `elem` actual From 0bb56d629fbca141c75a6819fd13796e8e9650b2 Mon Sep 17 00:00:00 2001 From: Florian Engel Date: Sun, 11 Oct 2026 11:14:27 +0200 Subject: [PATCH 4/5] Complete field value for none cabal-version fields at first line --- .../Plugin/Cabal/Completion/Completions.hs | 44 +++++++++---------- plugins/hls-cabal-plugin/test/Completer.hs | 10 +++++ plugins/hls-cabal-plugin/test/Context.hs | 4 +- .../cabal-version-not-in-first-line.cabal | 1 + 4 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 plugins/hls-cabal-plugin/test/testdata/cabal-version-not-in-first-line.cabal 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 7e00041192..bfaab0aa8c 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 @@ -137,30 +137,26 @@ findCursorContext :: -- ^ The fields to traverse Context findCursorContext cursor parentHistory prefixText fields = - case (cursor, parentHistory) of - (Syntax.Position 1 _, (_, TopLevel) NE.:| []) - | not (any isCabalVersion fields) -> (TopLevel, CabalVersion) - _ -> case findFieldSection cursor fields of - Nothing -> (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) - | 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 -> - findCursorContext - cursor - (NE.cons (Syntax.positionCol (getAnnotation section) + 1, Stanza (getFieldName section) (getOptionalSectionName args)) parentHistory) - prefixText - sectionFields + case findFieldSection cursor fields of + Nothing + | cursorRow == 1 -> (TopLevel, CabalVersion) + | 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 -> (TopLevel, CabalVersion) + | 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 -> + findCursorContext cursor + (NE.cons (Syntax.positionCol (getAnnotation section) + 1, Stanza (getFieldName section) (getOptionalSectionName args)) parentHistory) + prefixText sectionFields where inSameLineAsSectionName section = Syntax.positionRow (getAnnotation section) == Syntax.positionRow cursor stanzaCtx = snd $ NE.head parentHistory conditionalKeywords = ["if", "elif", "else"] - isCabalVersion field = - case field of - Syntax.Field (Syntax.Name _ "cabal-version") _ -> True - _ -> False + + cursorRow = Syntax.positionRow cursor -- | Finds the cursor's context, where the cursor is already found to be in a specific field -- @@ -171,11 +167,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 = (stanzaCtx, cabalVersionOrNone) -- 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 = (stanzaCtx, cabalVersionOrNone) + | cursorIsBeforeFieldName = (stanzaCtx, cabalVersionOrNone) | otherwise = (stanzaCtx, KeyWord (getFieldName field <> ":")) where (minIndent, stanzaCtx) = NE.head ctx @@ -191,3 +187,5 @@ classifyFieldContext ctx cursor field cursorColumn = Syntax.positionCol cursor fieldColumn = Syntax.positionCol (getAnnotation field) + + cabalVersionOrNone = if Syntax.positionRow cursor == 1 then CabalVersion else None diff --git a/plugins/hls-cabal-plugin/test/Completer.hs b/plugins/hls-cabal-plugin/test/Completer.hs index e7b0049360..bcd62e750c 100644 --- a/plugins/hls-cabal-plugin/test/Completer.hs +++ b/plugins/hls-cabal-plugin/test/Completer.hs @@ -82,11 +82,21 @@ basicCompleterTests = 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) diff --git a/plugins/hls-cabal-plugin/test/Context.hs b/plugins/hls-cabal-plugin/test/Context.hs index fa38c3af7d..3582bfa318 100644 --- a/plugins/hls-cabal-plugin/test/Context.hs +++ b/plugins/hls-cabal-plugin/test/Context.hs @@ -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 @?= (TopLevel, CabalVersion) , 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 @@ -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) + [ (TopLevel, CabalVersion) , (TopLevel, KeyWord "cabal-version:") , (TopLevel, None) , (TopLevel, KeyWord "description:") 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: From 47c72c681f7883930d7a7fec85dd4d6f1e3abc61 Mon Sep 17 00:00:00 2001 From: Florian Engel Date: Sun, 18 Oct 2026 18:50:04 +0200 Subject: [PATCH 5/5] Move cabal version form field to stanza context --- .../Plugin/Cabal/Completion/Completions.hs | 20 ++++++++++--------- .../src/Ide/Plugin/Cabal/Completion/Types.hs | 12 ++++------- plugins/hls-cabal-plugin/test/Context.hs | 12 +++++------ 3 files changed, 21 insertions(+), 23 deletions(-) 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 bfaab0aa8c..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 @@ -55,9 +55,11 @@ contextToCompleter (Stanza s _, KeyWord kw) = Just m -> case Map.lookup kw m of Nothing -> errorNoopCompleter (LogUnknownKeyWordInContextError kw) Just l -> l -contextToCompleter (TopLevel, CabalVersion) = constantCompleter ["cabal-version:"] --- If we are at the start of a file, we should be in the toplevel stanza -contextToCompleter (Stanza _ _, CabalVersion) = errorNoopCompleter LogNotInTopLevelStanzaAtStartOfFirstLine +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. @@ -139,12 +141,12 @@ findCursorContext :: findCursorContext cursor parentHistory prefixText fields = case findFieldSection cursor fields of Nothing - | cursorRow == 1 -> (TopLevel, CabalVersion) + | 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 -> (TopLevel, CabalVersion) + | 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 -> @@ -167,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, cabalVersionOrNone) + | 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, cabalVersionOrNone) - | cursorIsBeforeFieldName = (stanzaCtx, cabalVersionOrNone) + | cursorIsInFieldName = (firstLineOrStanzaCtx, None) + | cursorIsBeforeFieldName = (firstLineOrStanzaCtx, None) | otherwise = (stanzaCtx, KeyWord (getFieldName field <> ":")) where (minIndent, stanzaCtx) = NE.head ctx @@ -188,4 +190,4 @@ classifyFieldContext ctx cursor field cursorColumn = Syntax.positionCol cursor fieldColumn = Syntax.positionCol (getAnnotation field) - cabalVersionOrNone = if Syntax.positionRow cursor == 1 then CabalVersion else None + 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 c5cfe6eade..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 @@ -21,8 +21,6 @@ data Log LogUnknownKeyWordInContextError KeyWordName | -- | This should never occur since we extract the word to lookup from the same map we look it up in. LogUnknownStanzaNameInContextError StanzaName - | -- | This should never occur since we should be at the toplevel if we at the first character in the first line - LogNotInTopLevelStanzaAtStartOfFirstLine | LogFilePathCompleterIOError FilePath IOError | LogUseWithStaleFastNoResult | LogMapLookUpOfKnownKeyFailed T.Text @@ -36,8 +34,6 @@ instance Pretty Log where "Lookup of key word failed for:" <+> viaShow kw LogUnknownStanzaNameInContextError sn -> "Lookup of stanza name failed for:" <+> viaShow sn - LogNotInTopLevelStanzaAtStartOfFirstLine -> - "Not in toplevel stanza, but at start of first line" LogFilePathCompleterIOError fp ioErr -> "When trying to complete the file path:" <+> pretty fp <+> "the following unexpected IO error occurred" <+> viaShow ioErr LogUseWithStaleFastNoResult -> "Package description couldn't be read" @@ -81,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'. -- @@ -92,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) @@ -103,8 +102,6 @@ data FieldContext -- occurs right before the current word -- to be completed KeyWord !KeyWordName - | -- | Point is in the first line of the file and cabal version is missing - CabalVersion | -- | Keyword context where no keyword occurs -- right before the current word to be completed None @@ -112,7 +109,6 @@ data FieldContext instance Pretty FieldContext where pretty (KeyWord kw) = "KeyWord" <+> pretty kw - pretty CabalVersion = "CabalVersion" pretty None = "No Keyword" type KeyWordName = T.Text diff --git a/plugins/hls-cabal-plugin/test/Context.hs b/plugins/hls-cabal-plugin/test/Context.hs index 3582bfa318..f2272df3f2 100644 --- a/plugins/hls-cabal-plugin/test/Context.hs +++ b/plugins/hls-cabal-plugin/test/Context.hs @@ -12,8 +12,8 @@ import Ide.Plugin.Cabal import Ide.Plugin.Cabal.Completion.Completer.Paths import Ide.Plugin.Cabal.Completion.Completions import Ide.Plugin.Cabal.Completion.Types (Context, - FieldContext (CabalVersion, KeyWord, None), - StanzaContext (Stanza, TopLevel)) + FieldContext (KeyWord, None), + 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, CabalVersion) + 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, CabalVersion) + 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, CabalVersion) + 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, CabalVersion) + [ (FirstLine, None) , (TopLevel, KeyWord "cabal-version:") , (TopLevel, None) , (TopLevel, KeyWord "description:")