Auto merge of #1917 - puremourning:fixit-multiple-files-upstream, r=Valloric

[READY] Support FixIt and Refactor commands across multiple files

## Apply FixIt chunks across files

Previously, FixIts could only apply to the current buffer. Now we can apply FixIts across files, even if they are not currently open in the user's Vim. We obey existing configuration about how/where to open new files and apply changes in such a way as to not scribble the user's filesystem, and allow full undo history, like existing FixIt commands. Vim's python API actually makes this really quite easy.

To achieve this, we sort the chunks by filename, apply the existing logic unchanged. The only difference is that `ReplaceChunk` no longer implicitly assumes it applies to `vim.current.buffer`.

## Recognise 'FixIt' responses for any subcommand

Previously, we used the subcommand name to determine the type of response to expect. This coupled the client and the server and didn't allow us to apply FixIts for a "Refactor" command, or "GoTo" for a, theoretical "Find" command.

Now 'GoTo' and 'FixIt' commands don't need to start with those prefixes. For 'FixIt' we can detect the response type by looking for the 'fixits' entry in the response.

## Other changes

The `vimsupport.OpenFilename` method needed to handle the presence of Vim swap files and the various user responses. This pushed the cyclomatic complexity over the threshold, so the code was partially obfuscated to accommodate the cyclomatic complexity gods.

<!-- Reviewable:start -->
[<img src="https://reviewable.io/review_button.png" height=40 alt="Review on Reviewable"/>](https://reviewable.io/reviews/valloric/youcompleteme/1917)
<!-- Reviewable:end -->
This commit is contained in:
Homu 2016-02-22 07:18:00 +09:00
commit be0de73c42
6 changed files with 1226 additions and 194 deletions

201
README.md
View File

@ -28,6 +28,9 @@ YouCompleteMe: a code-completion engine for Vim
- [Diagnostic highlighting groups](#diagnostic-highlighting-groups)
- [Commands](#commands)
- [YcmCompleter subcommands](#ycmcompleter-subcommands)
- [Go to declaration/definition/etc. commands](#goto-commands)
- [Semantic type information and documentation](#semantic-information-commands)
- [Refactoring and FixIt commands](#refactoring-and-fixit-commands)
- [Options](#options)
- [FAQ](#faq)
- [Contributor Code of Conduct](#contributor-code-of-conduct)
@ -104,9 +107,21 @@ features plus extra:
- Supertab
- neocomplcache
YCM also provides semantic go-to-definition/declaration commands for C-family
languages & Python. Expect more IDE features powered by the various YCM semantic
engines in the future.
### And that's not all...
YCM also provides [semantic IDE-like features](#quick-feature-summary) in a
number of languages, including:
- finding declarations, definitions, usages, etc. of identifiers,
- displaying type information for classes, variables, functions etc.,
- displaying documentation for methods, members, etc. in the preview window,
- fixing common coding errors, like missing semi-colons, typos, etc.,
- semantic renaming of variables across files (JavaScript only).
Features vary by file type, so make sure to check out the [file type feature
summary](#quick-feature-summary) and the
[full list of completer subcommands](#ycmcompleter-subcommands) to
find out what's available for your favourite languages.
You'll also find that YCM has filepath completers (try typing `./` in a file)
and a completer that integrates with [UltiSnips][].
@ -682,6 +697,7 @@ Quick Feature Summary
### JavaScript
* Intelligent auto-completion
* Renaming variables (`RefactorRename <new name>`)
* Go to definition, find references (`GoToDefinition`, `GoToReferences`)
* Type information for identifiers (`GetType`)
* View documentation comments for identifiers (`GetDoc`)
@ -1106,20 +1122,25 @@ purpose.
### The `:YcmCompleter` command
This command can be used to invoke completer-specific commands. If the first
This command gives access to a number of additional [IDE-like
features](#quick-feature-summary) in YCM, for things like semantic GoTo, type
information, FixIt and refactoring.
Technically the command invokes completer-specific commands. If the first
argument is of the form `ft=...` the completer for that file type will be used
(for example `ft=cpp`), else the native completer of the current buffer will be
used.
Call `YcmCompleter` without further arguments for information about the
commands you can call for the selected completer.
Call `YcmCompleter` without further arguments for a list of the
commands you can call for the current completer.
See the _YcmCompleter subcommands_ section for more information on the available
subcommands.
See the [file type feature summary](#quick-feature-summary) for an overview of
the features available for each file type. See the _YcmCompleter subcommands_
section for more information on the available subcommands and their usage.
YcmCompleter subcommands
------------------------
[See the docs for the `YcmCompleter` command before tackling this section.]
NOTE: See the docs for the `YcmCompleter` command before tackling this section.
The invoked subcommand is automatically routed to the currently active semantic
completer, so `:YcmCompleter GoToDefinition` will invoke the `GoToDefinition`
@ -1131,23 +1152,26 @@ You may also want to map the subcommands to something less verbose; for
instance, `nnoremap <leader>jd :YcmCompleter GoTo<CR>`
maps the `<leader>jd` sequence to the longer subcommand invocation.
The various `GoTo*` subcommands add entries to Vim's `jumplist` so you can use
### GoTo commands
These commands are useful for jumping around and exploring code. When moving
the cursor, the subcommands add entries to Vim's `jumplist` so you can use
`CTRL-O` to jump back to where you where before invoking the command (and
`CTRL-I` to jump forward; see `:h jumplist` for details).
### The `GoToInclude` subcommand
#### The `GoToInclude` subcommand
Looks up the current line for a header and jumps to it.
Supported in filetypes: `c, cpp, objc, objcpp`
### The `GoToDeclaration` subcommand
#### The `GoToDeclaration` subcommand
Looks up the symbol under the cursor and jumps to its declaration.
Supported in filetypes: `c, cpp, objc, objcpp, cs, go, python, rust`
### The `GoToDefinition` subcommand
#### The `GoToDefinition` subcommand
Looks up the symbol under the cursor and jumps to its definition.
@ -1159,7 +1183,7 @@ with `#include` directives (directly or indirectly) in that file.
Supported in filetypes: `c, cpp, objc, objcpp, cs, go, javascript, python,
rust, typescript`
### The `GoTo` subcommand
#### The `GoTo` subcommand
This command tries to perform the "most sensible" GoTo operation it can.
Currently, this means that it tries to look up the symbol under the cursor and
@ -1170,7 +1194,7 @@ jump to it. For C#, implementations are also considered and preferred.
Supported in filetypes: `c, cpp, objc, objcpp, cs, go, javascript, python, rust`
### The `GoToImprecise` subcommand
#### The `GoToImprecise` subcommand
WARNING: This command trades correctness for speed!
@ -1183,7 +1207,7 @@ latency.
Supported in filetypes: `c, cpp, objc, objcpp`
### The `GoToReferences` subcommand
#### The `GoToReferences` subcommand
This command attempts to find all of the references within the project to the
identifier under the cursor and populates the quickfix list with those
@ -1191,19 +1215,28 @@ locations.
Supported in filetypes: `javascript, python, typescript`
### The `ClearCompilationFlagCache` subcommand
#### The `GoToImplementation` subcommand
YCM caches the flags it gets from the `FlagsForFile` function in your
`ycm_extra_conf.py` file if you return them with the `do_cache` parameter set to
`True`. The cache is in memory and is never invalidated (unless you restart Vim
of course).
Looks up the symbol under the cursor and jumps to its implementation (i.e.
non-interface). If there are multiple implementations, instead provides a list
of implementations to choose from.
This command clears that cache entirely. YCM will then re-query your
`FlagsForFile` function as needed in the future.
Supported in filetypes: `cs`
Supported in filetypes: `c, cpp, objc, objcpp`
#### The `GoToImplementationElseDeclaration` subcommand
### The `GetType` subcommand
Looks up the symbol under the cursor and jumps to its implementation if one,
else jump to its declaration. If there are multiple implementations, instead
provides a list of implementations to choose from.
Supported in filetypes: `cs`
### Semantic information commands
These commands are useful for finding static information about the code, such
as the types of variables, viewing declarations and documentation strings.
#### The `GetType` subcommand
Echos the type of the variable or method under the cursor, and where it differs,
the derived type.
@ -1234,7 +1267,7 @@ NOTE: Causes re-parsing of the current translation unit.
Supported in filetypes: `c, cpp, objc, objcpp, javascript, typescript`
### The `GetParent` subcommand
#### The `GetParent` subcommand
Echos the semantic parent of the point under the cursor.
@ -1265,7 +1298,26 @@ NOTE: Causes re-parsing of the current translation unit.
Supported in filetypes: `c, cpp, objc, objcpp`
### The `FixIt` subcommand
#### The `GetDoc` subcommand
Displays the preview window populated with quick info about the identifier
under the cursor. Depending on the file type, this includes things like:
* The type or declaration of identifier,
* Doxygen/javadoc comments,
* Python docstrings,
* etc.
Supported in filetypes: `c, cpp, objc, objcpp, cs, python, typescript,
javascript`
### Refactoring and FixIt commands
These commands make changes to your source code in order to perform refactoring
or code correction. YouCompleteMe does not perform any action which cannot be
undone, and never saves or writes files to the disk.
#### The `FixIt` subcommand
Where available, attempts to make changes to the buffer to correct the
diagnostic closest to the cursor position.
@ -1290,8 +1342,8 @@ indication).
NOTE: Causes re-parsing of the current translation unit.
NOTE: After applying a fix-it, the diagnostics UI is not immediately updated.
This is due to a technical restriction in vim, and moving the cursor, or issuing
the the `:YcmForceCompileAndDiagnostics` command will refresh the diagnostics.
This is due to a technical restriction in Vim. Moving the cursor, or issuing
the `:YcmForceCompileAndDiagnostics` command will refresh the diagnostics.
Repeated invocations of the `FixIt` command on a given line, however, _do_ apply
all diagnostics as expected without requiring refreshing of the diagnostics UI.
This is particularly useful where there are multiple diagnostics on one line, or
@ -1299,34 +1351,81 @@ where after fixing one diagnostic, another fix-it is available.
Supported in filetypes: `c, cpp, objc, objcpp, cs`
### The `GetDoc` subcommand
#### The `RefactorRename <new name>` subcommand
Displays the preview window populated with quick info about the identifier
under the cursor. This includes, depending on the language, things like:
In supported file types, this command attempts to perform a semantic rename of
the identifier under the cursor. This includes renaming declarations,
definitions and usages of the identifier, or any other language-appropriate
action. The specific behavior is defined by the semantic engine in use.
* The type or declaration of identifier
* Doxygen/javadoc comments
* Python docstrings
* etc.
Similar to `FixIt`, this command applies automatic modifications to your source
files. Rename operations may involve changes to multiple files, which may or may
not be open in Vim buffers at the time. YouCompleteMe handles all of this for
you. The behavior is described in [the following section](#multi-file-refactor).
Supported in filetypes: `c, cpp, objc, objcpp, cs, python, typescript,
javascript`
Supported in filetypes: `javascript` (variables only)
### The `StartServer` subcommand
#### Multi-file Refactor
When a Refactor or FixIt command touches multiple files, YouCompleteMe attempts
to apply those modifications to any existing open, visible buffer in the current
tab. If no such buffer can be found, YouCompleteMe opens the file in a new
small horizontal split at the top of the current window, applies the change,
and then *hides* the window. NOTE: The buffer remains open, and must be
manually saved. A confirmation dialog is opened prior to doing this to remind
you that this is about to happen.
Once the modifications have been made, the quickfix list (see `:help quickfix`)
is opened and populated with the locations of all modifications. This can be
used to review all automatic changes made. Typically, use the `CTRL-W
<enter>` combination to open the selected file in a new split.
The buffers are *not* saved automatically. That is, you must save the modified
buffers manually after reviewing the changes from the quickfix list. Changes
can be undone using Vim's powerful undo features (see `:help undo`). Note
that Vim's undo is per-buffer, so to undo all changes, the undo commands must
be applied in each modified buffer separately.
NOTE: While applying modifications, Vim may find files which are already open
and have a swap file. The command is aborted if you select Abort or Quit in any
such prompts. This leaves the Refactor operation partially complete and must be
manually corrected using Vim's undo features. The quickfix list is *not*
populated in this case. Inspect `:buffers` or equivalent (see `:help buffers`)
to see the buffers that were opened by the command.
### Miscellaneous commands
These commands are for general administration, rather than IDE-like features.
They cover things like the semantic engine server instance and compilation
flags.
#### The `ClearCompilationFlagCache` subcommand
YCM caches the flags it gets from the `FlagsForFile` function in your
`ycm_extra_conf.py` file if you return them with the `do_cache` parameter set to
`True`. The cache is in memory and is never invalidated (unless you restart Vim
of course).
This command clears that cache entirely. YCM will then re-query your
`FlagsForFile` function as needed in the future.
Supported in filetypes: `c, cpp, objc, objcpp`
#### The `StartServer` subcommand
Starts the semantic-engine-as-localhost-server for those semantic engines that
work as separate servers that YCM talks to.
Supported in filetypes: `cs, go, javascript, rust`
### The `StopServer` subcommand
#### The `StopServer` subcommand
Stops the semantic-engine-as-localhost-server for those semantic engines that
work as separate servers that YCM talks to.
Supported in filetypes: `cs, go, javascript, rust`
### The `RestartServer` subcommand
#### The `RestartServer` subcommand
Restarts the semantic-engine-as-localhost-server for those semantic engines that
work as separate servers that YCM talks to.
@ -1340,7 +1439,7 @@ python binary to use to restart the Python semantic engine.
Supported in filetypes: `cs, python, rust`
### The `ReloadSolution` subcommand
#### The `ReloadSolution` subcommand
Instruct the Omnisharp server to clear its cache and reload all files from disk.
This is useful when files are added, removed, or renamed in the solution, files
@ -1348,22 +1447,6 @@ are changed outside of Vim, or whenever Omnisharp cache is out-of-sync.
Supported in filetypes: `cs`
### The `GoToImplementation` subcommand
Looks up the symbol under the cursor and jumps to its implementation (i.e.
non-interface). If there are multiple implementations, instead provides a list
of implementations to choose from.
Supported in filetypes: `cs`
### The `GoToImplementationElseDeclaration` subcommand
Looks up the symbol under the cursor and jumps to its implementation if one,
else jump to its declaration. If there are multiple implementations, instead
provides a list of implementations to choose from.
Supported in filetypes: `cs`
Functions
--------
@ -1896,7 +1979,7 @@ let g:ycm_csharp_server_port = 0
By default, when YCM inserts a namespace, it will insert the `using` statement
under the nearest `using` statement. You may prefer that the `using` statement is
inserted somewhere, for example, to preserve sorting. If so, you can set this
option to override this behaviour.
option to override this behavior.
When this option is set, instead of inserting the `using` statement itself, YCM
will set the global variable `g:ycm_namespace_to_insert` to the namespace to

View File

@ -5,6 +5,7 @@ Contents ~
1. Introduction |youcompleteme-introduction|
2. Intro |youcompleteme-intro|
1. And that's not all... |youcompleteme-thats-not-all...|
3. Installation |youcompleteme-installation|
1. Mac OS X Installation |youcompleteme-mac-os-x-installation|
2. Ubuntu Linux x64 Installation |youcompleteme-ubuntu-linux-x64-installation|
@ -48,23 +49,29 @@ Contents ~
6. The |:YcmToggleLogs| command
7. The |:YcmCompleter| command
7. YcmCompleter subcommands |youcompleteme-ycmcompleter-subcommands|
1. The |GoToInclude| subcommand
2. The |GoToDeclaration| subcommand
3. The |GoToDefinition| subcommand
4. The |GoTo| subcommand
5. The |GoToImprecise| subcommand
6. The |GoToReferences| subcommand
7. The |ClearCompilationFlagCache| subcommand
8. The |GetType| subcommand
9. The |GetParent| subcommand
10. The |FixIt| subcommand
11. The |GetDoc| subcommand
12. The |StartServer| subcommand
13. The |StopServer| subcommand
14. The |RestartServer| subcommand
15. The |ReloadSolution| subcommand
16. The |GoToImplementation| subcommand
17. The |GoToImplementationElseDeclaration| subcommand
1. GoTo commands |youcompleteme-goto-commands|
1. The |GoToInclude| subcommand
2. The |GoToDeclaration| subcommand
3. The |GoToDefinition| subcommand
4. The |GoTo| subcommand
5. The |GoToImprecise| subcommand
6. The |GoToReferences| subcommand
7. The |GoToImplementation| subcommand
8. The |GoToImplementationElseDeclaration| subcommand
2. Semantic information commands |youcompleteme-semantic-information-commands|
1. The |GetType| subcommand
2. The |GetParent| subcommand
3. The |GetDoc| subcommand
3. Refactoring and FixIt commands |youcompleteme-refactoring-fixit-commands|
1. The |FixIt| subcommand
2. The 'RefactorRename <new name>' subcommand |RefactorRename-new-name|
3. Multi-file Refactor |youcompleteme-multi-file-refactor|
4. Miscellaneous commands |youcompleteme-miscellaneous-commands|
1. The |ClearCompilationFlagCache| subcommand
2. The |StartServer| subcommand
3. The |StopServer| subcommand
4. The |RestartServer| subcommand
5. The |ReloadSolution| subcommand
8. Functions |youcompleteme-functions|
1. The |youcompleteme#GetErrorCount| function
2. The |youcompleteme#GetWarningCount| function
@ -193,6 +200,10 @@ Image: Build Status [1] Image: Build status [3]
- YcmCompleter subcommands
- Go to declaration/definition/etc. commands
- Semantic type information and documentation
- Refactoring and FixIt commands
- Options
- FAQ
- Contributor Code of Conduct
@ -280,9 +291,22 @@ features plus extra:
- Supertab
- neocomplcache
YCM also provides semantic go-to-definition/declaration commands for C-family
languages & Python. Expect more IDE features powered by the various YCM
semantic engines in the future.
-------------------------------------------------------------------------------
*youcompleteme-thats-not-all...*
And that's not all... ~
YCM also provides semantic IDE-like features in a number of languages,
including:
- finding declarations, definitions, usages, etc. of identifiers,
- displaying type information for classes, variables, functions etc.,
- displaying documentation for methods, members, etc. in the preview window,
- fixing common coding errors, like missing semi-colons, typos, etc.,
- semantic renaming of variables across files (JavaScript only).
Features vary by file type, so make sure to check out the file type feature
summary and the full list of completer subcommands to find out what's available
for your favourite languages.
You'll also find that YCM has filepath completers (try typing './' in a file)
and a completer that integrates with UltiSnips [17].
@ -911,6 +935,7 @@ TypeScript ~
JavaScript ~
- Intelligent auto-completion
- Renaming variables ('RefactorRename <new name>')
- Go to definition, find references (|GoToDefinition|, |GoToReferences|)
- Type information for identifiers (|GetType|)
- View documentation comments for identifiers (|GetDoc|)
@ -1372,20 +1397,24 @@ purpose.
-------------------------------------------------------------------------------
The *:YcmCompleter* command
This command can be used to invoke completer-specific commands. If the first
This command gives access to a number of additional IDE-like features in YCM,
for things like semantic GoTo, type information, FixIt and refactoring.
Technically the command invokes completer-specific commands. If the first
argument is of the form 'ft=...' the completer for that file type will be used
(for example 'ft=cpp'), else the native completer of the current buffer will be
used. Call 'YcmCompleter' without further arguments for information about the
commands you can call for the selected completer.
used. Call 'YcmCompleter' without further arguments for a list of the commands
you can call for the current completer.
See the _YcmCompleter subcommands_ section for more information on the
available subcommands.
See the file type feature summary for an overview of the features available for
each file type. See the _YcmCompleter subcommands_ section for more information
on the available subcommands and their usage.
===============================================================================
*youcompleteme-ycmcompleter-subcommands*
YcmCompleter subcommands ~
[See the docs for the 'YcmCompleter' command before tackling this section.]
NOTE: See the docs for the 'YcmCompleter' command before tackling this section.
The invoked subcommand is automatically routed to the currently active semantic
completer, so ':YcmCompleter GoToDefinition' will invoke the |GoToDefinition|
@ -1397,7 +1426,12 @@ You may also want to map the subcommands to something less verbose; for
instance, 'nnoremap <leader>jd :YcmCompleter GoTo<CR>' maps the '<leader>jd'
sequence to the longer subcommand invocation.
The various 'GoTo*' subcommands add entries to Vim's 'jumplist' so you can use
-------------------------------------------------------------------------------
*youcompleteme-goto-commands*
GoTo commands ~
These commands are useful for jumping around and exploring code. When moving
the cursor, the subcommands add entries to Vim's 'jumplist' so you can use
'CTRL-O' to jump back to where you where before invoking the command (and
'CTRL-I' to jump forward; see ':h jumplist' for details).
@ -1465,17 +1499,29 @@ locations.
Supported in filetypes: 'javascript, python, typescript'
-------------------------------------------------------------------------------
The *ClearCompilationFlagCache* subcommand
The *GoToImplementation* subcommand
YCM caches the flags it gets from the 'FlagsForFile' function in your
'ycm_extra_conf.py' file if you return them with the 'do_cache' parameter set
to 'True'. The cache is in memory and is never invalidated (unless you restart
Vim of course).
Looks up the symbol under the cursor and jumps to its implementation (i.e. non-
interface). If there are multiple implementations, instead provides a list of
implementations to choose from.
This command clears that cache entirely. YCM will then re-query your
'FlagsForFile' function as needed in the future.
Supported in filetypes: 'cs'
Supported in filetypes: 'c, cpp, objc, objcpp'
-------------------------------------------------------------------------------
The *GoToImplementationElseDeclaration* subcommand
Looks up the symbol under the cursor and jumps to its implementation if one,
else jump to its declaration. If there are multiple implementations, instead
provides a list of implementations to choose from.
Supported in filetypes: 'cs'
-------------------------------------------------------------------------------
*youcompleteme-semantic-information-commands*
Semantic information commands ~
These commands are useful for finding static information about the code, such
as the types of variables, viewing declarations and documentation strings.
-------------------------------------------------------------------------------
The *GetType* subcommand
@ -1535,6 +1581,28 @@ NOTE: Causes re-parsing of the current translation unit.
Supported in filetypes: 'c, cpp, objc, objcpp'
-------------------------------------------------------------------------------
The *GetDoc* subcommand
Displays the preview window populated with quick info about the identifier
under the cursor. Depending on the file type, this includes things like:
- The type or declaration of identifier,
- Doxygen/javadoc comments,
- Python docstrings,
- etc.
Supported in filetypes: 'c, cpp, objc, objcpp, cs, python, typescript,
javascript'
-------------------------------------------------------------------------------
*youcompleteme-refactoring-fixit-commands*
Refactoring and FixIt commands ~
These commands make changes to your source code in order to perform refactoring
or code correction. YouCompleteMe does not perform any action which cannot be
undone, and never saves or writes files to the disk.
-------------------------------------------------------------------------------
The *FixIt* subcommand
@ -1561,29 +1629,82 @@ indication).
NOTE: Causes re-parsing of the current translation unit.
NOTE: After applying a fix-it, the diagnostics UI is not immediately updated.
This is due to a technical restriction in vim, and moving the cursor, or
issuing the the |:YcmForceCompileAndDiagnostics| command will refresh the
diagnostics. Repeated invocations of the |FixIt| command on a given line,
however, _do_ apply all diagnostics as expected without requiring refreshing of
the diagnostics UI. This is particularly useful where there are multiple
This is due to a technical restriction in Vim. Moving the cursor, or issuing
the |:YcmForceCompileAndDiagnostics| command will refresh the diagnostics.
Repeated invocations of the |FixIt| command on a given line, however, _do_
apply all diagnostics as expected without requiring refreshing of the
diagnostics UI. This is particularly useful where there are multiple
diagnostics on one line, or where after fixing one diagnostic, another fix-it
is available.
Supported in filetypes: 'c, cpp, objc, objcpp, cs'
-------------------------------------------------------------------------------
The *GetDoc* subcommand
*RefactorRename-new-name*
The 'RefactorRename <new name>' subcommand ~
Displays the preview window populated with quick info about the identifier
under the cursor. This includes, depending on the language, things like:
In supported file types, this command attempts to perform a semantic rename of
the identifier under the cursor. This includes renaming declarations,
definitions and usages of the identifier, or any other language-appropriate
action. The specific behavior is defined by the semantic engine in use.
- The type or declaration of identifier
- Doxygen/javadoc comments
- Python docstrings
- etc.
Similar to |FixIt|, this command applies automatic modifications to your source
files. Rename operations may involve changes to multiple files, which may or
may not be open in Vim buffers at the time. YouCompleteMe handles all of this
for you. The behavior is described in the following section.
Supported in filetypes: 'c, cpp, objc, objcpp, cs, python, typescript,
javascript'
Supported in filetypes: 'javascript' (variables only)
-------------------------------------------------------------------------------
*youcompleteme-multi-file-refactor*
Multi-file Refactor ~
When a Refactor or FixIt command touches multiple files, YouCompleteMe attempts
to apply those modifications to any existing open, visible buffer in the
current tab. If no such buffer can be found, YouCompleteMe opens the file in a
new small horizontal split at the top of the current window, applies the
change, and then _hides_ the window. NOTE: The buffer remains open, and must be
manually saved. A confirmation dialog is opened prior to doing this to remind
you that this is about to happen.
Once the modifications have been made, the quickfix list (see ':help quickfix')
is opened and populated with the locations of all modifications. This can be
used to review all automatic changes made. Typically, use the 'CTRL-W <enter>'
combination to open the selected file in a new split.
The buffers are _not_ saved automatically. That is, you must save the modified
buffers manually after reviewing the changes from the quickfix list. Changes
can be undone using Vim's powerful undo features (see ':help undo'). Note that
Vim's undo is per-buffer, so to undo all changes, the undo commands must be
applied in each modified buffer separately.
NOTE: While applying modifications, Vim may find files which are already open
and have a swap file. The command is aborted if you select Abort or Quit in any
such prompts. This leaves the Refactor operation partially complete and must be
manually corrected using Vim's undo features. The quickfix list is _not_
populated in this case. Inspect ':buffers' or equivalent (see ':help buffers')
to see the buffers that were opened by the command.
-------------------------------------------------------------------------------
*youcompleteme-miscellaneous-commands*
Miscellaneous commands ~
These commands are for general administration, rather than IDE-like features.
They cover things like the semantic engine server instance and compilation
flags.
-------------------------------------------------------------------------------
The *ClearCompilationFlagCache* subcommand
YCM caches the flags it gets from the 'FlagsForFile' function in your
'ycm_extra_conf.py' file if you return them with the 'do_cache' parameter set
to 'True'. The cache is in memory and is never invalidated (unless you restart
Vim of course).
This command clears that cache entirely. YCM will then re-query your
'FlagsForFile' function as needed in the future.
Supported in filetypes: 'c, cpp, objc, objcpp'
-------------------------------------------------------------------------------
The *StartServer* subcommand
@ -1623,24 +1744,6 @@ files are changed outside of Vim, or whenever Omnisharp cache is out-of-sync.
Supported in filetypes: 'cs'
-------------------------------------------------------------------------------
The *GoToImplementation* subcommand
Looks up the symbol under the cursor and jumps to its implementation (i.e. non-
interface). If there are multiple implementations, instead provides a list of
implementations to choose from.
Supported in filetypes: 'cs'
-------------------------------------------------------------------------------
The *GoToImplementationElseDeclaration* subcommand
Looks up the symbol under the cursor and jumps to its implementation if one,
else jump to its declaration. If there are multiple implementations, instead
provides a list of implementations to choose from.
Supported in filetypes: 'cs'
===============================================================================
*youcompleteme-functions*
Functions ~
@ -2137,7 +2240,7 @@ The *g:ycm_csharp_insert_namespace_expr* option
By default, when YCM inserts a namespace, it will insert the 'using' statement
under the nearest 'using' statement. You may prefer that the 'using' statement
is inserted somewhere, for example, to preserve sorting. If so, you can set
this option to override this behaviour.
this option to override this behavior.
When this option is set, instead of inserting the 'using' statement itself, YCM
will set the global variable 'g:ycm_namespace_to_insert' to the namespace to

View File

@ -33,10 +33,6 @@ class CommandRequest( BaseRequest ):
self._arguments = _EnsureBackwardsCompatibility( arguments )
self._completer_target = ( completer_target if completer_target
else 'filetype_default' )
self._is_goto_command = (
self._arguments and self._arguments[ 0 ].startswith( 'GoTo' ) )
self._is_fixit_command = (
self._arguments and self._arguments[ 0 ].startswith( 'FixIt' ) )
self._response = None
@ -61,29 +57,31 @@ class CommandRequest( BaseRequest ):
if not self.Done() or self._response is None:
return
if self._is_goto_command:
return self._HandleGotoResponse()
if self._is_fixit_command:
return self._HandleFixitResponse()
# If not a dictionary or a list, the response is necessarily a
# scalar: boolean, number, string, etc. In this case, we print
# it to the user.
if not isinstance( self._response, ( dict, list ) ):
return self._HandleBasicResponse()
if 'fixits' in self._response:
return self._HandleFixitResponse()
if 'message' in self._response:
return self._HandleMessageResponse()
if 'detailed_info' in self._response:
return self._HandleDetailedInfoResponse()
# The only other type of response we understand is GoTo, and that is the
# only one that we can't detect just by inspecting the response (it should
# either be a single location or a list)
return self._HandleGotoResponse()
def _HandleGotoResponse( self ):
if isinstance( self._response, list ):
defs = [ _BuildQfListItem( x ) for x in self._response ]
vim.eval( 'setqflist( %s )' % repr( defs ) )
vimsupport.SetQuickFixList(
[ _BuildQfListItem( x ) for x in self._response ] )
vim.eval( 'youcompleteme#OpenGoToList()' )
else:
vimsupport.JumpToLocation( self._response[ 'filepath' ],
@ -96,12 +94,10 @@ class CommandRequest( BaseRequest ):
vimsupport.EchoText( "No fixits found for current line" )
else:
chunks = self._response[ 'fixits' ][ 0 ][ 'chunks' ]
vimsupport.ReplaceChunksList( chunks )
vimsupport.EchoTextVimWidth( "FixIt applied "
+ str( len( chunks ) )
+ " changes" )
try:
vimsupport.ReplaceChunks( chunks )
except RuntimeError as e:
vimsupport.PostMultiLineNotice( e.message )
def _HandleBasicResponse( self ):

View File

@ -18,7 +18,9 @@
from ycm.test_utils import MockVimModule
MockVimModule()
import json
from mock import patch, call
from nose.tools import ok_
from ycm.client.command_request import CommandRequest
@ -85,6 +87,177 @@ class GoToResponse_QuickFix_test:
self._request.RunPostCommandActionsIfNeeded()
vim_eval.assert_has_calls( [
call( 'setqflist( {0} )'.format( repr( expected_qf_list ) ) ),
call( 'setqflist( {0} )'.format( json.dumps( expected_qf_list ) ) ),
call( 'youcompleteme#OpenGoToList()' ),
] )
class Response_Detection_test:
def BasicResponse_test( self ):
def _BasicResponseTest( command, response ):
with patch( 'vim.command' ) as vim_command:
request = CommandRequest( [ command ] )
request._response = response
request.RunPostCommandActionsIfNeeded()
vim_command.assert_called_with( "echom '{0}'".format( response ) )
tests = [
[ 'AnythingYouLike', True ],
[ 'GoToEvenWorks', 10 ],
[ 'FixItWorks', 'String!' ],
[ 'and8434fd andy garbag!', 10.3 ],
]
for test in tests:
yield _BasicResponseTest, test[ 0 ], test[ 1 ]
def FixIt_Response_Empty_test( self ):
# Ensures we recognise and handle fixit responses which indicate that there
# are no fixits available
def EmptyFixItTest( command ):
with patch( 'ycm.vimsupport.ReplaceChunks' ) as replace_chunks:
with patch( 'ycm.vimsupport.EchoText' ) as echo_text:
request = CommandRequest( [ command ] )
request._response = {
'fixits': []
}
request.RunPostCommandActionsIfNeeded()
echo_text.assert_called_with( 'No fixits found for current line' )
replace_chunks.assert_not_called()
for test in [ 'FixIt', 'Refactor', 'GoToHell', 'any_old_garbade!!!21' ]:
yield EmptyFixItTest, test
def FixIt_Response_test( self ):
# Ensures we recognise and handle fixit responses with some dummy chunk data
def FixItTest( command, response, chunks ):
with patch( 'ycm.vimsupport.ReplaceChunks' ) as replace_chunks:
with patch( 'ycm.vimsupport.EchoText' ) as echo_text:
request = CommandRequest( [ command ] )
request._response = response
request.RunPostCommandActionsIfNeeded()
replace_chunks.assert_called_with( chunks )
echo_text.assert_not_called()
basic_fixit = {
'fixits': [ {
'chunks': [ {
'dummy chunk contents': True
} ]
} ]
}
basic_fixit_chunks = basic_fixit[ 'fixits' ][ 0 ][ 'chunks' ]
multi_fixit = {
'fixits': [ {
'chunks': [ {
'dummy chunk contents': True
} ]
}, {
'additional fixits are ignored currently': True
} ]
}
multi_fixit_first_chunks = multi_fixit[ 'fixits' ][ 0 ][ 'chunks' ]
tests = [
[ 'AnythingYouLike', basic_fixit, basic_fixit_chunks ],
[ 'GoToEvenWorks', basic_fixit, basic_fixit_chunks ],
[ 'FixItWorks', basic_fixit, basic_fixit_chunks ],
[ 'and8434fd andy garbag!', basic_fixit, basic_fixit_chunks ],
[ 'additional fixits ignored', multi_fixit, multi_fixit_first_chunks ],
]
for test in tests:
yield FixItTest, test[ 0 ], test[ 1 ], test[ 2 ]
def Message_Response_test( self ):
# Ensures we correctly recognise and handle responses with a message to show
# to the user
def MessageTest( command, message ):
with patch( 'ycm.vimsupport.EchoText' ) as echo_text:
request = CommandRequest( [ command ] )
request._response = { 'message': message }
request.RunPostCommandActionsIfNeeded()
echo_text.assert_called_with( message )
tests = [
[ '___________', 'This is a message' ],
[ '', 'this is also a message' ],
[ 'GetType', 'std::string' ],
]
for test in tests:
yield MessageTest, test[ 0 ], test[ 1 ]
def Detailed_Info_test( self ):
# Ensures we correctly detect and handle detailed_info responses which are
# used to display information in the preview window
def DetailedInfoTest( command, info ):
with patch( 'ycm.vimsupport.WriteToPreviewWindow' ) as write_to_preview:
request = CommandRequest( [ command ] )
request._response = { 'detailed_info': info }
request.RunPostCommandActionsIfNeeded()
write_to_preview.assert_called_with( info )
tests = [
[ '___________', 'This is a message' ],
[ '', 'this is also a message' ],
[ 'GetDoc', 'std::string\netc\netc' ],
]
for test in tests:
yield DetailedInfoTest, test[ 0 ], test[ 1 ]
def GoTo_Single_test( self ):
# Ensures we handle any unknown type of response as a GoTo response
def GoToTest( command, response ):
with patch( 'ycm.vimsupport.JumpToLocation' ) as jump_to_location:
request = CommandRequest( [ command ] )
request._response = response
request.RunPostCommandActionsIfNeeded()
jump_to_location.assert_called_with(
response[ 'filepath' ],
response[ 'line_num' ],
response[ 'column_num' ] )
def GoToListTest( command, response ):
# Note: the detail of these called are tested by
# GoToResponse_QuickFix_test, so here we just check that the right call is
# made
with patch( 'ycm.vimsupport.SetQuickFixList' ) as set_qf_list:
with patch( 'vim.eval' ) as vim_eval:
request = CommandRequest( [ command ] )
request._response = response
request.RunPostCommandActionsIfNeeded()
ok_( set_qf_list.called )
ok_( vim_eval.called )
basic_goto = {
'filepath': 'test',
'line_num': 10,
'column_num': 100,
}
tests = [
[ GoToTest, 'AnythingYouLike', basic_goto ],
[ GoToTest, 'GoTo', basic_goto ],
[ GoToTest, 'FindAThing', basic_goto ],
[ GoToTest, 'FixItGoto', basic_goto ],
[ GoToListTest, 'AnythingYouLike', [ basic_goto ] ],
[ GoToListTest, 'GoTo', [] ],
[ GoToListTest, 'FixItGoto', [ basic_goto, basic_goto ] ],
]
for test in tests:
yield test[ 0 ], test[ 1 ], test[ 2 ]

View File

@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from ycm.test_utils import MockVimModule, MockVimCommand
from ycm.test_utils import ExtendedMock, MockVimModule, MockVimCommand
MockVimModule()
from ycm import vimsupport
@ -23,6 +23,7 @@ from nose.tools import eq_
from hamcrest import assert_that, calling, raises, none
from mock import MagicMock, call, patch
import os
import json
def ReplaceChunk_SingleLine_Repl_1_test():
@ -239,9 +240,9 @@ def ReplaceChunk_SingleToMultipleLines_test():
# now make another change to the "2nd" line
start, end = _BuildLocations( 2, 3, 2, 4 )
( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk(
start,
end,
( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk(
start,
end,
'cccc',
line_offset,
char_offset,
@ -343,7 +344,6 @@ def ReplaceChunk_SingleToMultipleLinesReplace_2_test():
eq_( char_offset, 4 )
def ReplaceChunk_MultipleLinesToSingleLine_test():
result_buffer = [ "aAa", "aBa", "aCaaaa" ]
start, end = _BuildLocations( 2, 2, 3, 2 )
@ -539,41 +539,517 @@ def _BuildLocations( start_line, start_column, end_line, end_column ):
}
def ReplaceChunksList_SortedChunks_test():
def ReplaceChunksInBuffer_SortedChunks_test():
chunks = [
_BuildChunk( 1, 4, 1, 4, '('),
_BuildChunk( 1, 11, 1, 11, ')' )
]
result_buffer = [ "CT<10 >> 2> ct" ]
vimsupport.ReplaceChunksList( chunks, result_buffer )
vimsupport.ReplaceChunksInBuffer( chunks, result_buffer, None )
expected_buffer = [ "CT<(10 >> 2)> ct" ]
eq_( expected_buffer, result_buffer )
def ReplaceChunksList_UnsortedChunks_test():
def ReplaceChunksInBuffer_UnsortedChunks_test():
chunks = [
_BuildChunk( 1, 11, 1, 11, ')'),
_BuildChunk( 1, 4, 1, 4, '(' )
]
result_buffer = [ "CT<10 >> 2> ct" ]
vimsupport.ReplaceChunksList( chunks, result_buffer )
vimsupport.ReplaceChunksInBuffer( chunks, result_buffer, None )
expected_buffer = [ "CT<(10 >> 2)> ct" ]
eq_( expected_buffer, result_buffer )
def _BuildChunk( start_line, start_column, end_line, end_column,
replacement_text ):
class MockBuffer( ):
"""An object that looks like a vim.buffer object, enough for ReplaceChunk to
generate a location list"""
def __init__( self, lines, name, number ):
self.lines = lines
self.name = name
self.number = number
def __getitem__( self, index ):
return self.lines[ index ]
def __len__( self ):
return len( self.lines )
def __setitem__( self, key, value ):
return self.lines.__setitem__( key, value )
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
return_value=1,
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.BufferIsVisible',
return_value=True,
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.OpenFilename' )
@patch( 'ycm.vimsupport.EchoTextVimWidth', new_callable=ExtendedMock )
@patch( 'vim.eval', new_callable=ExtendedMock )
@patch( 'vim.command', new_callable=ExtendedMock )
def ReplaceChunks_SingleFile_Open_test( vim_command,
vim_eval,
echo_text_vim_width,
open_filename,
buffer_is_visible,
get_buffer_number_for_filename ):
chunks = [
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
]
result_buffer = MockBuffer( [
'line1',
'line2',
'line3',
], 'single_file', 1 )
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
vimsupport.ReplaceChunks( chunks )
# Ensure that we applied the replacement correctly
eq_( result_buffer.lines, [
'replacementline2',
'line3',
] )
# GetBufferNumberForFilename is called twice:
# - once to the check if we would require opening the file (so that we can
# raise a warning)
# - once whilst applying the changes
get_buffer_number_for_filename.assert_has_exact_calls( [
call( 'single_file', False ),
call( 'single_file', False ),
] )
# BufferIsVisible is called twice for the same reasons as above
buffer_is_visible.assert_has_exact_calls( [
call( 1 ),
call( 1 ),
] )
# we don't attempt to open any files
open_filename.assert_not_called()
# But we do set the quickfix list
vim_eval.assert_has_exact_calls( [
call( 'setqflist( {0} )'.format( json.dumps( [ {
'bufnr': 1,
'filename': 'single_file',
'lnum': 1,
'col': 1,
'text': 'replacement',
'type': 'F'
} ] ) ) ),
] )
vim_command.assert_has_calls( [
call( 'copen 1' )
] )
# And it is ReplaceChunks that prints the message showing the number of
# changes
echo_text_vim_width.assert_has_exact_calls( [
call( 'Applied 1 changes' ),
] )
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
side_effect=[ -1, -1, 1 ],
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.BufferIsVisible',
side_effect=[ False, False, True ],
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.OpenFilename',
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.EchoTextVimWidth', new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.Confirm',
return_value=True,
new_callable=ExtendedMock )
@patch( 'vim.eval', return_value=10, new_callable=ExtendedMock )
@patch( 'vim.command', new_callable=ExtendedMock )
def ReplaceChunks_SingleFile_NotOpen_test( vim_command,
vim_eval,
confirm,
echo_text_vim_width,
open_filename,
buffer_is_visible,
get_buffer_number_for_filename ):
chunks = [
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
]
result_buffer = MockBuffer( [
'line1',
'line2',
'line3',
], 'single_file', 1 )
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
vimsupport.ReplaceChunks( chunks )
# We checked if it was OK to open the file
confirm.assert_has_exact_calls( [
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
] )
# Ensure that we applied the replacement correctly
eq_( result_buffer.lines, [
'replacementline2',
'line3',
] )
# GetBufferNumberForFilename is called 3 times. The return values are set in
# the @patch call above:
# - once to the check if we would require opening the file (so that we can
# raise a warning) (-1 return)
# - once whilst applying the changes (-1 return)
# - finally after calling OpenFilename (1 return)
get_buffer_number_for_filename.assert_has_exact_calls( [
call( 'single_file', False ),
call( 'single_file', False ),
call( 'single_file', False ),
] )
# BufferIsVisible is called 3 times for the same reasons as above, with the
# return of each one
buffer_is_visible.assert_has_exact_calls( [
call( -1 ),
call( -1 ),
call( 1 ),
] )
# We open 'single_file' as expected.
open_filename.assert_called_with( 'single_file', {
'focus': True,
'fix': True,
'size': 10
} )
# And close it again, then show the preview window (note, we don't check exact
# calls because there are other calls which are checked elsewhere)
vim_command.assert_has_calls( [
call( 'lclose' ),
call( 'hide' ),
call( 'copen 1' ),
] )
# And update the quickfix list
vim_eval.assert_has_exact_calls( [
call( '&previewheight' ),
call( 'setqflist( {0} )'.format( json.dumps( [ {
'bufnr': 1,
'filename': 'single_file',
'lnum': 1,
'col': 1,
'text': 'replacement',
'type': 'F'
} ] ) ) ),
] )
# And it is ReplaceChunks that prints the message showing the number of
# changes
echo_text_vim_width.assert_has_exact_calls( [
call( 'Applied 1 changes' ),
] )
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
side_effect=[ -1, -1, 1 ],
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.BufferIsVisible',
side_effect=[ False, False, True ],
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.OpenFilename',
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.EchoTextVimWidth',
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.Confirm',
return_value=False,
new_callable=ExtendedMock )
@patch( 'vim.eval',
return_value=10,
new_callable=ExtendedMock )
@patch( 'vim.command', new_callable=ExtendedMock )
def ReplaceChunks_User_Declines_To_Open_File_test(
vim_command,
vim_eval,
confirm,
echo_text_vim_width,
open_filename,
buffer_is_visible,
get_buffer_number_for_filename ):
# Same as above, except the user selects Cancel when asked if they should
# allow us to open lots of (ahem, 1) file.
chunks = [
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
]
result_buffer = MockBuffer( [
'line1',
'line2',
'line3',
], 'single_file', 1 )
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
vimsupport.ReplaceChunks( chunks )
# We checked if it was OK to open the file
confirm.assert_has_exact_calls( [
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
] )
# Ensure that buffer is not changed
eq_( result_buffer.lines, [
'line1',
'line2',
'line3',
] )
# GetBufferNumberForFilename is called once. The return values are set in
# the @patch call above:
# - once to the check if we would require opening the file (so that we can
# raise a warning) (-1 return)
get_buffer_number_for_filename.assert_has_exact_calls( [
call( 'single_file', False ),
] )
# BufferIsVisible is called once for the above file, which wasn't visible.
buffer_is_visible.assert_has_exact_calls( [
call( -1 ),
] )
# We don't attempt to open any files or update any quickfix list or anything
# like that
open_filename.assert_not_called()
vim_eval.assert_not_called()
vim_command.assert_not_called()
echo_text_vim_width.assert_not_called()
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
side_effect=[ -1, -1, 1 ],
new_callable=ExtendedMock )
# Key difference is here: In the final check, BufferIsVisible returns False
@patch( 'ycm.vimsupport.BufferIsVisible',
side_effect=[ False, False, False ],
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.OpenFilename',
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.EchoTextVimWidth',
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.Confirm',
return_value=True,
new_callable=ExtendedMock )
@patch( 'vim.eval',
return_value=10,
new_callable=ExtendedMock )
@patch( 'vim.command',
new_callable=ExtendedMock )
def ReplaceChunks_User_Aborts_Opening_File_test(
vim_command,
vim_eval,
confirm,
echo_text_vim_width,
open_filename,
buffer_is_visible,
get_buffer_number_for_filename ):
# Same as above, except the user selects Abort or Quick during the
# "swap-file-found" dialog
chunks = [
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
]
result_buffer = MockBuffer( [
'line1',
'line2',
'line3',
], 'single_file', 1 )
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
assert_that( calling( vimsupport.ReplaceChunks ).with_args( chunks ),
raises( RuntimeError,
'Unable to open file: single_file\nFixIt/Refactor operation '
'aborted prior to completion. Your files have not been '
'fully updated. Please use undo commands to revert the '
'applied changes.' ) )
# We checked if it was OK to open the file
confirm.assert_has_exact_calls( [
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
] )
# Ensure that buffer is not changed
eq_( result_buffer.lines, [
'line1',
'line2',
'line3',
] )
# We tried to open this file
open_filename.assert_called_with( "single_file", {
'focus': True,
'fix': True,
'size': 10
} )
vim_eval.assert_called_with( "&previewheight" )
# But raised an exception before issuing the message at the end
echo_text_vim_width.assert_not_called()
@patch( 'ycm.vimsupport.GetBufferNumberForFilename', side_effect=[
22, # first_file (check)
-1, # another_file (check)
22, # first_file (apply)
-1, # another_file (apply)
19, # another_file (check after open)
],
new_callable=ExtendedMock )
@patch( 'ycm.vimsupport.BufferIsVisible', side_effect=[
True, # first_file (check)
False, # second_file (check)
True, # first_file (apply)
False, # second_file (apply)
True, # side_effect (check after open)
],
new_callable=ExtendedMock)
@patch( 'ycm.vimsupport.OpenFilename',
new_callable=ExtendedMock)
@patch( 'ycm.vimsupport.EchoTextVimWidth',
new_callable=ExtendedMock)
@patch( 'ycm.vimsupport.Confirm', return_value=True,
new_callable=ExtendedMock)
@patch( 'vim.eval', return_value=10,
new_callable=ExtendedMock)
@patch( 'vim.command',
new_callable=ExtendedMock)
def ReplaceChunks_MultiFile_Open_test( vim_command,
vim_eval,
confirm,
echo_text_vim_width,
open_filename,
buffer_is_visible,
get_buffer_number_for_filename ):
# Chunks are split across 2 files, one is already open, one isn't
chunks = [
_BuildChunk( 1, 1, 2, 1, 'first_file_replacement ', '1_first_file' ),
_BuildChunk( 2, 1, 2, 1, 'second_file_replacement ', '2_another_file' ),
]
first_file = MockBuffer( [
'line1',
'line2',
'line3',
], '1_first_file', 22 )
another_file = MockBuffer( [
'another line1',
'ACME line2',
], '2_another_file', 19 )
vim_buffers = [ None ] * 23
vim_buffers[ 22 ] = first_file
vim_buffers[ 19 ] = another_file
with patch( 'vim.buffers', vim_buffers ):
vimsupport.ReplaceChunks( chunks )
# We checked for the right file names
get_buffer_number_for_filename.assert_has_exact_calls( [
call( '1_first_file', False ),
call( '2_another_file', False ),
call( '1_first_file', False ),
call( '2_another_file', False ),
call( '2_another_file', False ),
] )
# We checked if it was OK to open the file
confirm.assert_has_exact_calls( [
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
] )
# Ensure that buffers are updated
eq_( another_file.lines, [
'another line1',
'second_file_replacement ACME line2',
] )
eq_( first_file.lines, [
'first_file_replacement line2',
'line3',
] )
# We open '2_another_file' as expected.
open_filename.assert_called_with( '2_another_file', {
'focus': True,
'fix': True,
'size': 10
} )
# And close it again, then show the preview window (note, we don't check exact
# calls because there are other calls which are checked elsewhere)
vim_command.assert_has_calls( [
call( 'lclose' ),
call( 'hide' ),
call( 'copen 2' ),
] )
# And update the quickfix list with each entry
vim_eval.assert_has_exact_calls( [
call( '&previewheight' ),
call( 'setqflist( {0} )'.format( json.dumps( [ {
'bufnr': 22,
'filename': '1_first_file',
'lnum': 1,
'col': 1,
'text': 'first_file_replacement ',
'type': 'F'
}, {
'bufnr': 19,
'filename': '2_another_file',
'lnum': 2,
'col': 1,
'text': 'second_file_replacement ',
'type': 'F'
} ] ) ) ),
] )
# And it is ReplaceChunks that prints the message showing the number of
# changes
echo_text_vim_width.assert_has_exact_calls( [
call( 'Applied 2 changes' ),
] )
def _BuildChunk( start_line,
start_column,
end_line,
end_column,
replacement_text, filepath='test_file_name' ):
return {
'range': {
'start': {
'filepath': filepath,
'line_num': start_line,
'column_num': start_column,
},
'end': {
'filepath': filepath,
'line_num': end_line,
'column_num': end_column,
},
@ -582,14 +1058,14 @@ def _BuildChunk( start_line, start_column, end_line, end_column,
}
@patch( 'vim.command' )
@patch( 'vim.current' )
@patch( 'vim.command', new_callable=ExtendedMock )
@patch( 'vim.current', new_callable=ExtendedMock)
def WriteToPreviewWindow_test( vim_current, vim_command ):
vim_current.window.options.__getitem__ = MagicMock( return_value = True )
vimsupport.WriteToPreviewWindow( "test" )
vim_command.assert_has_calls( [
vim_command.assert_has_exact_calls( [
call( 'silent! pclose!' ),
call( 'silent! pedit! _TEMP_FILE_' ),
call( 'silent! wincmd P' ),
@ -598,7 +1074,9 @@ def WriteToPreviewWindow_test( vim_current, vim_command ):
vim_current.buffer.__setitem__.assert_called_with(
slice( None, None, None ), [ 'test' ] )
vim_current.buffer.options.__setitem__.assert_has_calls( [
vim_current.buffer.options.__setitem__.assert_has_exact_calls( [
call( 'modifiable', True ),
call( 'readonly', False ),
call( 'buftype', 'nofile' ),
call( 'swapfile', False ),
call( 'modifiable', False ),
@ -616,14 +1094,14 @@ def WriteToPreviewWindow_MultiLine_test( vim_current ):
slice( None, None, None ), [ 'test', 'test2' ] )
@patch( 'vim.command' )
@patch( 'vim.current' )
@patch( 'vim.command', new_callable=ExtendedMock )
@patch( 'vim.current', new_callable=ExtendedMock )
def WriteToPreviewWindow_JumpFail_test( vim_current, vim_command ):
vim_current.window.options.__getitem__ = MagicMock( return_value = False )
vimsupport.WriteToPreviewWindow( "test" )
vim_command.assert_has_calls( [
vim_command.assert_has_exact_calls( [
call( 'silent! pclose!' ),
call( 'silent! pedit! _TEMP_FILE_' ),
call( 'silent! wincmd P' ),
@ -634,15 +1112,15 @@ def WriteToPreviewWindow_JumpFail_test( vim_current, vim_command ):
vim_current.buffer.options.__setitem__.assert_not_called()
@patch( 'vim.command' )
@patch( 'vim.current' )
@patch( 'vim.command', new_callable=ExtendedMock )
@patch( 'vim.current', new_callable=ExtendedMock )
def WriteToPreviewWindow_JumpFail_MultiLine_test( vim_current, vim_command ):
vim_current.window.options.__getitem__ = MagicMock( return_value = False )
vimsupport.WriteToPreviewWindow( "test\ntest2" )
vim_command.assert_has_calls( [
vim_command.assert_has_exact_calls( [
call( 'silent! pclose!' ),
call( 'silent! pedit! _TEMP_FILE_' ),
call( 'silent! wincmd P' ),
@ -689,7 +1167,9 @@ def BufferIsVisibleForFilename_test():
eq_( vimsupport.BufferIsVisibleForFilename( 'another_filename' ), False )
@patch( 'vim.command', side_effect = MockVimCommand )
@patch( 'vim.command',
side_effect = MockVimCommand,
new_callable=ExtendedMock )
def CloseBuffersForFilename_test( vim_command ):
buffers = [
{
@ -709,14 +1189,14 @@ def CloseBuffersForFilename_test( vim_command ):
with patch( 'vim.buffers', buffers ):
vimsupport.CloseBuffersForFilename( 'some_filename' )
vim_command.assert_has_calls( [
vim_command.assert_has_exact_calls( [
call( 'silent! bwipeout! 2' ),
call( 'silent! bwipeout! 5' )
], any_order = True )
@patch( 'vim.command' )
@patch( 'vim.current' )
@patch( 'vim.command', new_callable=ExtendedMock )
@patch( 'vim.current', new_callable=ExtendedMock )
def OpenFilename_test( vim_current, vim_command ):
# Options used to open a logfile
options = {
@ -728,18 +1208,18 @@ def OpenFilename_test( vim_current, vim_command ):
vimsupport.OpenFilename( __file__, options )
vim_command.assert_has_calls( [
call( 'silent! 12split {0}'.format( __file__ ) ),
vim_command.assert_has_exact_calls( [
call( '12split {0}'.format( __file__ ) ),
call( "exec "
"'au BufEnter <buffer> :silent! checktime {0}'".format( __file__ ) ),
call( 'silent! normal G zz' ),
call( 'silent! wincmd p' )
] )
vim_current.buffer.options.__setitem__.assert_has_calls( [
vim_current.buffer.options.__setitem__.assert_has_exact_calls( [
call( 'autoread', True ),
] )
vim_current.window.options.__setitem__.assert_has_calls( [
vim_current.window.options.__setitem__.assert_has_exact_calls( [
call( 'winfixheight', True )
] )

View File

@ -20,6 +20,7 @@ import os
import tempfile
import json
import re
from collections import defaultdict
from ycmd.utils import ToUtf8IfNeeded
from ycmd import user_options_store
@ -28,6 +29,13 @@ BUFFER_COMMAND_MAP = { 'same-buffer' : 'edit',
'vertical-split' : 'vsplit',
'new-tab' : 'tabedit' }
FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT = (
'The requested operation will apply changes to {0} files which are not '
'currently open. This will therefore open {0} new files in the hidden '
'buffers. The quickfix list can then be used to review the changes. No '
'files will be written to disk. Do you wish to continue?' )
def CurrentLineAndColumn():
"""Returns the 0-based current line and 0-based current column."""
# See the comment in CurrentColumn about the calculation for the line and
@ -236,6 +244,15 @@ def SetLocationList( diagnostics ):
vim.eval( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
def SetQuickFixList( quickfix_list, display=False ):
"""list should be in qflist format: see ":h setqflist" for details"""
vim.eval( 'setqflist( {0} )'.format( json.dumps( quickfix_list ) ) )
if display:
vim.command( 'copen {0}'.format( len( quickfix_list ) ) )
JumpToPreviousWindow()
def ConvertDiagnosticsToQfList( diagnostics ):
def ConvertDiagnosticToQfFormat( diagnostic ):
# See :h getqflist for a description of the dictionary fields.
@ -428,6 +445,8 @@ def PresentDialog( message, choices, default_choice_index = 0 ):
def Confirm( message ):
"""Display |message| with Ok/Cancel operations. Returns True if the user
selects Ok"""
return bool( PresentDialog( message, [ "Ok", "Cancel" ] ) == 0 )
@ -449,6 +468,7 @@ def EchoTextVimWidth( text ):
old_ruler = GetIntValue( '&ruler' )
old_showcmd = GetIntValue( '&showcmd' )
vim.command( 'set noruler noshowcmd' )
vim.command( 'redraw' )
EchoText( truncated_text, False )
@ -490,9 +510,145 @@ def GetIntValue( variable ):
return int( vim.eval( variable ) )
def ReplaceChunksList( chunks, vim_buffer = None ):
if vim_buffer is None:
vim_buffer = vim.current.buffer
def _SortChunksByFile( chunks ):
"""Sort the members of the list |chunks| (which must be a list of dictionaries
conforming to ycmd.responses.FixItChunk) by their filepath. Returns a new
list in arbitrary order."""
chunks_by_file = defaultdict( list )
for chunk in chunks:
filepath = chunk[ 'range' ][ 'start' ][ 'filepath' ]
chunks_by_file[ filepath ].append( chunk )
return chunks_by_file
def _GetNumNonVisibleFiles( file_list ):
"""Returns the number of file in the iterable list of files |file_list| which
are not curerntly open in visible windows"""
return len(
[ f for f in file_list
if not BufferIsVisible( GetBufferNumberForFilename( f, False ) ) ] )
def _OpenFileInSplitIfNeeded( filepath ):
"""Ensure that the supplied filepath is open in a visible window, opening a
new split if required. Returns the buffer number of the file and an indication
of whether or not a new split was opened.
If the supplied filename is already open in a visible window, return just
return its buffer number. If the supplied file is not visible in a window
in the current tab, opens it in a new vertical split.
Returns a tuple of ( buffer_num, split_was_opened ) indicating the buffer
number and whether or not this method created a new split. If the user opts
not to open a file, or if opening fails, this method raises RuntimeError,
otherwise, guarantees to return a visible buffer number in buffer_num."""
buffer_num = GetBufferNumberForFilename( filepath, False )
# We only apply changes in the current tab page (i.e. "visible" windows).
# Applying changes in tabs does not lead to a better user experience, as the
# quickfix list no longer works as you might expect (doesn't jump into other
# tabs), and the complexity of choosing where to apply edits is significant.
if BufferIsVisible( buffer_num ):
# file is already open and visible, just return that buffer number (and an
# idicator that we *didn't* open a split)
return ( buffer_num, False )
# The file is not open in a visible window, so we open it in a split.
# We open the file with a small, fixed height. This means that we don't
# make the current buffer the smallest after a series of splits.
OpenFilename( filepath, {
'focus': True,
'fix': True,
'size': GetIntValue( '&previewheight' ),
} )
# OpenFilename returns us to the original cursor location. This is what we
# want, because we don't want to disorientate the user, but we do need to
# know the (now open) buffer number for the filename
buffer_num = GetBufferNumberForFilename( filepath, False )
if not BufferIsVisible( buffer_num ):
# This happens, for example, if there is a swap file and the user
# selects the "Quit" or "Abort" options. We just raise an exception to
# make it clear to the user that the abort has left potentially
# partially-applied changes.
raise RuntimeError(
'Unable to open file: {0}\nFixIt/Refactor operation '
'aborted prior to completion. Your files have not been '
'fully updated. Please use undo commands to revert the '
'applied changes.'.format( filepath ) )
# We opened this file in a split
return ( buffer_num, True )
def ReplaceChunks( chunks ):
"""Apply the source file deltas supplied in |chunks| to arbitrary files.
|chunks| is a list of changes defined by ycmd.responses.FixItChunk,
which may apply arbitrary modifications to arbitrary files.
If a file specified in a particular chunk is not currently open in a visible
buffer (i.e., one in a window visible in the current tab), we:
- issue a warning to the user that we're going to open new files (and offer
her the option to abort cleanly)
- open the file in a new split, make the changes, then hide the buffer.
If for some reason a file could not be opened or changed, raises RuntimeError.
Otherwise, returns no meaningful value."""
# We apply the edits file-wise for efficiency, and because we must track the
# file-wise offset deltas (caused by the modifications to the text).
chunks_by_file = _SortChunksByFile( chunks )
# We sort the file list simply to enable repeatable testing
sorted_file_list = sorted( chunks_by_file.iterkeys() )
# Make sure the user is prepared to have her screen mutilated by the new
# buffers
num_files_to_open = _GetNumNonVisibleFiles( sorted_file_list )
if num_files_to_open > 0:
if not Confirm(
FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( num_files_to_open ) ):
return
# Store the list of locations where we applied changes. We use this to display
# the quickfix window showing the user where we applied changes.
locations = []
for filepath in sorted_file_list:
( buffer_num, close_window ) = _OpenFileInSplitIfNeeded( filepath )
ReplaceChunksInBuffer( chunks_by_file[ filepath ],
vim.buffers[ buffer_num ],
locations )
# When opening tons of files, we don't want to have a split for each new
# file, as this simply does not scale, so we open the window, make the
# edits, then hide the window.
if close_window:
# Some plugins (I'm looking at you, syntastic) might open a location list
# for the window we just opened. We don't want that location list hanging
# around, so we close it. lclose is a no-op if there is no location list.
vim.command( 'lclose' )
# Note that this doesn't lose our changes. It simply "hides" the buffer,
# which can later be re-accessed via the quickfix list or `:ls`
vim.command( 'hide' )
# Open the quickfix list, populated with entries for each location we changed.
if locations:
SetQuickFixList( locations, True )
EchoTextVimWidth( "Applied " + str( len( chunks ) ) + " changes" )
def ReplaceChunksInBuffer( chunks, vim_buffer, locations ):
"""Apply changes in |chunks| to the buffer-like object |buffer|. Append each
chunk's start to the list |locations|"""
# We need to track the difference in length, but ensuring we apply fixes
# in ascending order of insertion point.
@ -519,7 +675,8 @@ def ReplaceChunksList( chunks, vim_buffer = None ):
chunk[ 'range' ][ 'end' ],
chunk[ 'replacement_text' ],
line_delta, char_delta,
vim_buffer )
vim_buffer,
locations )
line_delta += new_line_delta
char_delta += new_char_delta
@ -534,11 +691,12 @@ def ReplaceChunksList( chunks, vim_buffer = None ):
# returns the delta (in lines and characters) that any position after the end
# needs to be adjusted by.
def ReplaceChunk( start, end, replacement_text, line_delta, char_delta,
vim_buffer ):
vim_buffer, locations = None ):
# ycmd's results are all 1-based, but vim's/python's are all 0-based
# (so we do -1 on all of the values)
start_line = start[ 'line_num' ] - 1 + line_delta
end_line = end[ 'line_num' ] - 1 + line_delta
source_lines_count = end_line - start_line + 1
start_column = start[ 'column_num' ] - 1 + char_delta
end_column = end[ 'column_num' ] - 1
@ -563,6 +721,17 @@ def ReplaceChunk( start, end, replacement_text, line_delta, char_delta,
vim_buffer[ start_line : end_line + 1 ] = replacement_lines[:]
if locations is not None:
locations.append( {
'bufnr': vim_buffer.number,
'filename': vim_buffer.name,
# line and column numbers are 1-based in qflist
'lnum': start_line + 1,
'col': start_column + 1,
'text': replacement_text,
'type': 'F',
} )
new_line_delta = replacement_lines_count - source_lines_count
return ( new_line_delta, new_char_delta )
@ -710,30 +879,39 @@ def OpenFilename( filename, options = {} ):
size = ( options.get( 'size', '' ) if command in [ 'split', 'vsplit' ] else
'' )
focus = options.get( 'focus', False )
watch = options.get( 'watch', False )
position = options.get( 'position', 'start' )
# There is no command in Vim to return to the previous tab so we need to
# remember the current tab if needed.
if not focus and command is 'tabedit':
previous_tab = GetIntValue( 'tabpagenr()' )
else:
previous_tab = None
# Open the file
CheckFilename( filename )
vim.command( 'silent! {0}{1} {2}'.format( size, command, filename ) )
try:
vim.command( '{0}{1} {2}'.format( size, command, filename ) )
# When the file we are trying to jump to has a swap file,
# Vim opens swap-exists-choices dialog and throws vim.error with E325 error,
# or KeyboardInterrupt after user selects one of the options which actually
# opens the file (Open read-only/Edit anyway).
except vim.error as e:
if 'E325' not in str( e ):
raise
if command is 'split':
vim.current.window.options[ 'winfixheight' ] = options.get( 'fix', False )
if command is 'vsplit':
vim.current.window.options[ 'winfixwidth' ] = options.get( 'fix', False )
# Otherwise, the user might have chosen Quit. This is detectable by the
# current file not being the target file
if filename != GetCurrentBufferFilepath():
return
except KeyboardInterrupt:
# Raised when the user selects "Abort" after swap-exists-choices
return
if watch:
vim.current.buffer.options[ 'autoread' ] = True
vim.command( "exec 'au BufEnter <buffer> :silent! checktime {0}'"
.format( filename ) )
if position is 'end':
vim.command( 'silent! normal G zz' )
_SetUpLoadedBuffer( command,
filename,
options.get( 'fix', False ),
options.get( 'position', 'start' ),
options.get( 'watch', False ) )
# Vim automatically set the focus to the opened file so we need to get the
# focus back (if the focus option is disabled) when opening a new tab or
@ -743,3 +921,22 @@ def OpenFilename( filename, options = {} ):
JumpToTab( previous_tab )
if command in [ 'split', 'vsplit' ]:
JumpToPreviousWindow()
def _SetUpLoadedBuffer( command, filename, fix, position, watch ):
"""After opening a buffer, configure it according to the supplied options,
which are as defined by the OpenFilename method."""
if command is 'split':
vim.current.window.options[ 'winfixheight' ] = fix
if command is 'vsplit':
vim.current.window.options[ 'winfixwidth' ] = fix
if watch:
vim.current.buffer.options[ 'autoread' ] = True
vim.command( "exec 'au BufEnter <buffer> :silent! checktime {0}'"
.format( filename ) )
if position is 'end':
vim.command( 'silent! normal G zz' )