Kod tamamlama nasıl çalışır?


84

Birçok editör ve IDE'nin kod tamamlaması vardır. Bazıları çok "zeki", bazıları gerçekten değil. Daha zeki tiple ilgileniyorum. Örneğin, sadece a) mevcut kapsamda mevcutsa b) dönüş değeri geçerli ise bir fonksiyon sunan IDE'ler gördüm. (Örneğin "5 + foo [sekme]" den sonra, yalnızca doğru türdeki bir tam sayıya veya değişken adlarına eklenebilecek bir şey döndüren işlevler sunar.) Ayrıca daha sık kullanılan veya en uzun seçeneği öne koyduklarını da gördüm. listenin.

Kodu ayrıştırmanız gerektiğini anlıyorum. Ancak mevcut kodu düzenlerken genellikle geçersizdir, içinde sözdizimi hataları vardır. Eksik olan ve hata içeren bir şeyi nasıl ayrıştırırsınız?

Ayrıca bir zaman kısıtlaması var. Bir liste hazırlamak saniyeler sürerse, tamamlama faydasızdır. Bazen tamamlama algoritması binlerce sınıfla ilgilenir.

Bunun için iyi algoritmalar ve veri yapıları nelerdir?


1
Güzel bir soru. Codeblocks.org'daki Code :: Blocks gibi, bunu uygulayan bazı açık kaynaklı IDE'lerin kodlarına bir göz atmak isteyebilirsiniz .

Yanıtlar:


65

UnrealScript dil hizmeti ürünümdeki IntelliSense altyapısı karmaşıktır, ancak burada elimden geldiğince en iyi genel bakışını vereceğim. VS2008 SP1'deki C # dil hizmeti benim performans hedefimdir (iyi bir nedenden dolayı). Henüz orada değil, ancak tek bir karakter yazıldıktan sonra ctrl + boşluk beklemeden veya kullanıcının .(nokta) yazmasını beklemeden güvenle öneriler sunabileceğim kadar hızlı / doğru . İnsanlar [dil hizmetleri üzerinde çalışan] bu konu hakkında ne kadar fazla bilgi alırsa, ürünlerini kullanmam durumunda o kadar iyi son kullanıcı deneyimi elde ederim. Ayrıntılara bu kadar dikkat etmeyen talihsiz bir çalışma deneyimi yaşadığım bir dizi ürün var ve sonuç olarak IDE ile kodladığımdan daha fazla mücadele ettim.

Dil hizmetimde aşağıdaki gibi düzenlenmiştir:

  1. İmleçteki ifadeyi alın. Bu, üye erişim ifadesinin başından imlecin üzerinde bulunduğu tanımlayıcının sonuna kadar yürür . Üye erişim ifadesi genellikle formdadır aa.bb.cc, ancak aynı zamanda yöntem çağrılarını da içerebilir aa.bb(3+2).cc.
  2. İmleci çevreleyen bağlamı alın . Bu çok zor, çünkü her zaman derleyici ile aynı kuralları takip etmiyor (uzun hikaye), ama burada öyle olduğunu varsayalım. Genellikle bu, imlecin içinde bulunduğu yöntem / sınıf hakkında önbelleğe alınmış bilgileri almak anlamına gelir.
  3. Kapsamda görünen tüm öğelerden birini almak için IDeclarationProviderçağırabileceğiniz bağlam nesnesi uygulamalarını söyleyin . Benim durumumda, bu liste yerelleri / parametreleri (bir yöntem içindeyse), üyeleri (alanlar ve yöntemler, yalnızca bir örnek yöntemde olmadıkça statik ve temel türlerin hiçbir özel üyesini içermez), globals (I dili için türler ve sabitler) içerir. üzerinde çalışıyorum) ve anahtar kelimeler. Bu listede adı olan bir öğe olacaktır . # 1'deki ifadeyi değerlendirmenin ilk adımı olarak , bir sonraki adım için bize bir isim vererek bağlam numaralandırmasından öğeyi seçiyoruz .GetDeclarations()IEnumerable<IDeclaration>aaaaIDeclaration
  4. Daha sonra, (bir anlamda) "üyelerini" içeren başka bir tane elde etmek için operatörü IDeclarationtemsile uyguluyorum . Yana operatör farklı operatör, aramaya ve beklemek doğru nesne listelenen operatörünü uygular.aaIEnumerable<IDeclaration>aa.->declaration.GetMembers(".")IDeclaration
  5. Bu cc, beyan listesinin adında bir nesne içerebileceği veya içermeyebileceği üzerine vurana kadar devam eder cc. Eminim ki, birden fazla öğe başlıyorsa cc, onlar da görünmelidir. Bunu , kullanıcıya mümkün olan en yararlı bilgileri sağlamak için son numaralandırmayı alıp belgelenmiş algoritmamdan geçirerek çözüyorum .

IntelliSense arka ucu için bazı ek notlar:

  • Uygulamada LINQ'nun tembel değerlendirme mekanizmalarından kapsamlı bir şekilde yararlanıyorum GetMembers. Önbelleğimdeki her nesne, üyelerine değerlendirme yapan bir işlev sağlayabilir, bu nedenle ağaçla karmaşık eylemler yapmak neredeyse önemsizdir.
  • Her nesnenin List<IDeclaration>üyelerinden bir tanesini tutması yerine , üyeyi tanımlayan özel olarak biçimlendirilmiş bir dizgenin karmasını içeren bir yapı olan a List<Name>, burada Nametutuyorum. İsimleri nesnelere eşleyen muazzam bir önbellek var. Bu şekilde, bir dosyayı yeniden ayrıştırdığımda, dosyada bildirilen tüm öğeleri önbellekten kaldırabilir ve onu güncellenmiş üyelerle yeniden doldurabilirim. Fonksiyonların yapılandırılma şekli nedeniyle, tüm ifadeler hemen yeni öğelerle değerlendirilir.

IntelliSense "ön uç"

Kullanıcı yazdıkça, dosya sözdizimsel olarak doğru olduğundan daha sık hatalıdır . Bu nedenle, kullanıcı yazdığında önbelleğin bölümlerini gelişigüzel bir şekilde kaldırmak istemiyorum. Artımlı güncellemeleri olabildiğince hızlı bir şekilde ele almak için çok sayıda özel durum kuralım var. Artımlı önbellek yalnızca açık bir dosyada yerel olarak tutulur ve kullanıcının yazdıklarının arka uç önbelleğinin dosyadaki her yöntem gibi şeyler için yanlış satır / sütun bilgileri tutmasına neden olduğunu anlamamasını sağlamaya yardımcı olur.

  • Kurtarıcı faktörlerden biri, ayrıştırıcımın hızlı olmasıdır . Düşük öncelikli bir arka plan iş parçacığında kendi kendine yeten çalışırken 150 ms'de 20000 satırlık bir kaynak dosyanın tam önbellek güncellemesini işleyebilir. Bu ayrıştırıcı açık bir dosya üzerinde bir geçişi başarıyla tamamladığında (sözdizimsel olarak), dosyanın mevcut durumu genel önbelleğe taşınır.
  • Dosya sözdizimsel olarak doğru değilse, aranan dosyayı yeniden ayrıştırmak için bir ANTLR filtre ayrıştırıcısı kullanıyorum (bağlantı için üzgünüm - çoğu bilgi posta listesinde veya kaynak okunarak toplanıyor) :
    • Değişken / alan bildirimleri.
    • Sınıf / yapı tanımları için imza.
    • Yöntem tanımlarının imzası.
  • Yerel önbellekte, sınıf / yapı / yöntem tanımları imzada başlar ve küme ayracı iç içe geçme düzeyi çift düzeyine döndüğünde sona erer. Yöntemler, başka bir yöntem bildirimine ulaşıldığında da sona erebilir (yuvalama yöntemi yok).
  • Yerel önbellekte, değişkenler / alanlar hemen önceki kapatılmamış öğeye bağlanır . Bunun neden önemli olduğuna dair bir örnek için aşağıdaki kısa kod parçacığına bakın.
  • Ayrıca, kullanıcı yazarken, eklenen / çıkarılan karakter aralıklarını işaretleyen bir yeniden eşleme tablosu tutarım. Bu şunlar için kullanılır:
    • Bir yöntem dosyada tam ayrıştırmalar arasında hareket edebildiğinden / hareket ettiğinden, imlecin doğru bağlamını tanımlayabildiğimden emin olmak.
    • Go To Declaration / Definition / Reference'ın açık dosyalardaki öğeleri doğru bir şekilde bulduğundan emin olun.

Önceki bölüm için kod pasajı:

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

Bu düzen ile uyguladığım IntelliSense özelliklerinin bir listesini ekleyeceğimi düşündüm. Her birinin resimleri burada yer almaktadır.

  • Otomatik tamamlama
  • Araç ipuçları
  • Yöntem İpuçları
  • Sınıf Görünümü
  • Kod Tanımlama Penceresi
  • Tarayıcıyı Çağır (VS 2010 sonunda bunu C # 'a ekler)
  • Anlamsal olarak doğru Tüm Referansları Bul

Bu harika, teşekkürler. Sıralama yaparken büyük / küçük harfe duyarlı önyargıyı hiç düşünmemiştim. Özellikle uyumsuz diş telleriyle başa çıkabilmenizi seviyorum.
stribika

16

Belirli bir uygulama tarafından hangi algoritmaların kullanıldığını tam olarak söyleyemem, ancak bazı eğitimli tahminler yapabilirim. Bir trie , bu problem için çok kullanışlı bir veri yapısıdır: IDE, her düğümde bir miktar ekstra meta veri ile projenizdeki tüm sembollerin belleğinde büyük bir trie tutabilir.

Bir karakter yazdığınızda, üçlüde bir yolda ilerler. Belirli bir üçlü düğümün soyundan gelenlerin tümü olası tamamlamalardır. Daha sonra IDE'nin mevcut bağlamda anlamlı olanlara göre filtrelemesi gerekir, ancak yalnızca sekme tamamlama açılır penceresinde görüntülenebilecek kadar çok hesaplaması gerekir.

Daha gelişmiş sekme tamamlama, daha karmaşık bir süreç gerektirir. Örneğin, Visual Assist X , yalnızca CamelCase sembollerinin büyük harflerini yazmanız gereken bir özelliğe sahiptir - örneğin, SFN yazarsanız, SomeFunctionNamesekme tamamlama penceresinde size sembolü gösterir .

Trie'yi (veya diğer veri yapılarını) hesaplamak, projenizdeki tüm sembollerin bir listesini almak için tüm kodunuzu ayrıştırmayı gerektirir. Visual Studio .ncbbunu, projenizin yanında depolanan bir dosya olan IntelliSense veritabanında depolar , böylece projenizi her kapattığınızda ve yeniden açtığınızda her şeyi yeniden ayrıştırmak zorunda kalmaz. Büyük bir projeyi ilk kez açtığınızda (örneğin, kaynak kontrolünü yeni senkronize ettiniz), VS her şeyi ayrıştırmak ve veritabanını oluşturmak için zaman alır.

Artımlı değişiklikleri nasıl ele aldığını bilmiyorum. Dediğiniz gibi, kod yazarken% 90 oranında geçersiz sözdizimidir ve her boşta kaldığınızda her şeyi yeniden düzenlemek, CPU'nuza çok az bir fayda sağlamak için çok büyük bir vergi koyar, özellikle çok sayıda kaynak dosya.

Ya (a) yalnızca projenizi gerçekten oluşturduğunuzda (veya muhtemelen kapattığınızda / açtığınızda) yeniden ayrıştırdığından veya (b) kodu yalnızca bulunduğunuz yerin çevresinde ayrıştırdığı bir tür yerel ayrıştırma yaptığından şüpheleniyorum. Sadece ilgili sembollerin isimlerini almak için sınırlı bir şekilde düzenlenmiştir. C ++ öylesine karmaşık bir dilbilgisine sahip olduğu için, ağır şablon metaprogramlama ve benzerlerini kullanıyorsanız karanlık köşelerde garip davranabilir.


Trie gerçekten iyi bir fikir. Artımlı değişikliklere gelince, işe yaramadığı zaman dosyayı yeniden ayrıştırmayı denemek, mevcut satırı görmezden gelmek ve bu işe yaramadığında çevreleyen {...} bloğunu göz ardı etmek mümkün olabilir. Her şey başarısız olursa, son veritabanını kullanın.
stribika

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.