[algorithm] 코드 완성은 어떻게 작동합니까?
많은 편집기와 IDE에는 코드 완성 기능이 있습니다. 그들 중 일부는 매우 “지능적”이고 다른 일부는 그렇지 않습니다. 나는 더 지능적인 유형에 관심이 있습니다. 예를 들어, a) 현재 범위에서 사용 가능 b) 반환 값이 유효한 경우에만 함수를 제공하는 IDE를 보았습니다. (예를 들어 “5 + foo [tab]”뒤에는 정수 또는 올바른 유형의 변수 이름에 추가 할 수있는 것을 반환하는 함수 만 제공됩니다.) 또한 더 자주 사용되거나 가장 긴 옵션을 앞에 배치하는 것을 보았습니다. 목록의.
코드를 구문 분석해야한다는 것을 알고 있습니다. 그러나 일반적으로 현재 코드를 편집하는 동안 유효하지 않은 구문 오류가 있습니다. 불완전하고 오류가있는 경우 어떻게 구문 분석합니까?
시간 제약도 있습니다. 목록을 만드는 데 몇 초가 걸리면 완성은 쓸모가 없습니다. 때로는 완료 알고리즘이 수천 개의 클래스를 처리합니다.
이를위한 좋은 알고리즘과 데이터 구조는 무엇입니까?
답변
제 UnrealScript 언어 서비스 제품의 IntelliSense 엔진은 복잡하지만 여기서 가능한 한 최선의 개요를 제공하겠습니다. VS2008 SP1의 C # 언어 서비스는 제 성능 목표입니다. 아직 거기에는 없지만 Ctrl + space 또는 사용자가 .
(점)을 입력 할 때까지 기다리지 않고 단일 문자를 입력 한 후 안전하게 제안을 제공 할 수있을만큼 빠르고 정확합니다 . [언어 서비스에 종사하는] 사람들이이 주제에 대해 더 많은 정보를 얻을수록, 그들의 제품을 사용하면 더 나은 최종 사용자 경험을 얻게됩니다. 불행한 경험을하면서 세부 사항에 그다지 세심한주의를 기울이지 않은 제품이 많이 있으며 결과적으로 코딩보다 IDE와 더 많이 싸우고있었습니다.
내 언어 서비스에서는 다음과 같이 배치됩니다.
- 커서에서 표현식을 가져옵니다. 이는 멤버 액세스 식 의 시작 부분 에서 커서가있는 식별자의 끝까지 이동합니다. 멤버 액세스 식은 일반적으로 형식
aa.bb.cc
이지만에서와 같이 메서드 호출을 포함 할 수도 있습니다aa.bb(3+2).cc
. - 가져 오기 상황에 맞는 커서 주변을. 이것은 컴파일러와 항상 동일한 규칙을 따르지 않기 때문에 매우 까다 롭습니다 (긴 이야기). 여기서는 그렇게한다고 가정합니다. 일반적으로 이것은 커서가있는 메서드 / 클래스에 대한 캐시 된 정보를 가져 오는 것을 의미합니다.
- 컨텍스트 객체가을 구현한다고 가정 해 보겠습니다
IDeclarationProvider
. 여기서 호출 하여 범위에 표시되는 모든 항목GetDeclarations()
을 가져올IEnumerable<IDeclaration>
수 있습니다. 필자의 경우이 목록에는 지역 / 매개 변수 (메서드에있는 경우), 멤버 (필드 및 메서드, 인스턴스 메서드가 아닌 경우에만 정적, 기본 유형의 전용 멤버 없음), 전역 (언어 I의 유형 및 상수)이 포함됩니다. 작업 중) 및 키워드. 이 목록에는 이름이있는 항목이 있습니다aa
. # 1의 발현을 평가하는 첫 번째 단계로, 우리는 이름으로 컨텍스트 열거에서 항목을 선택aa
우리에게주는IDeclaration
다음 단계. - 다음으로 연산자를
IDeclaration
표현aa
에 적용하여IEnumerable<IDeclaration>
“멤버”(어떤 의미에서)를 포함하는 다른 연산자를 얻습니다aa
. 때문에.
작업자가에서 다른->
사업자, 나는 전화declaration.GetMembers(".")
와 기대IDeclaration
를 올바르게에 객체가 나열 연산자를 적용 할 수 있습니다. - 이것은 내가 명중 할 때까지 계속됩니다
cc
. 여기서 선언 목록 은 이름을 가진 객체를 포함 할 수도 있고 포함 하지 않을 수도 있습니다cc
. 아시다시피으로 시작하는 항목이 여러 개이면 해당 항목도cc
표시되어야합니다. 최종 열거를 가져와 문서화 된 알고리즘 을 통해 전달 하여 사용자에게 가능한 가장 유용한 정보를 제공 함으로써이 문제를 해결 합니다.
IntelliSense 백엔드에 대한 몇 가지 추가 참고 사항은 다음과 같습니다.
- .NET 구현시 LINQ의 지연 평가 메커니즘을 광범위하게 사용합니다
GetMembers
. 내 캐시의 각 개체는 구성원에게 평가하는 펑터를 제공 할 수 있으므로 트리로 복잡한 작업을 수행하는 것은 거의 사소한 일입니다. - 대신 유지하는 각 개체의
List<IDeclaration>
회원들의를, 나는 계속List<Name>
여기서Name
구성원을 설명하는 특별한 형식의 문자열의 해시를 포함하는 구조체입니다. 이름을 개체에 매핑하는 엄청난 캐시가 있습니다. 이렇게하면 파일을 다시 구문 분석 할 때 파일에 선언 된 모든 항목을 캐시에서 제거하고 업데이트 된 멤버로 다시 채울 수 있습니다. 펑터가 구성된 방식으로 인해 모든 표현식은 즉시 새 항목으로 평가됩니다.
IntelliSense “프런트 엔드”
사용자가 입력 할 때 파일 은 올바른 것보다 더 자주 구문 적으로 부정확 합니다. 따라서 사용자가 입력 할 때 캐시 섹션을 임의로 제거하고 싶지 않습니다. 가능한 한 빨리 증분 업데이트를 처리하기 위해 많은 특수 사례 규칙이 있습니다. 증분 캐시는 열린 파일에 대해서만 로컬로 유지되며 사용자가 입력으로 인해 백엔드 캐시가 파일의 각 방법과 같은 항목에 대해 잘못된 행 / 열 정보를 보유하고 있음을 사용자가 인식하지 못하도록합니다.
- 하나의 구속 요소는 내 파서가 빠르다는 것 입니다. 우선 순위가 낮은 백그라운드 스레드에서 독립적으로 작동하면서 150ms 안에 20000 라인 소스 파일의 전체 캐시 업데이트를 처리 할 수 있습니다. 이 구문 분석기가 열린 파일에 대해 성공적으로 (구문 적으로) 패스를 완료 할 때마다 파일의 현재 상태가 전역 캐시로 이동됩니다.
- 파일의 구문이 올바르지 않으면 ANTLR 필터 파서 (링크에 대해 죄송합니다. 대부분의 정보는 메일 링리스트에 있거나 소스를 읽음으로써 수집 됨) 를 사용하여 다음을 찾는 파일을 재분석합니다.
- 변수 / 필드 선언.
- 클래스 / 구조체 정의에 대한 서명입니다.
- 메서드 정의에 대한 서명입니다.
- 로컬 캐시에서 클래스 / 구조체 / 메소드 정의는 서명에서 시작하여 중괄호 중첩 수준이 짝수로 돌아갈 때 끝납니다. 다른 메서드 선언에 도달하면 메서드가 종료 될 수도 있습니다 (중첩 메서드 없음).
- 로컬 캐시에서 변수 / 필드는 바로 앞의 닫히지 않은 요소에 연결됩니다 . 이것이 중요한 이유에 대한 예제는 아래의 간단한 코드 스 니펫을 참조하십시오.
- 또한 사용자가 입력 할 때 추가 / 제거 된 문자 범위를 표시하는 리맵 테이블을 유지합니다. 다음 용도로 사용됩니다.
- 메서드가 전체 구문 분석 사이에서 파일에서 이동할 수 있기 때문에 커서의 올바른 컨텍스트를 식별 할 수 있는지 확인합니다.
- Go To Declaration / Definition / Reference가 열린 파일에서 항목을 올바르게 찾습니다.
이전 섹션의 코드 스 니펫 :
class A
{
int x; // linked to A
void foo() // linked to A
{
int local; // linked to foo()
// foo() ends here because bar() is starting
void bar() // linked to A
{
int local2; // linked to bar()
}
int y; // linked again to A
이 레이아웃으로 구현 한 IntelliSense 기능 목록을 추가 할 것이라고 생각했습니다. 각각의 사진은 여기에 있습니다.
- 자동 완성
- 도구 팁
- 방법 팁
- 클래스보기
- 코드 정의 창
- Call Browser (VS 2010은 마침내 이것을 C #에 추가합니다)
- 의미 상 올바른 모든 참조 찾기
답변
특정 구현에서 어떤 알고리즘이 사용되는지 정확히 말할 수는 없지만 몇 가지 정보를 바탕으로 추측 할 수 있습니다. 트라이은 이 문제에 대한 매우 유용한 데이터 구조입니다 : IDE에서 각 노드에서 몇 가지 추가 메타 데이터, 프로젝트의 모든 심볼의 메모리에 큰 트라이을 유지할 수 있습니다.
문자를 입력하면 트라이의 경로를 따라 이동합니다. 특정 트라이 노드의 모든 자손은 가능한 완료입니다. 그런 다음 IDE는 현재 컨텍스트에서 의미가있는 것으로 필터링하기 만하면되지만 탭 완성 팝업 창에 표시 할 수있는만큼만 계산하면됩니다.
고급 탭 완성에는 더 복잡한 시도가 필요합니다. 예를 들어 Visual Assist X 에는 CamelCase 기호의 대문자 만 입력하면되는 기능이 있습니다. 예를 들어 SFN을 입력하면 SomeFunctionName
탭 완성 창에 기호 가 표시됩니다.
트라이 (또는 기타 데이터 구조)를 계산하려면 프로젝트의 모든 기호 목록을 가져 오기 위해 모든 코드를 구문 분석해야합니다. Visual Studio는이를 .ncb
프로젝트와 함께 저장된 파일 인 IntelliSense 데이터베이스에 저장하므로 프로젝트를 닫고 다시 열 때마다 모든 것을 다시 구문 분석 할 필요가 없습니다. 대규모 프로젝트 (예 : 소스 컨트롤에서 방금 동기화 한 프로젝트)를 처음 열면 VS는 모든 것을 구문 분석하고 데이터베이스를 생성하는 데 시간이 걸립니다.
점진적 변경을 어떻게 처리하는지 모르겠습니다. 말했듯이 코드를 작성할 때 90 %의 유효하지 않은 구문이며, 유휴 상태 일 때마다 모든 것을 다시 구문 분석하면 CPU에 막대한 세금이 부과되지만 특히에 포함 된 헤더 파일을 수정하는 경우 많은 수의 소스 파일.
나는 그것이 (a) 프로젝트를 실제로 빌드 할 때만 (또는 아마도 그것을 닫거나 열 때) 재분석하거나, (b) 방금 한 곳에서만 코드를 구문 분석하는 일종의 로컬 구문 분석을 수행한다고 생각합니다. 관련 기호의 이름을 얻기 위해 제한된 방식으로 편집되었습니다. C ++에는 매우 복잡한 문법이 있기 때문에 무거운 템플릿 메타 프로그래밍 등을 사용하는 경우 어두운 구석에서 이상하게 작동 할 수 있습니다.