As a very high-level overview, the IDE contains a compiler. (Well, most parts of a compiler: it doesn't need to generate code or optimize, but all the rest is there, lexing, parsing, semantic analysis, type inference, type checking, macro expansion, symbol resolution, etc.)
From the information gleaned from this analysis, the IDE constructs a semantic model of the code, and then, when it encounters incomplete code, it uses sufficiently advanced magic to figure out how best to complete it. (A simple algorithm would be to offer the shortest possible completion, but normally, IDEs are much more sophisticated than that.)
Because of the code duplication between IDEs and compilers, in recent years, there have been efforts to integrate the two. E.g. Microsoft's Roslyn compiler for C# and Visual Basic.NET was explicitly designed with APIs that allow an IDE to access all the required information. Likewise, the nsc
(New Scala Compiler) and dotc
compilers for Scala, and the Clang compiler for C / C++ have APIs for embedding into an IDE.
Note that the compiler built into an IDE has some different requirements from a classic batch compiler: it needs to be asynchronous, reactive, concurrent, fast, incremental, and most of the time, the code it deals with will be incomplete, invalid, and have errors. However, even despite these conflicting requirements, it makes sense to merge the two into one, because this guarantees that the IDE and the compiler always have the same understanding of the code.
As a counter-example, IntelliJ IDEA uses its own compiler framework. IDEA uses a single language-agnostic semantic graph for the entire project, no matter how many different languages are used within the project. This allows it to have really cool features such as automatically converting code between different languages, or refactoring across languages in a polyglot project. But, it runs into precisely the problem I mentioned above, especially often with Scala, where IntelliJ shows errors for code that actually compiles fine with the Scala compiler or vice versa.
Microsoft has developed the Language Server Protocol, which is an API that allows IDEs to communicate with compilers using a standardized protocol. This means that compilers that implement the LSP will automatically work with every IDE that implements the LSP, and likewise, IDEs that implement the LSP will automatically support every language for which a compiler exists that implements the LSP. Nowadays, lots of compilers (e.g. the tsc
TypeScript compiler, Idris, Scala) and IDEs (Visual Studio Code, Emacs, Vim) implement it.
In the same vein and based on the success of the LSP, there is now an effort by the Scala community to define a Build Server Protocol that allows IDEs to abstract over build tools (SBT, Maven, Gradle, Mill).
Addendum: Everything I wrote above, applies to "good™️" IDEs with semantic features. There are much, much, simpler IDEs that, for example, simply offer every word (even from comments) of the current file as completions, regardless of whether that word is even syntactically legal in that context.