Hoàn thành mã hoạt động như thế nào?


84

Rất nhiều trình chỉnh sửa và IDE đã hoàn thành mã. Một số người trong số họ rất "thông minh", những người khác thì không thực sự như vậy. Tôi quan tâm đến loại thông minh hơn. Ví dụ, tôi đã thấy các IDE chỉ cung cấp một chức năng nếu nó là a) khả dụng trong phạm vi hiện tại b) giá trị trả về của nó là hợp lệ. (Ví dụ: sau "5 + foo [tab]", nó chỉ cung cấp các hàm trả về một cái gì đó có thể được thêm vào một số nguyên hoặc tên biến thuộc loại chính xác.) Tôi cũng đã thấy rằng họ đặt tùy chọn dài nhất hoặc được sử dụng thường xuyên hơn ở phía trước của danh sách.

Tôi nhận ra bạn cần phải phân tích cú pháp mã. Nhưng thông thường trong khi chỉnh sửa mã hiện tại không hợp lệ, có lỗi cú pháp trong đó. Làm thế nào để bạn phân tích cú pháp một cái gì đó khi nó chưa hoàn chỉnh và có lỗi?

Cũng có một hạn chế về thời gian. Việc hoàn thành sẽ vô ích nếu mất vài giây để đưa ra danh sách. Đôi khi thuật toán hoàn thành xử lý hàng nghìn lớp.

Các thuật toán và cấu trúc dữ liệu tốt cho việc này là gì?


1
Một câu hỏi hay. Bạn có thể muốn xem mã của một số IDE nguồn mở triển khai điều này, chẳng hạn như Code :: Blocks tại codeblocks.org .

1
Dưới đây là bài viết để tạo Code Completion trong C # Tạo Code Completion trong C #
Pritam Zope

Câu trả lời:


64

Công cụ IntelliSense trong sản phẩm dịch vụ ngôn ngữ UnrealScript của tôi rất phức tạp, nhưng tôi sẽ đưa ra một cái nhìn tổng quan nhất có thể ở đây. Dịch vụ ngôn ngữ C # trong VS2008 SP1 là mục tiêu hiệu suất của tôi (vì lý do chính đáng). Nó vẫn chưa ở đó, nhưng nó đủ nhanh / chính xác để tôi có thể đưa ra các đề xuất một cách an toàn sau khi nhập một ký tự duy nhất mà không cần đợi ctrl + space hoặc người dùng nhập .(dấu chấm). Những người [làm việc trên các dịch vụ ngôn ngữ] nhận được càng nhiều thông tin về chủ đề này, thì tôi càng có được trải nghiệm người dùng cuối tốt hơn nếu tôi sử dụng sản phẩm của họ. Có một số sản phẩm tôi đã có kinh nghiệm đáng tiếc khi làm việc với nó đã không chú ý đến các chi tiết, và kết quả là tôi đã chiến đấu với IDE nhiều hơn là viết mã.

Trong dịch vụ ngôn ngữ của tôi, nó được trình bày như sau:

  1. Lấy biểu thức tại con trỏ. Quá trình này sẽ đi từ đầu của biểu thức truy cập thành viên đến cuối của định danh mà con trỏ kết thúc. Biểu thức truy cập thành viên thường có dạng aa.bb.cc, nhưng cũng có thể chứa các lệnh gọi phương thức như trong aa.bb(3+2).cc.
  2. Lấy bối cảnh xung quanh con trỏ. Điều này rất phức tạp, bởi vì nó không phải lúc nào cũng tuân theo các quy tắc giống như trình biên dịch (câu chuyện dài), nhưng ở đây giả sử nó có. Nói chung, điều này có nghĩa là lấy thông tin đã lưu trong bộ nhớ cache về phương thức / lớp mà con trỏ nằm trong đó.
  3. Giả sử đối tượng ngữ cảnh thực hiện IDeclarationProvider, nơi bạn có thể gọi GetDeclarations()để nhận một IEnumerable<IDeclaration>trong số tất cả các mục hiển thị trong phạm vi. Trong trường hợp của tôi, danh sách này chứa các local / tham số (nếu trong một phương thức), các thành viên (các trường và phương thức, chỉ tĩnh trừ khi trong một phương thức phiên bản và không có các thành viên riêng của các kiểu cơ sở), toàn cầu (các kiểu và hằng số cho ngôn ngữ I đang làm việc trên), và từ khóa. Trong danh sách này sẽ có một mục có tên aa. Bước đầu tiên trong việc đánh giá biểu thức trong # 1, chúng tôi chọn mục từ liệt kê ngữ cảnh với tên aa, cung cấp cho chúng tôi IDeclarationbước tiếp theo.
  4. Tiếp theo, tôi áp dụng toán tử cho IDeclarationđại diện aađể lấy một toán tử khác IEnumerable<IDeclaration>có chứa "thành viên" (theo nghĩa nào đó) của aa. Vì .nhà điều hành khác với ->nhà điều hành, tôi gọi declaration.GetMembers(".")và mong muốn IDeclarationđối tượng áp dụng chính xác nhà điều hành được liệt kê.
  5. Điều này tiếp tục cho đến khi tôi nhấn cc, nơi danh sách khai báo có thể có hoặc không có đối tượng có tên cc. Tôi chắc rằng bạn biết, nếu bắt đầu bằng nhiều mục cc, chúng cũng sẽ xuất hiện. Tôi giải quyết vấn đề này bằng cách thực hiện thống kê cuối cùng và chuyển nó qua thuật toán được ghi lại của tôi để cung cấp cho người dùng thông tin hữu ích nhất có thể.

Dưới đây là một số lưu ý bổ sung cho chương trình phụ trợ IntelliSense:

  • Tôi sử dụng rộng rãi các cơ chế đánh giá lười biếng của LINQ trong việc triển khai GetMembers. Mỗi đối tượng trong bộ nhớ cache của tôi có thể cung cấp một bộ chức năng đánh giá các thành viên của nó, vì vậy việc thực hiện các hành động phức tạp với cây là gần như không nhỏ.
  • Thay vì mỗi đối tượng giữ một List<IDeclaration>thành viên của nó, tôi giữ a List<Name>, đâu Namelà một cấu trúc chứa băm của một chuỗi có định dạng đặc biệt mô tả thành viên. Có một bộ nhớ cache khổng lồ ánh xạ tên cho các đối tượng. Bằng cách này, khi tôi phân tích lại một tệp, tôi có thể xóa tất cả các mục được khai báo trong tệp khỏi bộ nhớ cache và tái tạo nó với các thành viên đã cập nhật. Do cách cấu hình các chức năng, tất cả các biểu thức ngay lập tức được đánh giá thành các mục mới.

IntelliSense "giao diện người dùng"

Khi người dùng nhập, tệp sai cú pháp thường xuyên hơn là chính xác. Do đó, tôi không muốn xóa các phần của bộ đệm ẩn một cách bừa bãi khi người dùng nhập. Tôi có một số lượng lớn các quy tắc trường hợp đặc biệt để xử lý các bản cập nhật gia tăng nhanh nhất có thể. Bộ đệm tăng dần chỉ được giữ cục bộ cho một tệp đang mở và giúp đảm bảo người dùng không nhận ra rằng việc nhập của họ đang khiến bộ đệm phụ lưu giữ thông tin dòng / cột không chính xác cho những thứ như từng phương thức trong tệp.

  • Một yếu tố đáng giá là trình phân tích cú pháp của tôi rất nhanh . Nó có thể xử lý cập nhật bộ đệm đầy đủ của tệp nguồn dòng 20000 trong 150ms trong khi hoạt động độc lập trên luồng nền có mức độ ưu tiên thấp. Bất cứ khi nào trình phân tích cú pháp này hoàn thành việc chuyển một tệp đang mở thành công (về mặt cú pháp), trạng thái hiện tại của tệp sẽ được chuyển vào bộ đệm chung.
  • Nếu tệp không đúng về mặt cú pháp, tôi sử dụng trình phân tích cú pháp bộ lọc ANTLR (xin lỗi về liên kết - hầu hết thông tin đều có trong danh sách gửi thư hoặc được thu thập từ việc đọc nguồn) để phân tích lại tệp đang tìm kiếm:
    • Khai báo biến / trường.
    • Chữ ký cho các định nghĩa lớp / cấu trúc.
    • Chữ ký cho các định nghĩa phương pháp.
  • Trong bộ đệm ẩn cục bộ, các định nghĩa lớp / struct / phương thức bắt đầu ở chữ ký và kết thúc khi mức lồng trong dấu ngoặc nhọn quay trở lại mức chẵn. Các phương thức cũng có thể kết thúc nếu đạt đến khai báo phương thức khác (không có phương thức lồng).
  • Trong bộ nhớ cache của địa phương, biến / lĩnh vực có liên quan đến ngay trước không khép kín phần tử. Xem đoạn mã ngắn gọn bên dưới để biết ví dụ về lý do tại sao điều này lại quan trọng.
  • Ngoài ra, khi người dùng nhập, tôi giữ một bảng remap đánh dấu các phạm vi ký tự được thêm / xóa. Điều này được sử dụng cho:
    • Đảm bảo rằng tôi có thể xác định ngữ cảnh chính xác của con trỏ, vì một phương thức có thể / di chuyển trong tệp giữa các phân đoạn đầy đủ.
    • Đảm bảo Đi tới Khai báo / Định nghĩa / Tham chiếu định vị chính xác các mục trong tệp đang mở.

Đoạn mã cho phần trước:

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

Tôi đã nghĩ rằng tôi sẽ thêm một danh sách các tính năng IntelliSense mà tôi đã triển khai với bố cục này. Hình ảnh của mỗi cái được đặt ở đây.

  • Tự động hoàn thành
  • Mẹo công cụ
  • Mẹo phương pháp
  • Xem lớp học
  • Cửa sổ định nghĩa mã
  • Trình duyệt cuộc gọi (VS 2010 cuối cùng đã thêm điều này vào C #)
  • Đúng về mặt ngữ nghĩa Tìm tất cả tài liệu tham khảo

Điều này là rất tốt, cảm ơn bạn. Tôi chưa bao giờ nghĩ đến sự thiên vị phân biệt chữ hoa chữ thường khi sắp xếp. Tôi đặc biệt thích rằng bạn có thể đối phó với niềng răng không khớp.
stribika

15

Tôi không thể nói chính xác thuật toán nào được sử dụng bởi bất kỳ triển khai cụ thể nào, nhưng tôi có thể đưa ra một số phỏng đoán có học thức. Một Trie là một cấu trúc dữ liệu rất hữu ích cho vấn đề này: IDE có thể duy trì một Trie lớn trong bộ nhớ của tất cả các biểu tượng trong dự án của bạn, với một số siêu dữ liệu thêm tại mỗi nút.

Khi bạn nhập một ký tự, nó sẽ đi xuống một đường dẫn trong trie. Tất cả các con cháu của một nút trie cụ thể đều có thể hoàn thành. Sau đó, IDE chỉ cần lọc những cái đó theo những cái có ý nghĩa trong ngữ cảnh hiện tại, nhưng nó chỉ cần tính càng nhiều càng tốt có thể được hiển thị trong cửa sổ bật lên hoàn thành tab.

Hoàn thành tab nâng cao hơn yêu cầu một bộ ba phức tạp hơn. Ví dụ, Visual Assist X có một tính năng theo đó bạn chỉ cần nhập các chữ cái in hoa của các ký hiệu CamelCase - ví dụ: nếu bạn nhập SFN, nó sẽ hiển thị cho bạn ký hiệu SomeFunctionNametrong cửa sổ hoàn thành tab của nó.

Việc tính toán trie (hoặc các cấu trúc dữ liệu khác) yêu cầu phải phân tích cú pháp tất cả mã của bạn để có được danh sách tất cả các ký hiệu trong dự án của bạn. Visual Studio .ncblưu trữ tệp này trong cơ sở dữ liệu IntelliSense của nó, một tệp được lưu trữ cùng với dự án của bạn, để nó không phải phân tích lại mọi thứ mỗi khi bạn đóng và mở lại dự án của mình. Lần đầu tiên bạn mở một dự án lớn (giả sử bạn vừa đồng bộ hóa điều khiển nguồn biểu mẫu), VS sẽ dành thời gian để phân tích cú pháp mọi thứ và tạo cơ sở dữ liệu.

Tôi không biết cách nó xử lý các thay đổi gia tăng. Như bạn đã nói, khi bạn đang viết mã, nó là cú pháp không hợp lệ trong 90% thời gian và việc viết lại mọi thứ bất cứ khi nào bạn chạy không tải sẽ đặt một khoản thuế lớn lên CPU của bạn vì rất ít lợi ích, đặc biệt nếu bạn đang sửa đổi tệp tiêu đề được một số lượng lớn các tệp nguồn.

Tôi nghi ngờ rằng nó (a) chỉ trả lời bất cứ khi nào bạn thực sự xây dựng dự án của mình (hoặc có thể khi bạn đóng / mở nó) hoặc (b) nó thực hiện một số loại phân tích cục bộ nơi nó chỉ phân tích mã xung quanh nơi bạn vừa được chỉnh sửa theo một số kiểu giới hạn, chỉ để lấy tên của các biểu tượng có liên quan. Vì C ++ có một ngữ pháp phức tạp vượt trội, nó có thể hoạt động kỳ lạ trong góc tối nếu bạn đang sử dụng lập trình siêu mẫu nặng và những thứ tương tự.


Bộ ba là một ý tưởng thực sự tốt. Đối với các thay đổi gia tăng, trước tiên có thể thử phân tích cú pháp lại tệp khi cách đó không hoạt động, hãy bỏ qua dòng hiện tại và khi điều đó không hiệu quả, hãy bỏ qua khối {...} đi kèm. Nếu vẫn thất bại, hãy sử dụng cơ sở dữ liệu cuối cùng.
stribika

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.