Menyiapkan Griffon Untuk Dipakai Di Eclipse STS

Pada suatu hari, saya mendengarkan perbincangan antara seorang mahasiswa dan seorang dosen.  Sang mahasiswa yang terkenal pintar ini berkata bahwa aplikasi desktop akan digantikan dengan situs web dan segala sesuatunya akan berbasis web.   Lalu, si dosen memberikan jawaban mengapa aplikasi desktop masih dibutuhkan.   Tidak ada hasil yang pasti dari tanya jawab tersebut.  Apakah benar aplikasi desktop akan usang dan tidak dibutuhkan lagi?

Sebagai seorang developer yang pernah terlibat dalam pemograman low-level, saya tahu itu tidak benar.   Mari lakukan pembuktian sederhana.   Windows menawarkan API yang bisa dipakai oleh program desktop (yang bisa dilihat di http://msdn.microsoft.com/en-us/library/windows/desktop/ff818516(v=vs.85).aspx).  Semakin canggih API yang ditawarkan oleh sebuah OS, berarti semakin mantap OS tersebut.  API dari Windows bisa dipanggil untuk melakukan manipulasi registry, grafis, jaringan, manajemen memori, printing,  sensor, threading, dan lainnya.   Logika berikut ini memperlihatkan apa yang akan terjadi bila seluruh aplikasi harus beralih menjadi web dan tidak boleh ada aplikasi desktop lagi:

  1. Berapa banyak dari API tersebut yang dapat dipanggil melalui JavaScript di browser secara langsung?  Bisa dipastikan bahwa sebuah aplikasi web murni yang berbasis HTML5, CSS3 dan JavaScript  sekalipun tidak dapat memanfaatkan Windows API yang ada sesuka hati.
  2. Dengan demikian seluruh API yang lengkap tersebut sia-sia dan berlebihan.  Hanya perlu sisakan API yang dipakai oleh browser.
  3. Tidak perlu merilis versi baru dari sistem operasi, karena semua aplikasi berjalan di web, sehingga cukup rilis versi baru dari browser.
  4. Karena browser wajib memiliki kemampuan yang sama dan seragam (misalnya mengikuti standar HTML5), maka seluruh sistem operasi dan semua hardware komputer memiliki sifat yang sama, sehingga pada akhirnya hanya butuh satu pembuat hardware sekaligus pembuat sistem operasi (kembali seperti zaman berjayanya IBM PC + IBM PC-DOS ?!).  Tidak ada lagi developer yang perlu membuat program di Intel Parallel Studio XE ataupun yang memakai AMD Accelerated Parallel Processing Math Libraries.

Pembuktian terbalik di atas menghasilkan sebuah kondisi yang sulit menjadi kenyataan dan tidak ada gejala bahwa sedang terjadi proses untuk menuju ke arah kondisi tersebut, sehingga kesimpulannya adalah tidak semua aplikasi harus berupa aplikasi web.   Tidak dipungkiri bahwa aplikasi web sangat populer.  Walaupun masih kalah dalam hal kinerja dan fleksibilitas, aplikasi web memiliki daya tarik tersendiri karena dapat menjangkau pengguna dengan mudah.

Bila Groovy memiliki framework Grails untuk aplikasi web, maka terdapat juga Griffon untuk membuat aplikasi desktop.   Bisa dibilang ini adalah untuk pertama kalinya saya menemukan sebuah framework MVC untuk aplikasi desktop di Java.  Yup!  Biasanya yang saya temukan adalah UI layer seperti SwingX atau Eclipse SWT, tetapi Griffon benar-benar sebuah framework yang memisahkan model view controller persis seperti di Grails.

Saya akan mencoba memakai Griffon di STS dimana sebelumnya saya sudah men-install plugin Groovy.  Setelah men-download dan men-extract Griffon pada sebuah folder, saya akan menambahkan lokasi folder tersebut sebagai nilai dari environment variables GRIFFON_HOME.  Untuk menambah environment variable baru, klik kanan My Computer, memilih Properties, Advanced System Settings,  lalu memilih tab Advanced, men-klik Environment Variables…  Pada dialog yang muncul, saya bisa men-klik tombol New di User variables.  Pada Variable name, saya isi dengan GRIFFON_HOME.  Pada Variable value, saya isi dengan lokasi dimana file Griffon berada, misalnya C:\Programs\Griffon-1-1.0.  Setelah itu, saya memilih variabel Path, men-klik Edit, lalu diakhir nilai variabel tersebut, saya menambahkan lokasi folder bin, misalnya …;C:\Programs\griffon-1.1.0\bin (dimana … adalah nilai yang sudah ada dan tidak perlu diubah).

Sayang sekali saya tidak memperoleh fasilitas ‘mewah’ seperti saat memakai Grails.  Untuk membuat proyek Griffon baru, saya harus memakai Command Prompt.   Jadi, langkah pertama yang saya lakukan adalah membuka command prompt dan memberikan perintah untuk membuat proyek baru seperti berikut ini:

C:\Users\Snake\Desktop> griffon create-app latihan
...
Created Tests for Latihan
Created Griffon Application at C:\Users\Snake\Desktop/latihan

C:\Users\Snake\Desktop> cd latihan

C:\Users\Snake\Desktop\latihan> griffon integrate-with --eclipse
...
Created Eclipse project files.
  [delete] Deleting directory C:\Users\Snake\.griffon\1.1.0\projects\latihan\integration-files

C:\Users\Snake\Desktop\latihan> griffon install-plugin eclipse-support
...
Installed plugin 'eclipse-support-0.6.3' in C:\Users\Snake\Snake\.griffon\1.1.0\projects\latihan\plugins/eclipse-support-0.6.3

C:\Users\Snake\Desktop\latihan> griffon integrate-with --ant
...
Created Ant build file.
  [delete] Deleting directory C:\Users\Snake\.griffon\1.1.0\projects\latihan\integration-files

Setelah ini, saya bisa membuka Spring Tool Suite (STS).  Saya akan melakukan konfigurasi yang hanya dilakukan sekali saja pada saat pertama kali memakai Griffon.  Saya memilih menu Window, Preferences.  Lalu saya mencari menu Java, Build Path, Classpath Variables.  Saya men-klik tombol New …, lalu mengisi Name dengan GRIFFON_HOME dan mengisi Path dengan lokasi instalasi Griffon, misalnya C:\Programs\Griffon-1-1.0.  Setelah menklik tombol OK,  saya kembali men-klik tombol New…  Kali ini saya mengisi Name dengan USER_HOME dan mengisi Path dengan lokasi home user saya, misalnya C:\Users\Snake.  Klik tombol OK untuk menutup dialog.

Untuk membuka proyek, saya memilih menu File, Import…  Pada dialog yang muncul, saya memilih Existing Projects into Workspace.  Kemudian di Select root directory, saya memilih lokasi direktori saat saya memberikan perintah create-app di atas, yaitu di C:\Users\Snake\Desktop.  Secara otomatis akan muncul nama proyek latihan dengan tanda centang.  Saya kemudian men-klik tombol Finish.

Di Package Explorer, saya akan memperoleh sebuah proyek yang memiliki struktur seperti pada gambar berikut ini:

Struktur Proyek Griffon

Struktur Proyek Griffon

Pertanyaan yang muncul adalah bagaimana cara menjalankan proyek ini?  Lagi-lagi tidak ada dukungan yang memanjakan seperti saat memakai Grails.  Saya harus memakai Ant build file.  Caranya adalah dengan memilih menu Window, Show View, Ant.  Pada view Ant yang muncul, saya men-klik icon Add Buildfiles (iconnya berupa tombol tambah disamping semut).  Lalu saya memilih file build.xml yang ada di proyek latihan.   Tampilan view Ant akan berisi daftar target seperti pada gambar berikut ini:

Tampilan View Ant

Tampilan View Ant

Saya sudah lama tidak menjumpai proyek yang memakai Ant (sejak beralih ke Maven).   Sungguh terasa nostalgia!  Untuk menjalankan salah satu target yang ada, saya tinggal men-double klik nama target tersebut.  Misalnya untuk menjalankan aplikasi desktop ini, saya bisa men-double click target run-app.  Bila ingin menjalankan aplikasi melalui Java Web Start, saya dapat memakai target run-webstart.

Iklan

Terintegrasi Dengan Memakai Eclipse Grails Plugin Di STS

Grails adalah sebuah framework untuk pengembangan web secara agile dengan memakai Groovy.   Untuk memakai Grails, saya perlu memberikan perintah di Command Prompt, misalnya selalu dimulai dengan perintah grails create-app untuk membuat proyek baru.  Untuk menjalankan proyek, saya perlu mengetik perintah grails run-app.  Dan seterusnya.

Saat pertama kali memakai Grails, saya langsung teringat pada apa yang dilakukan oleh teman saya yang sedang membuat tesis yang melibatkan Symfony, sebuah framework agile untuk PHP.  Keduanya memiliki kemiripan yang serupa: harus sering-sering memberikan perintah lewat Command Prompt (sebenarnya bukan hanya Symfony, tapi masih banyak lagi, seperti Ruby on Rails dan Zend Framework).

Pada beberapa kali kesempatan, sang teman mencari saya untuk berdiskusi mengenai kode programnya, sehingga terkadang saya harus mengubah kode program di notebook-nya.   Dia tidak memakai sebuah Integrated Development Environment (IDE), melainkan memakai sebuah editor yaitu Komodo Edit.  Dalam hal ini, editor berfungsi memberikan nilai tambah berupa syntax highlightning.  Ada juga project tree untuk melihat folder secara cepat.  Tapi untuk melakukan operasi lain, misalnya menjalankan pengujian, saya perlu menekan ALT+TAB untuk berpindah ke command prompt dan mengetik perintah disana.  Setelah membaca hasil output dari command prompt, saya perlu kembali ke editor, mencari file yang failed di project tree untuk di-edit.

Saya berusaha agar lingkungan pengembangan saya tidak seperti itu saat memakai Grails.  Mungkin hal ini terlihat sepele, tapi pada saat saya sedang ‘aktif’ coding, saya ingin semua referensi yang saya butuhkan muncul pada saat itu juga. Misalnya  bila saya ingin tahu apa isi file tertentu sebelum melanjutkan coding, maka saya ingin isi file tersebut bisa langsung muncul dalam hitungan beberapa detik.  Bila terlalu lama, maka bayangan kode program yang tadinya mengalir dengan lancar bisa sirna, dan saya jadi bertanya dalam hati,  ‘tadi mau ngapain ya?’  Selain itu, kadang-kadang saya menghabiskan waktu berjam-jam karena sebuah bug yang timbul hanya karena saya membuat sebuah asumsi atau perkiraan yang salah; padahal saya bisa memastikan kebenaran asumsi saya dengan membuka file lain yang terkait, tetapi karena repot mencari jarum di tumpukan jerami, saya memilih untuk bergantung sepenuhnya pada asumsi.   Tapi ada kasusnya saya memakai editor, misalnya saat melakukan perubahan minor pada proyek yang sudah jadi di server.

Saya beruntung karena ada plugin Eclipse Grails untuk Spring Tool Suite (berbasis Eclipse).  Setelah meng-install plugin ini, yang saya lakukan adalah menentukan lokasi Grails yang akan dipakai dengan memilih menu Window, Preferences, Groovy, Grails seperti yang terlihat di gambar berikut ini:

Menentukan Instalasi Grails Yang Dipakai

Menentukan Instalasi Grails Yang Dipakai

Untuk membuat sebuah proyek Grails baru, saya tinggal memilih menu File, New, Grails Project, seperti yang terlihat pada gambar berikut ini:

Membuat Proyek Grails Baru

Membuat Proyek Grails Baru

Struktur proyek yang ditampilkan di Project Explorer tidak sepenuhnya sesuai dengan struktur folder proyek, melainkan ada pengkategorian secara logika, misalnya domain, controllers, views, dan sebagainya seperti yang terlihat pada gambar berikut ini:

Struktur Proyek Grails

Struktur Proyek Grails

Lalu bagaimana caranya memberikan perintah yang sebelum diberikan melalui Command Prompt?  Saya bisa men-klik icon Grails Command History seperti yang terlihat pada gambar berikut ini:

Grails Command History

Grails Command History

Akan muncul sebuah popup dimana saya bisa mengetik perintah Grails (dan tetap ada Ctrl+Space untuk completion) seperti yang terlihat pada gambar berikut ini:

Memberikan perintah Grails

Memberikan perintah Grails

Untuk menjalankan aplikasi, bisa juga langsung dari tombol Run As..  yang memiliki shortcut untuk perintah run-app seperti yang terlihat pada gambar berikut ini:

Menjalankan Proyek

Menjalankan Proyek

Pada saat membuat kode program, saya dapat menahan tombol Ctrl dan men-klik  sebuah identifier untuk menampilkan file dimana ia didefinisikan.  Shortcut Ctrl+Shift+R untuk mencari file yang ada di proyek juga bisa bekerja dengan baik, seperti yang terlihat pada gambar berikut ini:

Shortcut Ctrl+Shift+R di Eclipse

Shortcut Ctrl+Shift+R di Eclipse

Hasil test reports selain dapat dibaca dengan mudah versi HTML-nya,  juga dapat ditampilkan dalam bentuk  JUnit View yang  familiar, seperti yang terlihat pada gambar berikut ini:

Melihat Test Reports

Melihat Test Reports

Aptana Journal #12: Selesai Tanpa Kiamat

Tanggal 21 Desember 2012 dikabarkan adalah akhir dari peradaban manusia.   Beruntungnya, tanggal 21 sudah berlalu, dan sekarang sudah memasuki tanggal 22 (at least, bila  jam kiamat berdasarkan patokan waktu di Indonesia).   Hari ini memang adalah hari yang melelahkan bagi saya, tapi tidak ada tanda-tanda kiamat selain hujan yang turun terus menerus.  Okay, ini berarti saya masih punya kesempatan untuk membuat post blog baru.

Beberapa waktu lalu, saya membaca panduan kurikulum ilmu komputer yang dirancang oleh ACM (Association for Computing Machinary).   Apa itu ACM?  ACM adalah organisasi yang bergerak di bidang pendidikan dan penelitian advance computing dimana anggotanya adalah para praktisi di bidangnya.  ACM memiliki panduan kurikulum yang kerap dijadikan sebagai masukan oleh berbagai universitas di dunia.

Kurikulum ACM merefleksikan apa yang dibutuhkan oleh industri saat ini.  ACM juga terus merevisi kurikulumnya dengan meminta masukan dari para praktisi di industri.  Ini adalah hal bagus, karena selama ini, saya sering merasa kalangan akademis menciptakan sebuah “dunia semu” bagi mahasiswanya.  Mahasiswa dipacu untuk mengerjakan ujian yang merefleksikan “dunia semu” ini.  Lalu begitu lulus, mahasiswa harus beradaptasi lagi dengan “dunia nyata” (industri!) yang ternyata beda jauh dari “dunia semu” di kampus.  Bukankah sungguh ironis: mahasiswa mati-matian berjuang mendapat nilai ‘A’ hanya untuk kualitas “dunia semu”;  para mafia kampus akan meningkatkan tingkat kesulitan “dunia semu” menjadi “dunia tak jelas” sehingga mereka mendapat ‘uang saku’ tambahan; dan mahasiswa akan berjuang merayu dosen agar tidak pelit nilai (bila sudah kerja nanti, apakah mereka akan merayu bos agar naik gaji?)  Bila ini dibiarkan terus menerus, kampus akan membentuk sebuah ‘sistem’ tersendiri yang semakin terpisah dari dunia industri.  Gelar akademis dan level golongan dosen lama-lama akan menjadi formalitas tanpa isi, tanpa kebanggaan (mungkin yang tersisa hanya ‘egoisme’ anak kecil dalam membela almamater).

Pada panduan yang saya baca, salah satu masukan dari industri adalah untuk meningkatkan pelajaran yang berkaitan dengan keamanan komputer dan software archeology.  Arkeologi?  Ini bukan pelajaran sejarah ‘kan?!  Software archeology adalah sebuah aktifitas untuk mempelajari kode program buatan orang lain dimana seseorang tidak memiliki dokumentasi yang lengkap atas kode program tersebut.  Ibaratkan dengan arkeologi, kode program buatan orang lain tersebut adalah sebuah artifact bersejarah.  Seorang arkeolog akan memprediksi kenapa ada tumpukan batu di dekat kuil kuno, ia akan menganalisa benda-benda sekitarnya untuk menemukan jawaban.  Dalam software archeology,  seseorang juga harus menganalisa kenapa sebuah bagian kode program dibuat dan untuk tujuan apa.

Mengapa software archeology dianggap penting oleh industri?  Karena, pada saat seorang lulusan bekerja di industri, mereka tidak selalu membuat software dari awal.  Kebanyakan yang terjadi adalah mereka harus bergabung dengan tim dimana proyek mungkin sudah berjalan 1 atau 2 bulan.   Kadang-kadang mereka adalah pengganti karyawan lama yang tiba-tiba menghilang sehingga mereka harus bekerja tanpa sempat melalui transfer ilmu.   Suatu hari nanti, mungkin mereka harus merombak sebuah sistem lama yang sudah dibuat 5 tahun lalu dimana seluruh developer-nya sudah resign.  Disinilah software archeology mulai menunjukkan perannya.

Apa yang saya lakukan selama seri artikel ini, mulai dari Aptana Journal #1 hingga Aptana Journal #12, adalah upaya memahami dan mengubah kode program untuk sebuah software yang dibuat orang lain, yaitu Aptana Studio 3.  Saya tidak punya dokumentasi yang lengkap.  Di kode program Aptana Studio 3, tidak selalu ada JavaDoc!    Ini adalah software archeology.  Tidak ada pembuat kode program original untuk ditanyai (bukan karena orang-orang tersebut adalah manusia purba yang sudah punah!)  Tentu saja, software archeology yang serius memiliki metode formal dan terstruktur, dan saya hanya memakai metode menulis dokumentasi dalam HTML (juga ada class diagram dan sequence diagram yang menempel di dinding!).

Mengubah Aptana Studio 3 untuk mendukung jQuery melalui polymorphism ‘semu’ mungkin tidak selalu berguna bagi semua orang, tapi hal ini akan sangat membantu saya.  Salah satu bagian yang cukup melelahkan adalah mendokumentasikan setiap method dan property jQuery yang ada di http://api.jquery.com ke dalam file ScriptDoc XML.   File yang saya buat tersebut dapat ditemukan di https://docs.google.com/open?id=0B-_rVDnaVRCbNEw5OU1WRUdXM1U.

Perilaku content assist dan context info yang telah berubah dapat dilihat pada animasi berikut (ini adalah animasi GIF berukuran 2.5 MB):

Animasi Yang Menunjukkan Hasil Perubahan

Aptana Journal #11: Menambahkan Dukungan Inline JSDoc

Artikel sebelumnya adalah salah satu post yang menarik di akhir tahun 2012 ini karena artikel tersebut adalah artikel ke-212 yang saya tulis di blog ini 🙂

Post ke 212 Untuk Seri ke 10 Di Akhir Tahun 2012

Post ke 212 Untuk Seri ke 10 Di Akhir Tahun 2012

Dan kembali ke artikel 213,  saya dihadapkan permasalahan bagaimana menampilkan content assist untuk argumen dalam anonymous inner function, misalnya yang paling sering ditemui adalah jQuery.Event.  Sebagai contoh, content assist tidak bekerja dengan baik di gambar berikut ini:

Content Assist Tidak Menampilkan proposal untuk object jQuery.Event

Content Assist Tidak Menampilkan proposal untuk object jQuery.Event

Padahal, e adalah sebuah variabel dengan tipe jQuery.Event. Dan saya sudah menambahkan bagian berikut ini ke ScriptDoc XML yang dipakai:

<javascript>
   ... <!-- isi diabaikan -->

   <class type="jQuery.Event">
     <properties>
        <property name="currentTarget" type="Element" scope="instance">
           <description>The current DOM element within the event bubbling phase.</description>
        </property>
     </properties>
   </class>
</javascript>

Dalam bahasa seperti JavaScript, memang sangat sulit untuk menentukan apa tipe dari sebuah variabel.  Tapi, bila programmer JavaScript menambahkan sebuah komentar dengan makna khusus, misalnya JSDoc, maka Aptana Studio bisa mengetahui tipe dari variabel e dan menampilkan content assist untuk variabel tersebut.  Saat ini, Aptana Studio mendukung penggunaan @type untuk elemen selain function sehingga content assist bekerja dengan baik bila saya menambahkan JSDOC tersebut seperti berikut ini:

Menggunakan JSDoc @type di Aptana Studio

Menggunakan JSDoc @type di Aptana Studio

Tapi saya merasa syntax tersebut masih terlalu panjang.  Oleh sebab itu, saya mencoba memodifikasi Aptana Studio agar mendukung inline JSDoc yang lebih singkat.

Hasil penelusuran membawa saya ke class JSTypeUtil di method applyDocumentation(). Method ini akan memeriksa apakah property adalah FunctionElement. Bila iya, maka ia akan memberikan dokumentasi untuk FunctionElement tersebut.  Jika bukan, ia memberikan dokumentasi ke PropertyElement yang ada berdasarkan block.getText(), block.getTags(TagType.TYPE), dan block.getTags(TagType.EXAMPLE) (dimana block adalah sebuah DocumentationBlock).

Untuk mendukung inline JSDOC, saya perlu mengubah alur di atas,  sehingga kode program akan terlihat seperti berikut ini:

public static void applyDocumentation(PropertyElement property, JSNode node, DocumentationBlock block)
{
   if (property instanceof FunctionElement)
   {
       ... // kode program diabaikan
   }
   else
   {
       if (block != null)
       {
           // Bila tidak ada tag TYPE, maka ini adalah sebuah inline JSDOC
           if (block.getTags(TagType.TYPE).isEmpty()) {
              if (!StringUtil.isEmpty(block.getText().trim())) {
                 property.addType(block.getText().trim());
              }
           } else {

              ... // ini adalah kode program semula
           }
       }
   }
}

Sekarang, bila saya memakai inline JSDoc, maka content assist akan bekerja sesuai dengan yang diharapkan seperti pada gambar berikut ini:

Content Assist Untuk Inline JSDoc

Content Assist Untuk Inline JSDoc

Ops!  Tunggu dulu..  Bila saya memperhatikan contoh inline JSDoc di halaman http://code.google.com/p/jsdoc-toolkit/wiki/InlineDocs, terlihat bahwa tidak ada spasi di antara tipe dan komentar.  Versi perubahan saya saat ini masih tidak compatible.

Oleh sebab itu, saya masih perlu melakukan perubahan.  Saya harus mengubah SDoc.flex  dan SDoc.grammar agar mengenali inline JSDoc yang tidak dipisahkan dengan spasi.

Saya mulai dengan menambahkan sebuah definisi terminal baru di /com.aptana.js.core/parsing/SDoc.flex dengan nama INLINE_DOCUMENTATION seperti berikut ini:

%terminals INLINE_DOCUMENTATION;

Lalu, pada definisi rule Block, saya mengubahnya sehingga terlihat seperti berikut ini:

Block
	=	START_DOCUMENTATION Text.text END_DOCUMENTATION
		{:
			return new DocumentationBlock((String) text.value);
		:}
	|	START_DOCUMENTATION Tags.tags END_DOCUMENTATION
		{:
			return new DocumentationBlock((List<Tag>) tags.value);
		:}
	|	START_DOCUMENTATION Text.text Tags.tags END_DOCUMENTATION
		{:
			return new DocumentationBlock((String) text.value, (List<Tag>) tags.value);
		:}
	|	INLINE_DOCUMENTATION.text
	    {:
	    	return new InlineDocumentationBlock((String)text.value);
	    :}
	;

Pada kode program di atas, saya memisahkan antara dokumentasi biasa yang diwakili oleh class DocumentationBlock dan dokumentasi inline yang diwakili oleh class InlineDocumentationBlock.   Saat ini class InlineDocumentationBlock belum ada, sehingga saya perlu membuatnya.   Karena InlineDocumentationBlock pada dasarnya adalah bentuk khusus dari DocumentationBlock, maka saya tinggal menurunkan class tersebut dari DocumentationBlock.   Class baru ini dibuat di package com.aptana.js.internal.core.parsing.sdoc.model di proyek com.aptana.js.core dengan isi seperti berikut ini:

package com.aptana.js.internal.core.parsing.sdoc.model;

/**
 * Model to represent inline doc comments. See 
 * <a href="http://code.google.com/p/jsdoc-toolkit/wiki/InlineDocs">
 * http://code.google.com/p/jsdoc-toolkit/wiki/InlineDocs</a> for example.
 * 
 * @author SolidSnake
 *
 */
public class InlineDocumentationBlock extends DocumentationBlock {

	public InlineDocumentationBlock(String content) {
		super(content);
	}

}

Setelah itu, pada file /com.aptana.js.core/com/aptana/js/internal/core/parsing/sdoc/SDocTokenType.java, di bagian enumeration SDocTokenType, saya menambahkan baris berikut ini:

  INLINE_DOCUMENTATION(Terminals.INLINE_DOCUMENTATION),

Lalu, saya melakukan perubahan di file , dengan menambahkan baris berikut ini di <YYINITIAL>:

  // inline documentation
  "/**" [^ \t\r\n{\[\]#]+ "*/"  {
	String text = yytext(); 
	return newToken(SDocTokenType.INLINE_DOCUMENTATION, text.substring(3,text.length()-2)); }

Perubahan pada scanner generator dan parser generator telah selesai, saya pun mencoba menjalankan build.js.xml untuk memastikan bahwa file Java bisa dihasilkan dengan baik.  Caranya adalah dengan men-klik kanan file build.js.xml, kemudian memilih Run As, Ant Build.  Setelah memastikan bahwa Console menampilkan tulisan BUILD SUCCESSFUL,  saya mencoba me-refresh package com.aptana.js.internal.core.parsing.sdoc dengan men-klik kanan package tersebut dan memilih Refresh.

Perubahan terakhir yang perlu saya lakukan kembali lagi ke class JSTypeUtil di method applyDocumentation(). Saya kembali mengubah method tersebut sehingga terlihat seperti berikut ini:

public static void applyDocumentation(PropertyElement property, JSNode node, DocumentationBlock block)
{
  if (property instanceof FunctionElement)
  {
    applyDocumentation((FunctionElement) property, node, block);
  }
  else
  {
    if (block != null)
    {
      if (block instanceof InlineDocumentationBlock) {

        if (!StringUtil.isEmpty(block.getText().trim())) {
          property.addType(block.getText().trim());
        }

      } else {

        // apply description
        property.setDescription(block.getText());

        // apply types
        for (Tag tag : block.getTags(TagType.TYPE))
        {
          TypeTag typeTag = (TypeTag) tag;

          for (Type type : typeTag.getTypes())
          {
            ReturnTypeElement returnType = new ReturnTypeElement();

            returnType.setType(type.toSource());
            returnType.setDescription(typeTag.getText());
            property.addType(returnType);
          }
        }

        // apply examples
        for (Tag tag : block.getTags(TagType.EXAMPLE))
        {
          ExampleTag exampleTag = (ExampleTag) tag;

          property.addExample(exampleTag.getText());
        }

      }
    }
  }
}

Sekarang, inline JSDoc bisa bekerja dengan baik seperti yang terlihat pada gambar berikut ini:

Inline JSDoc yang bekerja dengan baik

Inline JSDoc yang bekerja dengan baik

Aptana Journal #10: Menampilkan Parameter Di Content Assist

Walaupun berhasil menampilkan ‘polymorphism‘ di content assist, seluruh nama method yang muncul selalu dengan nama yang sama.  Hal ini terkadang bisa sangat membingungkan.  Oleh sebab itu, saya ingin melakukan perubahan dimana nama method yang muncul di content assist juga menyertakan parameter.

Untuk itu, saya perlu mengubah method addProposal(Set<ICompletionProposal>, PropertyElement, int, URI, String, String[] di class JSContentAssistProcessor dengan menambahkan bagian seperti berikut ini:

PropertyElementProposal proposal = null;
if (property instanceof FunctionElement) {
  FunctionElement function = (FunctionElement) property;
  String documentation = JSModelFormatter.CONTEXT_INFO.getDocumentation(function);
  ContextInformation contextInformation = new ContextInformation(function.getName(), documentation);

  String proposalValue = StringUtil.join(null, function.getName(), "(",
    StringUtil.join(", ", function.getParameterNames()), ")");
  proposal = new PropertyElementProposal(proposalvalue, function.getName()+"()", function.getName().length()+1,
    function, offset, replaceLength, projectURI, contextInformation);
}

Langkah berikutnya adalah mengubah constructor yang pernah saya tambahkan di PropertyElementProposal.  Saya akui bahwa perubahan ini bukan yang terbaik, karena saya hanya menambah tanpa berani mengubah karena takut merusak yang sudah ada.  Akibatnya, parameter constructor terlihat panjang seperti berikut ini:

public PropertyElementProposal(String displayString, String replacementString, int cursorPosition,
  PropertyElement property, int offset, int replaceLength, URI uri, ContextInformation contextInformation)
{
  super(replacementString, offset, replaceLength, cursorPosition, null, displayString, 
    contextInformation, null);
  this.property = property;
  this.uri = uri;
}

Perjuangan belum selesai sampai disini, karena urutan tampilnya content assist tiba-tiba jadi berantakan.  Saya perlu mencari tahu kenapa, dan akhirnya menemukan jawaban di method validate() milik CommonCompletionProposal. Sebagai informasi, class PropertyElementProposal adalah turunan dari CommonCompletionProposal, sehingga method validate() ini ikut dipanggil. Berikut adalah isi method validate():

public boolean validate(IDocument document, int offset, DocumentEvent event)
{
  if (offset < this._replacementOffset)
    return false;

  int overlapIndex = getDisplayString().length - _replacementString.length();
  overlapIndex = Math.max(0, overlapIndex);
  String endPortion = getDisplayString().substring(overlapIndex);
  boolean validated = isValidPrefix(getPrefix(document, offset), endPortion);

  if (validated && event!=null)
  {
     // ... kode program diabaikan
  }

  return validated;
}

Karena nilai _displayString sangat berbeda dengan nilai _replacementString, maka hasil variabel endPortion jadi aneh.  Tapi karena kode program ini bukan saya yang buat, saya tidak mengerti apa tujuan awalnya.

Lalu apa saya harus mengubah method validate() di CommonCompletionProposal?   Tidak, lebih baik saya men-override method validate() ini di PropertyElementProposal sehingga perubahan tidak berdampak CommonCompletionProposal yang lain.   Oleh sebab itu, saya menambahkan method berikut ini di PropertyElementProposal (yang isinya hampir sama seperti di CommonCompletionProposal:

@Override
public boolean validate(IDocument document, int offset, DocumentEvent event)
{
	if (offset < this._replacementOffset)
		return false;

	int posisiKurung = getDisplayString().indexOf('(');
	String propertyName = null;
	if (posisiKurung == -1) {
		propertyName = getDisplayString();
	} else {
		propertyName = getDisplayString().substring(0, getDisplayString().indexOf('('));
	}
	int overlapIndex = propertyName.length() - _replacementString.length();
	overlapIndex = Math.max(0, overlapIndex);
	String endPortion = getDisplayString().substring(overlapIndex);
	boolean validated = isValidPrefix(getPrefix(document, offset), endPortion);

	if (validated && event != null)
	{
		// make sure that we change the replacement length as the document content changes
		int delta = ((event.fText == null) ? 0 : event.fText.length()) - event.fLength;
		final int newLength = Math.max(_replacementLength + delta, 0);
		_replacementLength = newLength;
	}

	return validated;
}

Sekarang, content assist untuk method yang memiliki banyak variasi tidak akan terlihat membingungkan lagi karena sudah ada informasi parameter seperti yang terlihat pada gambar berikut ini:

Content Assist Dengan Informasi Parameter

Content Assist Dengan Informasi Parameter

Aptana Journal #9: Mengorbankan Static Method

Pada Journal #1, saya melakukan perubahan pada ParserUtil di method getParentObjectTypes() dimana saya menganggap seluruh referensi jQuery adalah ke Function<jQuery> bukan Class<jQuery>.  Dengan kata lain, saya menganggap semua penggunaan jQuery berdasarkan instance, misalnya: $("p").blur(). Padahal, pada kenyataannya, ada beberapa method jQuery yang dapat diakses tanpa harus membuat instance, misalnya: $.ajax().  Method seperti ini adalah method static.   Dengan pendekatan yang saya tempuh saat ini, seluruh method, baik yang static maupun per-instace, akan ditampilkan dalam content assist saat pengguna mengetik jQuery atau $.   Saya pikir trade-off ini masih dapat diterima bila dibandingkan dengan keuntungan yang saya peroleh saat melakukan coding jQuery nanti (membuat widget, misalnya).

jQuery mengandung beberapa definisi class yang dapat dipakai diluar, misalnya Callbacks, Deferred, dan jqXHR.  Kebanyakan dari class tersebut dapat dibuat dengan constructor berupa method static di object jQuery.   Karena saya tidak mendukung method static, maka saya mendokumentasikan seluruh constructor sebagai function biasa (per-instance).

Permasalahan yang saya hadapi adalah content assist tidak bekerja dengan baik untuk constructor tersebut.  Aptana Studio menganggap seluruh constructor tersebut mengembalikan sebuah Object yang universal.

Mengapa demikian?  Untuk menjawab pertanyaan ini, saya pun melakukan penelusuran, yang membawa saya pada class JSNodeTypeInferrer. Class ini memiliki sebuah method dengan nama visit() dimana berisi cuplikan seperti berikut ini:

public void visit(JSGetPropertyNode node)
{
   ... // kode diabaikan
   // TODO Combine with similiar code from ParseUtil.getParentObjectTypes
   if (JSTypeCOnstants.FUNCTION_JQUERY.equals(typeName)
          && lhs instanceof JSIdentifierNode
          && (JSTypeConstants.DOLLAR.equals(lhs.getText()) || JSTypeConstants.JQUERY.
                 .equals(lhs.getText())))
   {
       typeName = JSTypeConstants.CLASS_JQUERY;
   }
   ... // kode diabaikan
}

Wow!  Sesuai dengan komentar TODO di atas-nya,  disini telah terjadi duplikasi kode program.  Bila saya menghilangkan logic di ParseUtil.getParentObjectTypes() (petualangan di journal #1), maka saya WAJIB  menghilangkan bagian yang ini juga!  Ini bisa membingungkan orang-orang, terutama saya yang berasumsi bahwa perubahan pada ParseUtil sudah menyelesaikan masalah, tetapi disini juga perlu diubah.  Untuk itu, saya memberikan komentar pada baris di atas sehingga mereka tidak akan mengubah Function<jQuery> menjadi Class<jQuery>.

Tapi petualangan belum berakhir sampai disini.  Setelah tipe class bisa ditemukan dengan baik, Apatana Studio tetap tidak mengembalikan method yang spesifik untuk class tersebut, melainkan hanya class bersifat umum yaitu Object.

Mengapa demikian?  Penelusuran kode program membawa saya ke method addTypeProperties() di class JSContentAssistProcessor.  Pada method ini terdapat sebuah baris kode program seperti berikut ini:

Collection properties = indexHelper.getTypeMembers(index, allTypes);

Lalu, apa isi dari method getTypeMembers()? Isinya seperti berikut ini:

public Collection getTypeMembers(Index index, List<String> typeNames) {
return CollectionsUtil.union(getMembers(index, typeNames), getMembers(getIndex(), typeNames));
}

Pada saat melakukan tracing, masing-masing getMembers() telah mengembalikan method yang valid dan benar.  Tetapi pada saat keduanya digabungkan kedalam sebuah Set, selalu ada yang hilang!  Hal ini tiba-tiba mengingatkan saya pada ‘bug‘ yang saya temui di journal #4 sehubungan dengan nilai di Set yang menghilang.   Ternyata benar, class FunctionElement belum memiliki method equals() dan hashCode().  Saya segera memilih menu Source, Generate hashCode() and equals().

Sampai disini semua sudah mendingan, tapi petualangan belum berakhir.  Mengapa demikian?  Karena method yang muncul selalu terduplikasi, ada yang mengandung dokumentasi dan ada yang tidak.  Padahal, saya menginginkan hanya yang mengandung dokumentasi saja yang ditampilkan (dan tampilkan versi tidak terdokumentasi bila seandainya tidak ada yang lebih baik).

Untuk mengatasi permasalahan tersebut, saya mengubah method visit di JSNodeTypeInfererrer menjadi seperti:

public void visit(JSGetPropertyNode node)
{
   ... // kode program diabaikan
   if (properties!=null)
   {
      for (PropertyElement property: properties)
      {
          if (property instanceof FunctionElement)
          {
              FunctionElement function = (FunctionElement) property;
              boolean adaVersiTerdokumentasi = false;
              if (function.getDescription()==null || function.getDescription().length()==0) {
                  for (PropertyElement item: properties) {
                     if (item instanceof FunctionElement && item.getName().equals(function.getName()) && 
                         item.getDescription()!=null && item.getDescription().length()>0) {
                         adaVersiTerdokumentasi = true;
                     }
                  }
              }
              if (adaVersiTerdokumentasi) continue;

              ... // kode program diabaikan
          }

          ... // kode program diabaikan
      }
   }
}

Belajar dari petualangan hari ini, saya sudah beberapa kali melakukan filtering elemen yang mengandung dokumentasi.  Sepertinya logika ini bisa dikumpulkan ke sebuah class sehingga saat melakukan perubahan, saya cukup mengubah class tersebut.

Sekarang, saya akan melakukan pengujian, misalnya, saya akan memanggil method static Callback seperti berikut ini:

Menampilkan Content Assist Untuk $

Menampilkan Content Assist Untuk $

Context info untuk method tersebut akan muncul seperti yang terlihat di gambar berikut ini:

Menampilkan Context Info Untuk Callbacks

Menampilkan Context Info Untuk Callbacks

Setelah itu, bila saya memanggil salah satu method object Callbacks, maka content assist akan muncul, seperti berikut ini:

Menampilkan Content Assist Untuk  Object Callbacks

Menampilkan Content Assist Untuk Object Callbacks

Beruntungnya, content assist pada saat terjadi chaining tetap bekerja dengan baik seperti yang terlihat di gambar berikut ini:

Content  assist setelah method add di Calllbacks

Content assist setelah method add di Calllbacks

Aptana Journal #8: Example

Pada file ScriptDoc XML yang saya buat, saya selalu menyertakan bagian <examples>  seperti berikut ini:

<examples>
  <example>Find all div elements within an XML document from an Ajax response.
   $("div", xml.responseXML);</example>
</examples>

Saat ini, isi examples tersebut tidak akan ditampilkan di context info ataupun di proposal content assist.  Lalu, kapan ditampilkan?  Pada saat hover (meletakkan mouse agak lama) pada posisi perintah JavaScript, akan muncul sebuah dialog yang berisi dokumentasi.  Saya merasa lebih baik bila context info juga menyertakan isi examples sehingga saya bisa mengetahui dengan cepat cara pakai method JavaScript yang sedang saya ketik.

Bagaimana caranya?  Beruntungnya, rancangan Aptana Studio cukup baik, karena hal-hal yang berkaitan dengan penampilan context info berada di satu class yaitu JSModelFormatter.  Class ini menyediakan instance static-nya dengan nama CONTEXT_INFO seperti yang terlihat di cuplikan kode program berikut ini:

public static final JSModelFormatter CONTEXT_INFO = new JSModelFormatter(false, Section.SIGNATURE)
{
   private static final String BULLET = "\u2022";
   private TagStripperAndTypeBolder stripAndBold = new TagStripperAndTypeBolder();

   public String getDocumentation(Collection<PropertyElement> properties)
   {
      ...
   }
}

Nilai String yang dikembalikan oleh method getDocumentation() di atas akan langsung ditampilkan sebagai context info.  Dengan demikian, bila saya ingin menampilkan examples pada context info, maka saya dapat menambahkan baris program di sekitar akhir dari method yang isinya seperti berikut ini:

result.add("\nExamples:");
result.add(StringUtil.concat(function.getExamples()));

Sekarang, bila saya menampilkan context info, saya juga akan memperoleh informasi examples seperti yang terlihat pada gambar berikut ini:

Tampilan Context Info Disertai Examples

Tampilan Context Info Disertai Examples

Aptana Journal #7: Alias

Pada petualangan sebelumnya, constructor jQuery berhasil dimunculkan dengan baik.  Tapi hal ini tidak berlaku untuk $.  Sebagai informasi, $ adalah alias untuk jQuery sehingga apa yang muncul sebagai constructor jQuery juga harus muncul sebagai $.   Pendekatan yang saya pakai mungkin akan menghilangkan perbedaan antara static method dan instance method, tapi ini adalah trade-off yang masih dapat ditoleransi.   Okay, tapi content assist dan context info untuk $  saat ini tidak bekerja dengan baik!  Padahal, saya sudah menambahkan baris berikut ini di file *.sdocml:

... 
  <aliases>
    <alias name="$" type="jQuery" />
  </aliases>
...

Mengapa content assist masih tidak bekerja dengan baik?  Untuk menjawab pertanyaan ini, saya kembali melihat isi method index() di class SDocMLFileIndexingParticipant. Bagian yang menarik adalah di berikut ini:

public void index(BuildContext context, Index index, IProgressMonitor monitor) throws CoreException
{

   ... // diabaikan

   // process results
   JSIndexWriter indexer = new JSIndexWriter();
   TypeElement[] types = reader.getTypes();
   AliasElement[] aliases = reader.getAliases();
   URI location = context.getURI();

   ... // diabaikan

   for (AliasElement alias: aliases)
   {
      String typeName = alias.getType();
      PropertyElement property = window.getProperty(typeName);
      if (property!=null)
      {
         if (property instanceof FunctionElement)
         {
            property = new FunctionElement((FunctionElement) property);
         }
         else
         {
            property = new PropertyElement(property);
         }
         property.setName(alias.getName());
      }
      else 
      {
         property = new PropertyElement();
         property.setName(alias.getName());
         property.addType(typeName);
      }
      window.addProperty(property);
   }

   indexer.writeType(index, window, location);

   ... // kode diabaikan
}

Pada kode program di atas, terlihat bahwa window.getProperty(typeName) akan dipanggil untuk mendapatkan tipe alias. Hasil kembaliannya selalu 1 buah PropertyElement!  Padahal $ harusnya adalah alias untuk seluruh variasi constructor jQuery (yang jumlahnya lebih dari 1).

Oleh sebab itu, saya menambahkan sebuah method baru di class TypeElement yang akan mengembalikan seluruh PropertyElement dengan nama yang sama (bila ada!). Berikut ini adalah isi method tersebut:

public List<PropertyElement> getProperties(String name)
{
  List<PropertyElement> listReturn = new ArrayList<PropertyElement>();
  if (name!=null && name.length()>0 && this._properties!=null) {
    for (PropertyElement property: this._properties) {
      if (name.equals(property.getName())) {
        listReturn.add(property);
      }
    }
  }
  return listReturn;
}

Berikutnya saya menyesuaikan method index() di SDocMLFileIndexingParticipant agar memanggil method yang baru saya buat di atas, dimana perubahan yang saya lakukan terlihat seperti berikut ini:

... // kode program di abaikan
for (AliasElement alias: aliases)
{
  String typeName = alias.getType();
  List<PropertyElement> listProperties = window.getProperties(typeName);
  if (listProperties.size() > 0) {
    for (PropertyElement property: listProperties) {
      if (property instanceof FunctionElement) {
        property = new FunctionElement((FunctionElement) property);
      } else {
        property = new PropertyElement(property);
      }
      property.setName(alias.getName());
      window.addProperty(property);
    }
  } else {
    PropertyElement property = new PropertyElement();
    property.setName(alias.getName());
    property.addType(typeName);
    window.addProperty(property);
  }
}
... // kode program diabaikan

Perjuangan masih belum selesai sampai disini!  Karena deskripsi dokumentasi tiba-tiba saja hilang entah kemana!!! Loh?  Mengapa?

Untuk menjawab keanehan baru tersebut, saya memeriksa isi constructor FunctionElement. Okay, constructor akan men-copy beberapa nilai dari base seperti _parameters, _returnTypes, dan sebagainya. Tidak ada yang men-copy _description disini!

Okay, berarti copy _description mungkin dilaksanakan di parent class-nya yaitu PropertyElement. Jadi, saya memeriksa isi constructor PropertyElement. Disini beberapa nilai dari base seperti _owningType, _types dan _examples akan di-copy! Tapi tidak ada yang men-copy _description sama sekali!  Wow, padahal _examples saja di-copy disini, masa _description tidak diikutkan? Oleh sebab itu, saya mengubah constructor PropertyElement sehingga terlihat seperti berikut ini:

public PropertyElement(PropertyElement base)
{
  this._owningType = base.getOwningType();
  this._isInstanceProperty = base.isInstanceProperty();
  this._isClassProperty = base.isClassProperty();
  this._isInternal = base.isInternal();
  this._types = new ArrayList<ReturnTypeElement>(base.getTypes());
  this._examples = new ArrayList<String>(base.getExamples());

  // Menambah deskripsi
  super.setDescription(base.getDescription());
}

Sekarang content assist dan context info untuk $ dapat bekerja dengan baik seperti yang terlihat di gambar berikut ini:

Content Assist  Untuk Tanda $

Content Assist Untuk Tanda $

Aptana Journal #6: Struktur Class Yang Aneh

Berangkat dari petualangan sebelumnya, dimana saya mengira semuanya sudah beres, tapi ternyata tidak.  Content assist dan context info hanya bekerja dengan benar di statement pertama.  Pada statement atau baris berikutnya, context info mulai sedikit ngawur.  Bahkan context info sama sekali tidak muncul untuk JavaScript yang berada dalam HTML.   Mengapa bisa demikian?

Setelah melakukan penelurusan, saya menemukan bahwa permasalahan terletak pada nilai contextInformationOffset yang diberikan pada saat memanggil method showContextInformation() di insertProposal() di class CompletionProposalPopup:

private void insertProposal(ICompletionProposal p, char trigger, int stateMask, final int offset)
{
  ... // kode diabaikan
  try
  {
     ... // kode diabaikan
     if (info != null)
     {
        int contextInformationOffset;
        if (p instanceof ICompletionProposalExtension)
        {
           ICompletionProposalExtension e = (ICompletionProposalExtension) p;
           contextInformationOffset = e.getContextInformationPosition();
        }
        ... // kode diabaikan
        fContentAssistant.showContextInformation(info, contextInformationOffset);
     }
     else
     {
        fContentAssistant.showContextInformation(null, -1);
     }
}

Nilai contextInformationOffset yang saya peroleh selalu 0 apapun yang terjadi.  Padahal ini seharusnya mengikuti posisi context.  Mengapa demikian?

Sebuah proposal untuk function diwakili oleh PropertyElementProposal.  Class tersebut diturunkan dari class CommonCompletionProposal.  Pada class CommonCompletionProposal, saya dapat menemukan atribut _replacementOffset dan method getContextInformationPosition().  Secara insting, saya mengira bahwa atribut dan method tersebut dipakai untuk menyimpan offset.   Dengan demikian, saya tinggal menyertakan nilai yang dibutuhkan melalu constructor di PropertyElementProposal.  Karena PropertyElementProposal adalah turunan dari CommonCompletionProposal, maka nilai tersebut seharusnya tinggal dipakai.

Lalu apa yang salah?  Well, asumsi saya terlalu naif!  Hal ini terbukti setelah saya memeriksa isi method getContextInformationPosition() di CommonCompletionProposal yang berupa:

public int getContextInformationPosition()
{
  return 0;
}

Ternyata apapun yang terjadi, nilai yang dikembalikan selalu 0.

Untuk mengatasi permasalahan yang ada, saya akan men-override method getContextInformationPosition() yang ada di CommonCompletionProposal dengan yang ada di PropertyElementProposal. Dengan demikian, perilaku CommonCompletionProposal tetap sama, karena saya tidak yakin kenapa nilai kembaliannya harus 0; sementara cukup perilaku PropertyElementProposal yang berubah sesuai dengan kebutuhan saya. Berikut ini adalah isi getContextInformationPosition() di PropertyElementProposal:

@Override
public int getContextInformationPosition()
{
  return super._replacementOffset;
}

Setelah penambahan method ini, context info akhirnya bekerja sesuai dengan yang diharapkan.

Aptana Journal #5: Perbaikan

Hari ini saya akan melanjutkan memperbaiki perubahan sebelumnya.  Bila terdapat lebih dari satu versi proposal content assist yang ditemukan, misalnya hasil parsing dari file JavaScript dan hasil parsing dari file ScriptDoc XML, maka saya hanya akan memakai versi yang memiliki dokumentasi.  Proposal content assist tanpa dokumentasi hanya akan dipakai bila tidak ada versi terdokumentasi yang ditemukan.

Untuk mencapai hasil di atas, saya perlu mengubah method addProjectGlobals() di class JSContentAssistProcessor. Bila sebelumnya isi method ini akan menambahkan seluruh PropertyElement yang ditemukan ke dalam Set proposal, maka saya akan mengubahnya untuk melakukan penyaringan seperti yang terlihat pada kode program berikut ini:

private void addProjectGlobals(Set<ICompletionProposal> proposals, int offset)
{
    Collection<PropertyElement> projectGlobals = indexHelper.getGlobals(getIndex(), getProject(), getFilename());

    if (!CollectionsUtil.isEmpty(projectGlobals))
    {
       String[] userAgentNames = getActiveUserAgentIds();
       URI projectURI = getProjectURI();

       // Tampung dulu seluruh FunctionElement yang akan diproses            
       List<FunctionElement> listFunctionElement = new ArrayList<FunctionElement>();
       for (PropertyElement property: CollectionsUtil.filter(projectGlobals, isVisibleFilter)) {
         if (property instanceof FunctionElement) {
           listFunctionElement.add((FunctionElement)property);
         }                
       }

       for (PropertyElement property : CollectionsUtil.filter(projectGlobals, isVisibleFilter))
       {

          // Hanya akan menyertakan proposal yang tidak mengandung informasi yang jelas
          // jika versi yang lebih terdokumentasi tidak tersedia.
          if (property instanceof FunctionElement) {                    
              FunctionElement thisFunctionElement = (FunctionElement) property;                    
              if (thisFunctionElement.getDescription()==null || thisFunctionElement.getDescription().length()==0) {
                 boolean ketemuYangTerdokumentasi = false;
                 for (FunctionElement item: listFunctionElement) {
                     // Mencari versi yang sudah terdokumentasi 
                     if (thisFunctionElement.getName().equals(item.getName()) && item.getDescription()!=null && item.getDescription().length()>0) {
                        ketemuYangTerdokumentasi = true;
                     }
                 }
                 if (ketemuYangTerdokumentasi) continue;
              }
          }
          String location = null;                
          List<String> documents = property.getDocuments();
          if (!CollectionsUtil.isEmpty(documents))
          {
             String docString = documents.get(0);
             int index = docString.lastIndexOf('/');
             if (index != -1)
             { 
                 location = docString.substring(index + 1);
             }
             else
             {
                 location = docString;
             }
          }
          addProposal(proposals, property, offset, projectURI, location, userAgentNames);
       }
    }
}

Setelah ini, saya ingin agar ketika sebuah proposal content assist dipilih oleh pengguna, maka context info (dokumentasi) untuk content assist tersebut langsung ditampilkan.  Hal ini berbeda dengan pendekatan Aptana Studio saat ini dimana context info hanya akan muncul setelah user mengetik tanda kurung buka.  Nilai context info saat ini diambil secara terpisah dari content assist seperti yang saya bahas di jurnal 2.

Apakah nilai context info tidak bisa diambil dari content assist?  Sepertinya bisa, karena saya melihat bahwa class CommonCompletionProposal memiliki sebuah atribut _contextInformation yang bertipe IContextInformation.

Lalu, bagaimana caranya supaya bisa memakai _contextInformation tersebut?  Untuk menjawab pertanyaan, ini saya menelusuri kode program untuk mencari tahu apa yang dikerjakan ketika sebuah proposal content asssist dipilih dan dipakai.   Method yang dipanggil adalah insertProposal() di class ContentAssistant.  Berikut ini adalah cuplikan di akhir dari method tersebut:

private void insertProposal(ICompletionProposal p, char trigger, int stateMask, final int offset)
{
  ... // kode diabaikan
  try
  {
     ... // kode diabaikan

     IContextInformation info = p.getContextInformation();
     if (info!=null)
     {
         ... // kode diabaikan
         fContentAssistant.showContextInformation(info, contextInformationOffset);
     }
     else
     {
         fContentAssistant.showContextInformation(null, -1);
     }
  }
  finally
  {
     ... // kode diabaikan
  }
}

Ternyata context info untuk sebuah proposal content assist akan ditampilkan secara otomatis bila ditemukan.  Yang terjadi adalah saat ini, content assist tidak memiliki context info.  Jadi, saya perlu menambahkan context info ke dalam setiap proposal untuk FunctionElement.  Selain itu, karena setiap fungsi dipanggil dengan kurung buka dan kurung tutup, saya akan menambahkan kurung buka dan kurung tutup untuk setiap proposal FunctionElement dimana posisi kursor harus berada di dalam kurung.  Ehem, fitur ini cukup berguna bagi saya, kenapa Aptana Studio tidak memberikannya?  Well, etika berkata bahwa tidak sopan mencela sebuah produk open-source; bila saya merasa ada yang kurang, maka saya harus menggunakan otak untuk memperbaiki kekurangan tersebut, bukan menggunakan mulut untuk mencela sesuatu yang diberikan pada saya secara bebas 🙂

Untuk mendukung context info pada proposal sebuah method/function, maka saya menambahkan sebuah constructor baru di PropertyElementProposal seperti berikut ini:

public PropertyElementProposal(String replacementString, int cursorPosition, PropertyElement property,
  int offset, int replaceLength, URI uri, ContextInformation contextInformation)
{
   super(replacementString, offset, replaceLength, cursorPosition, null, property.getName(), 
         contextInformation, null);
   this.property = property;
   this.uri = uri;
}

Setelah itu, pada bagian yang menambahkan proposal, yaitu di method addProposal() di class JSContentAssistProcessor, saya mengubah dan menyisipkan kode program seperti berikut ini:

PropertyElementProposal proposal = null;
if (property instanceof FunctionElement) {
  String documentation = JSModelFormatter.CONTEXT_INFO.getDocumentation(property);
  ContextInformation contextInformation = new ContextInformation(property.getName(), documentation);
  proposal = new PropertyElementProposal(property.getName()+"()", property.getName().length()+1, property,
    offset, replaceLength, projectURI, contextInformation);
} else {
  proposal = new PropertyElementProposal(property, offset, replaceLength, projectURI);
}

Pada kode program di atas, saya sangat terbantu oleh class JSModelFormatter yang dapat menghasilkan dokumentasi yang telah ter-format secara rapi dan tinggal ditampilkan.

Gambar berikut ini memperlihatkan apa yang terjadi ketika saya menambahkan content assist dan memilihnya:

Tampilan Content Assist Dan Context Info

Tampilan Content Assist Dan Context Info