IDE中,選中一個變數,文檔中其它地方的該變數也會高亮,這種功能叫什麼?如何實現的?

如Visual Studio,給出一個叫法就好了....


這種功能可以有很多不同的叫法,但其本質都是搜索一個語法元素(例如一個變數名)所代表的實體(例如一個變數)的所有被引用的地方(occurrences / references)。

這種功能的實現要點有這麼幾個:parser、AST、符號表。

  • IDE要有一個語法分析器(parser),能夠把輸入的源碼分析(parse)為一棵抽象語法樹(AST),並將AST里出現的符號及其所引用的實體之間的對應關係記錄在一種叫做符號表(symbol table)的數據結構里。這個動作既可以事先批量進行,也可以在用戶編輯的同時增量式進行——有沒有覺得有時候IDE里各種提示過了一小會兒才出現?這背後就很可能是增量式分析在進行中。Eclipse的增量式語法分析在這個文檔里有提到:FAQ How do I use a model reconciler?
  • 用戶在編輯器中選中一段文本時,IDE要可以找出這段文本對應的AST節點,並且通過符號表找到它所對應的實體。例如從一個簡單標識符找到它對應的局部變數聲明。
  • 接下來,在給定的AST範圍內(例如說一個方法內),找出所有其它引用同一實體的AST節點。
  • 最後在編輯器里對找到的這些節點進行高亮。

Visual Studio代碼不開源所以不方便用來討論實現。

在IntelliJ里,這個功能叫做「Highlight Usages」:IntelliJ IDEA 15.0
Help ::
Highlighting Usages

拿開源的Eclipse的Java Development Tools(JDT)為例。這個功能在Eclipse JDT里叫做「mark occurrences」:Mark occurrences - Tips and Tricks (JDT)

這個功能的實現可以從下面的鏈接開始跟蹤:

windowActivated() - eclipse.jdt.ui/JavaEditor.java at master · eclipse/eclipse.jdt.ui · GitHub

updateOccurrenceAnnotations() - eclipse.jdt.ui/JavaEditor.java at master · eclipse/eclipse.jdt.ui · GitHub

為舉例方便,來詳細看看其中在第3320行的一段:

if (locations == null selectedNode instanceof Name) {
IBinding binding= ((Name)selectedNode).resolveBinding();
if (binding != null markOccurrencesOfType(binding)) {
OccurrencesFinder finder= new OccurrencesFinder();
if (finder.initialize(astRoot, selectedNode) == null) {
locations= finder.getOccurrences();
}
}
}

可以看到,假如在編譯器中被選定的AST節點是一個「名字」(例如說標識符),那麼接下來要做的事情就是:

  1. 通過符號表找出這個標識符所引用的實體是什麼,這段代碼里就是resolveBinding()(追蹤實現可以到DefaultBindingResolver.resolveName()),得到一個IBinding。
  2. 在給定的AST範圍內,找出這個實體被引用的所有位置(occurrences)。
  3. 在編輯器里把找出來的位置設上高亮。

漁就給這麼多,接下來題主可以自己探索下去啦~

=============================================

題主說了「例如VS」,所以這裡的回答假定題主想知道的是高質量的「找出引用」功能。

如果不要求質量很高,也有更簡單的做法。補充個例子說明啥叫質量高。

假如有這樣的Java代碼:

class Foo {
int bar() { // method name "bar"
int bar = 1; // local variable "bar" 1
{
int bar = 2; // local variable "bar" 2
int baz = bar + 1; // local variable "bar" 2
}
return bar; // local variable "bar" 1
}
}

這段代碼里一共出現了5次「bar」標識符,但其實背後是3個不同的符號:

  • 方法"bar"

  • 局部變數"bar" 1
  • 局部變數"bar" 2

按照前面說的方法做分析的話,這裡會得到如下的抽象語法樹:(僅為講解而寫的簡單例子)

ClassDeclaration "Foo"
- MemberList
- MethodDeclaration "bar"
- signature "()I"
- methodBody
- StatementOrDeclarationList
- VariableDeclaration "bar" // 1
- initializer
- ConstantExpression 1
- BlockStatement
- VariableDeclaration "bar" // 2
- initializer
- ConstantExpression 2
- VariableDeclaration "baz"
- initializer
- BinaryExpression
- op +
- left
- VariableReference "bar" // 2
- right
- ConstantExpression 1
- ReturnStatement
- value
- VariableReference "bar" // 1

可以看到,通過語法分析,構造出來的AST足以區分5次"bar"出現的地方各自代表的意義。所以,例如選定baz的初始化表達式中的bar時,就只有局部變數"bar" 2會被高亮,而另外兩個"bar"實體都不會被高亮。

而比較簡單但低質量的做法就是從頭到尾都只做詞法分析——只將輸入的源碼切分為一個個單詞。

這樣做就會把本例子的5個"bar"都識別為同一個實體,標識的精度就變差了。

換個更簡單的例子再體會一下:

public void setFoo(int foo) { // parameter "foo"
this.foo = foo; // field "foo" / paramater "foo"
}

同樣,做足語法分析就能區分this.foo與foo,而只做詞法分析就會只能把它們看為同一實體。


我是來補充一下,C#的,首先技術基本就是就是 @RednaxelaFX所說的。

其實題主可以換種思考方式思考這個問題。我們在寫完程序編譯的時候,會對進syntax進行檢查,像靜態語言比如C/C++ java這種,假如我使用一個未聲明過的變數或者函數,編譯器就會告訴你「嘿,你用的這個函數或者變數我沒見過」也就是語法錯誤。我們逆著想為啥編譯器會知道,肯定是編譯器在讀這段代碼的時候,會去找是否存在「定義」。如果編譯器在做這個事情的同時,在這兩者之前建立一個map,是不是我就可以找一個變數,然後找到定義然後找到所有用到這個定義的地方:)

你用到到的地方其實在編譯器看來就是AST裡面的一個node, AST是啥看 @RednaxelaFX答案就知道了。而你的定義其實就是可以理解為symbol。

簡單點說symbol裡面有很多東西比如:

1.這個「定義」在哪裡

2.這個定義是static, abstract

當然編譯器裡面還有更多的東西,有興趣題主可以看看這個Reference Source這個是C#編譯器roslyn裡面對symbol的介面的定義。

而symbol和AST node的映射,就是semantic model. 我google了一下,這篇文章的介紹很不錯Learn Roslyn Now: Part 7 Introducing the Semantic Model 。

回到題目,過去其實IDE是要自己parse代碼才能做到這種事情。但想想其實這種事情編譯器做豈不是更合適!所以roslyn就是把很多編譯器裡面有用的功能做了定義,設計了api。所以現在IDE實現這種功能很簡單,比如下面這段code(這段code出自stack overflow的一個答案c# - Finding all references to a method with Roslyn):

string text = @"
class C
{
void M()
{
M();
M();
}
}";
var sourceText = SourceText.From(text);
//Create new document
var doc = ws.AddDocument(project.Id, "NewDoc", sourceText);
//Get the semantic model
var model = doc.GetSemanticModelAsync().Result;
//Get the syntax node for the first invocation to M()
var methodInvocation = doc.GetSyntaxRootAsync().Result.DescendantNodes().OfType&().First();
var methodSymbol = model.GetSymbolInfo(methodInvocation).Symbol;
//Finds all references to M()
var referencesToM = SymbolFinder.FindReferencesAsync(methodSymbol, doc.Project.Solution).Result;

最後還是微軟大法好呀!!


要做成 Eclipse 這樣就你就慢慢弄 ast 吧:

要快速實現,字元串匹配也就夠了:

大部分文本編輯軟體都有這個功能,就是簡單的分割單詞 + 字元串匹配啊,了不起可以根據不同文件擴展名設定.c或者.php的單詞分割的正則表達式而已。如圖,隨便抓篇文章下來,選中一個單詞,其他位置同樣的單詞也黃了,新聞稿沒有ast。Visual Code 也是一樣的,效果也差不多。

加點花招的話,根據 { }來匹配作用域,作用域的開始和結束,也可以根據擴展名配置到文本編輯器里,比如 editplus 配置作用域為 { 和 }:

配置函數命名規則:


occurrences match or occurrences highlight


It"s so called regular expression


關聯高亮,這個很簡單的,是ide的一項很實用的功能,兩種方法都可實現,一種是預先ide或第三方插件就對打開的文檔進行了梳理,函數和變數都被組織成樹壯,點擊後只要查找相關分支並找到關聯位置並高亮就好了;另一種就更簡單了,查找整篇文檔並做簡單語法分析(區別聲明和調用,使用略帶區別的高亮顏色),這不會有任何效率問題的,比如很多文檔編輯器


最簡單的方法,將選定的實體(字元串)作為查找項,查詢代碼中的匹配項,然後判斷是否應當高亮,比如如果前後均為非字母數字,則高亮,否則忽略(是某個變數的一部分而不是這個變數),這個實現代碼可能簡單,但是容易出疏漏。

最省事且完備的方法是執行詞法分析,詞法分析完成後,源程序可以視為由「詞」ID構成的序列,選定一個詞,可以通過符號表反查其ID,然後在這個序列中查找所有的相同ID,並得到該實體的位置信息(開始位置和結束位置),將其選定高亮。

語法分析完全沒有必要


推薦閱讀:

2017你覺得未來五年最具前景的一門編程語言是什麼?
是否有函數式編程的優秀實例?

TAG:程序員 | JavaScript | 集成開發環境 | 編譯原理 |