-
-
Notifications
You must be signed in to change notification settings - Fork 388
Improve handling of nonsense rename attempts #4111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ import Data.List.NonEmpty (NonEmpty ((:|)), | |
import qualified Data.Map as M | ||
import Data.Maybe | ||
import Data.Mod.Word | ||
import Data.Row | ||
import qualified Data.Set as S | ||
import qualified Data.Text as T | ||
import Development.IDE (Recorder, WithPriority, | ||
|
@@ -57,43 +58,66 @@ import Language.LSP.Server | |
instance Hashable (Mod a) where hash n = hash (unMod n) | ||
|
||
descriptor :: Recorder (WithPriority E.Log) -> PluginId -> PluginDescriptor IdeState | ||
descriptor recorder pluginId = mkExactprintPluginDescriptor recorder $ (defaultPluginDescriptor pluginId "Provides renaming of Haskell identifiers") | ||
{ pluginHandlers = mkPluginHandler SMethod_TextDocumentRename renameProvider | ||
, pluginConfigDescriptor = defaultConfigDescriptor | ||
{ configCustomConfig = mkCustomConfig properties } | ||
} | ||
descriptor recorder pluginId = mkExactprintPluginDescriptor recorder $ | ||
(defaultPluginDescriptor pluginId "Provides renaming of Haskell identifiers") | ||
{ pluginHandlers = mconcat | ||
[ mkPluginHandler SMethod_TextDocumentRename renameProvider | ||
, mkPluginHandler SMethod_TextDocumentPrepareRename prepareRenameProvider | ||
] | ||
, pluginConfigDescriptor = defaultConfigDescriptor | ||
{ configCustomConfig = mkCustomConfig properties } | ||
} | ||
|
||
prepareRenameProvider :: PluginMethodHandler IdeState Method_TextDocumentPrepareRename | ||
prepareRenameProvider state _pluginId (PrepareRenameParams (TextDocumentIdentifier uri) pos _progressToken) = do | ||
nfp <- getNormalizedFilePathE uri | ||
namesUnderCursor <- getNamesAtPos state nfp pos | ||
-- When this handler says that rename is invalid, VSCode shows "The element can't be renamed" | ||
-- and doesn't even allow you to create full rename request. | ||
-- This handler deliberately approximates "things that definitely can't be renamed" | ||
-- to mean "there is no Name at given position". | ||
-- | ||
-- In particular it allows some cases through (e.g. cross-module renames), | ||
-- so that the full rename handler can give more informative error about them. | ||
let renameValid = not $ null namesUnderCursor | ||
pure $ InL $ PrepareRenameResult $ InR $ InR $ #defaultBehavior .== renameValid | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unsure if this is the correct behaviour. It's very hard to tell what the intention of the various options for results of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say that lsp spec is quite clear in this regard:
I looked at what the vscode-languageclient is doing with that response (see here) and when you return the boolean: Since I find the default behavior unproblematic (by pressing F2 on bunch of things you can see that vscode's default behavior correctly finds the range of symbol under cursor), I went with this Bool return type. I'll let you read this / react and I can open a PR with some additional clarifying comment later today. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I'm just wondering if we can rely on the "default behaviour" or whether it would be better for us to be returning the actual range of the name? Maybe it's fine? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I played with it for a bit and it seems it would be hard to get this right if we're to provide the Range under cursor. The thing I had trouble to make work was qualified names. I'd be more comfortable keeping the current implementation which seems to work robustly on all cases we have in tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, and vscode does the right thing and just changes Anyway, this all seems like super useful information, so worth writing down! |
||
|
||
renameProvider :: PluginMethodHandler IdeState Method_TextDocumentRename | ||
renameProvider state pluginId (RenameParams _prog (TextDocumentIdentifier uri) pos newNameText) = do | ||
nfp <- getNormalizedFilePathE uri | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
directOldNames <- getNamesAtPos state nfp pos | ||
directRefs <- concat <$> mapM (refsAtName state nfp) directOldNames | ||
|
||
{- References in HieDB are not necessarily transitive. With `NamedFieldPuns`, we can have | ||
indirect references through punned names. To find the transitive closure, we do a pass of | ||
the direct references to find the references for any punned names. | ||
See the `IndirectPuns` test for an example. -} | ||
indirectOldNames <- concat . filter ((>1) . length) <$> | ||
mapM (uncurry (getNamesAtPos state) <=< locToFilePos) directRefs | ||
let oldNames = filter matchesDirect indirectOldNames ++ directOldNames | ||
matchesDirect n = occNameFS (nameOccName n) `elem` directFS | ||
where | ||
directFS = map (occNameFS. nameOccName) directOldNames | ||
refs <- HS.fromList . concat <$> mapM (refsAtName state nfp) oldNames | ||
|
||
-- Validate rename | ||
crossModuleEnabled <- liftIO $ runAction "rename: config" state $ usePropertyAction #crossModule pluginId properties | ||
unless crossModuleEnabled $ failWhenImportOrExport state nfp refs oldNames | ||
when (any isBuiltInSyntax oldNames) $ throwError $ PluginInternalError "Invalid rename of built-in syntax" | ||
|
||
-- Perform rename | ||
let newName = mkTcOcc $ T.unpack newNameText | ||
filesRefs = collectWith locToUri refs | ||
getFileEdit (uri, locations) = do | ||
verTxtDocId <- lift $ getVersionedTextDoc (TextDocumentIdentifier uri) | ||
getSrcEdit state verTxtDocId (replaceRefs newName locations) | ||
fileEdits <- mapM getFileEdit filesRefs | ||
pure $ InL $ fold fileEdits | ||
nfp <- getNormalizedFilePathE uri | ||
directOldNames <- getNamesAtPos state nfp pos | ||
directRefs <- concat <$> mapM (refsAtName state nfp) directOldNames | ||
|
||
{- References in HieDB are not necessarily transitive. With `NamedFieldPuns`, we can have | ||
indirect references through punned names. To find the transitive closure, we do a pass of | ||
the direct references to find the references for any punned names. | ||
See the `IndirectPuns` test for an example. -} | ||
indirectOldNames <- concat . filter ((>1) . length) <$> | ||
mapM (uncurry (getNamesAtPos state) <=< locToFilePos) directRefs | ||
let oldNames = filter matchesDirect indirectOldNames ++ directOldNames | ||
where | ||
matchesDirect n = occNameFS (nameOccName n) `elem` directFS | ||
directFS = map (occNameFS . nameOccName) directOldNames | ||
|
||
case oldNames of | ||
-- There were no Names at given position (e.g. rename triggered within a comment or on a keyword) | ||
[] -> throwError $ PluginInvalidParams "No symbol to rename at given position" | ||
_ -> do | ||
refs <- HS.fromList . concat <$> mapM (refsAtName state nfp) oldNames | ||
|
||
-- Validate rename | ||
crossModuleEnabled <- liftIO $ runAction "rename: config" state $ usePropertyAction #crossModule pluginId properties | ||
unless crossModuleEnabled $ failWhenImportOrExport state nfp refs oldNames | ||
when (any isBuiltInSyntax oldNames) $ throwError $ PluginInternalError "Invalid rename of built-in syntax" | ||
|
||
-- Perform rename | ||
let newName = mkTcOcc $ T.unpack newNameText | ||
filesRefs = collectWith locToUri refs | ||
getFileEdit (uri, locations) = do | ||
verTxtDocId <- lift $ getVersionedTextDoc (TextDocumentIdentifier uri) | ||
getSrcEdit state verTxtDocId (replaceRefs newName locations) | ||
fileEdits <- mapM getFileEdit filesRefs | ||
pure $ InL $ fold fileEdits | ||
|
||
-- | Limit renaming across modules. | ||
failWhenImportOrExport :: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{- IShouldNotBeRenaemable -} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{- IShouldNotBeRenaemable -} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is fine we only pick up the newest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is used for combining of responses in case multiple plugins happen to handle same type of request.
I'd say it's ok to leave it like this for now, because this PR introduces this new request type and for now rename plugin is the only one that supports it.