IntelliSense özelliği birçok gelişmiş IDE’nin olmazsa olmaz özelliği. –Ki artık öyle kullanım getirileri var ki, IntelliSense olmadan kod yazmak çok hoş karşılanan bir aktivite olmuyor. Üretkenlik, kalite ve hızlı geliştirme ihtiyaçları için yazılımcıların en sevdiği özellik belki de. Visual Studio’nun son sürümü(v16.7) ile birçok yazılımcı için hayat kurtaran bir IntelliSense özelliğine merhaba dedik. Açıkçası basit bir özellik olduğu için onu çok allayıp, pullayıp anlatmayacağım ama böyle bir özelliğin nasıl geliştirildiğini ve biz de benzer şekilde ihtiyaçlar için ne yapabiliriz buna değinmeye çalışacağım. Ama önce Visual Studio’da ne gelmiş bir bakalım…

Herkesin bildiği gibi DateTime veri tipini string olarak ifade etmek istediğimizde, çeşitli formatlar ile ifade şeklini ayarlayabiliyoruz. Ama bu formatlardan, hangisinin ne olduğunu hatırlamak bazen direkt mümkün olmuyor, hh/MM/yy mi? Ya da HHHH/MM/Y mi? gibi denemeler/yanılmalar ya da Google’a sormalar her yazılımcının yaptığı aktiviteler.

Hadi DateTime neyse ama bir Regex olayı var ki; kendi adıma aramın pekiyi olduğunu söylemem. Regex ifadeleri yazmak başlı başına ayrı bir mücadele.

Yazılımcıların bu mücadelesini kolaylaştırmak için Visual Studio 16.7 sürümü ile IntelliSense özelliklerinde DateTime formatları ve Regex ifadeleri için ipuçları geldi. “Eeee zaten var ki” diyenleri duyar gibi oluyorum. ReSharper gibi eklentiler ile hem cüzdanları hem de Visual Studio’yu yorarak benzer özelliklerden faydalanabiliyorduk.💰🥴

Ama artık onlara gerek yok…

Peki nasıl yapılıyor?

Microsoft içerisindeki Roslyntakımları, yazılım geliştiricilerin üretkenliğine de kafa yorup, bu ve benzer özellikler ile “daha kolay nasıl kod yazabiliriz” bunu da düşünüyorlar. Benzer şekilde; bazen, ben de acaba onlar bunları nasıl yapıyorlar diye kafa yorup çeşitli araştırmalara dalıyorum. Yeni bir şeyler öğrenmek için güzel bir yol oluyor. Açık kaynak geliştirilen çözümler üzerinden de kurcalamak hem keyifli hem de kolay oluyor. Ama açık kaynak yaklaşımla geliştirildiği için belki de; birçok soru işaretimizin dokümanları çok umduğumuz gibi olamıyor. “Kodları var zaten onları okuyun” diyor birileri sanırım.

Visual Studio’daki bu yeni özellik için de, benzer şekilde “nasıl yapmışlar acaba” diye kafa yormaya başlayınca ve az biraz bu tarz C# ile ilgili yapıların Roslyn ile geliştirildiğinizi bilince hemen GitHub’daki Roslyn kod deposuna yöneldim. Buraları biraz kurcalayınca Visual Studio 16.7 sürümde gelen bu özelliğin az çok nasıl çalıştığını anlamak ve öğrenmek çok da zor değilmiş.

Bu tarz özelliklerin Roslyn ile geliştirilip, Visual Studio’ya eklenti olarak eklendiğini az çok biliyorum. MEF tabanlı eklenti alt yapısı ve Roslyn sayesinde Visual Studio’yu genişletmek artık çok da zor değil. Bu yeni özelliğe benzer bir şekilde bir özellik geliştirmeye çalışarak biraz API’ları tanıyalım. Yapacağımız olay çok basit; basit bir IntelliSense CodeCompletion özelliği ile bir Visual Studio eklentisi örneği yapacağız. Her ayrıntının üzerinden tek tek geçmeyeceğim, burada anlatmaya çalışacağım yapıyı ve örnek kodları GitHub profilimde bulabilirsiniz.

Türkiye’deki şehir isimlerinden plaka kodlarını çıkaran bir örnek olsun. (Offff çok yaratıcıyım 🤦‍♂️).

Bu yaratıcı(!) örneğim, Visual Studio’daki snippet kavramı ile karıştırılmaya çok uygun. Aman haa sakın. Snippet’lar ile buradaki yapı tamamen farklı.

Öncelikle bu örnekte kullanacağımız temel bileşen, Microsoft.CodeAnalysis.Features. Tabi ki bir nuget paketi olarak erişmek mümkün. Roslyn çatısı altında geliştirilen Microsoft.CodeAnalysis.* paketleri ile yazılan kodlar üzerinde bir çok işlemi gerçekleştirmek mümkün. C# yada VB.NET ile yoğun bir şekilde çalışıyorsanız mutlaka araştırın derim. Zor ama zevkli…

Gelelim örneğe; CompletionProvider sınıfından üretilen bir LicensePlateCompleteProvider sınıfımız olsun. Bütün yapı, bu sınıfın içinde yer alacak.

namespace LicensePlateCodeComplete
{
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.Completion;
    using Microsoft.CodeAnalysis.Options;
    using Microsoft.CodeAnalysis.Text;
    using System.Collections.Generic;
    using System.Collections.Immutable;
    using System.Threading;
    using System.Threading.Tasks;

    //Export this completion prvider for Visual Studio extension model
    [ExportCompletionProvider("LicensePlateCompleteProvider", LanguageNames.CSharp)]
    internal class LicensePlateCompleteProvider : CompletionProvider
    {
        public LicensePlateCompleteProvider()
        {

        }
    }
}

Burada önemli olan LicensePlateCompleteProvider’ı, ExportCompletionProvider özelliği ile Managed Extensibility Framework(MEF)’ün export yaklaşımı ile Visual Studio için eklenti olabilmesi için dışarıya aktarıyoruz.

CompletionProvider sınıfından birkaç metodu override ederek istediğimiz fonksiyonel özellikleri tanımlayacağız.-Ki ProvideCompletionsAsync(CompletionContext context) abstract bir method olduğu için bunu zaten mutlaka override etmemiz gerekecek. Genel olarak kodun tamamlama ya da kodun değiştirilme mantığını bu metot içerisinde tanımlıyoruz.

namespace LicensePlateCodeComplete
{
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.Completion;
    using Microsoft.CodeAnalysis.Options;
    using Microsoft.CodeAnalysis.Text;
    using System.Collections.Generic;
    using System.Collections.Immutable;
    using System.Threading;
    using System.Threading.Tasks;

    //Export this completion prvider for Visual Studio extension model
    [ExportCompletionProvider("LicensePlateCompleteProvider", LanguageNames.CSharp)]
    internal class LicensePlateCompleteProvider : CompletionProvider
    {

        public LicensePlateCompleteProvider()
        {

        }

        public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
        {

            return false;
        }

        public override async Task ProvideCompletionsAsync(CompletionContext context)
        {

        }

        public override Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
        {

            return Task.FromResult(CompletionChange.Create(new TextChange(new TextSpan(int.Parse(startString), int.Parse(lengthString)), newText)));
        }
    }
}

ShouldTriggerCompletion() ile IntelliSense’i tetikleyen mantığı oluşturabiliyoruz. Bu örnekte (çift tırnağa)’a basınca direkt IntelliSense tavsiyeleri çıksın diye basit bir kural oluşturuyoruz.

public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{
    //Check to trigger completion
    //Only if trigger is Insertion and " is must be inserted as trigger character
    if (trigger.Kind == CompletionTriggerKind.Insertion)
    {
        if (trigger.Character == '"')
        {
            return true;
        }
    }

    return false;
}

ProvideCompletionsAsync() metodunda temel olarak IntelliSense önerilerini ve kod değişikliği/eklentisi nasıl olacak bunun mantığını oluşturuyoruz.

public override async Task ProvideCompletionsAsync(CompletionContext context)
{
    //Do not provide completion with ctrl+space
    if (context.Trigger.Kind == CompletionTriggerKind.InvokeAndCommitIfUnique)
        return;

    var document = context.Document;
    var cancellationToken = context.CancellationToken;
    var startPosition = context.Position;

    //Get code content
    var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);

    //Find exact starting position by checking code syntax
    while (char.IsLetter(text[startPosition - 1]))
    {
        startPosition--;
    }


    var replacmentSpan = TextSpan.FromBounds(startPosition, context.Position);

    FillLicenses(replacmentSpan);

    //Fill intelliSense dialog box with suggested data
    foreach (var item in LicensePlates)
    {
        var textChange = item.Change.TextChange;
        var properties = ImmutableDictionary.CreateBuilder<string, string>();
        properties.Add(StartKey, textChange.Span.Start.ToString());
        properties.Add(LengthKey, textChange.Span.Length.ToString());
        properties.Add(NewTextKey, textChange.NewText);

        context.AddItem(CompletionItem.Create(
                displayText: item.City,
                inlineDescription: item.Code,
                properties: properties.ToImmutable()));
    }

    context.IsExclusive = true;
}

Burada çok basitçe; ctrl+space‘e basınca IntelliSense olmasın, değiştirilecek değerin pozisyonunu doğru bulunsun gibi basit işlemleri yapıyoruz. private FillLicenses() metodu ile önerilecek plakaların listesini oluşturuyoruz. Daha sonra bunları da CompletionProvider’ın içeriğine ekliyoruz ki Visual Studio’da text editörü içerisine aktarılabilsin.

Biraz merak uyandırmak ve yazıyı çok uzun tutmamak için diğer metotların ayrıntısına çok girmeyeceğim. GitHub’daki proje üzerinden bakabilirsiniz.

Bu yapıyı Visual Studio eklentisi olarak kullanabilmek için, Visual Studio proje şablonlarından VSIX seçip ona dahil edebilirsiniz, şu an o kısımlara da çok girmeyeceğim. Direkt Visual Studio eklentisi yazmaya başlamak için bu adresi ziyaret edebilirsiniz.

Peki neden bu tarz geliştirmelere ihtiyaç var mı?

Visual Studio’nun bu versiyonundaki, bu ve benzeri özellikler yazılım geliştirenler için oldukça kolaylık ve üretkenlik sağlayan katma değerler. Ama peki bizim de bu tarz geliştirmeler yapmamız gerekli mi?

Geliştirdiğimiz çözümleri belli bir mimari düzen içerisinde yapıyorsak, sürdürebilirlik gibi kriterlerimiz varsa, ekibe ya da projelere katılan yeni arkadaşlarımızın daha kolay kod yazmasını sağlamak istiyorsak bu tarz geliştirmeleri yapıp, çözüm sağlama üretkenliğimizi arttırabiliriz.

Geliştirdiğimiz çözümlerde C# dilini esnetmek ya da kontrol etmek için Roslyn oldukça değerli. Kod standartları oluşturmak ve bunlara uyumu sağlamak da çözüm geliştirme kalitemiz için değerli olacaktır.

Çok da uzatmadan yavaştan bitiriyorum. Umarım merak uyandıran ve biraz olsun kafalarda yeni soru işaretleri oluşturan bir yazı olmuştur. Bu tarz konular ile ilgili bir kaç şey daha karalamak istiyorum ileride, umarım yine karambole gitmez. 😀

Mutlu kodlamalar.