 |
|
난이도 : 중급 Nathan A. Good, Author and Software Engineer, Consultant
2007 년 8 월 14 일 플러그인을 사용하여 엔터프라이즈 표준을 따르는 코드를 추가할 수 있는 스니펫을 정의할 수 있습니다. Web Tools Project에 속해있는 Snippets 뷰와 비슷한 이 플러그인은 코드의 조각들을 에디터로 드래그&드롭 방식으로 가져올 수 있습니다. 우리는 객체 지향의 베스트 프랙티스를 따르기 때문에 이 스니펫은 데이터베이스(Apache Derby), 파일 시스템, 웹 서비스 같은 어떤 소스에서도 로딩될 수 있습니다.
이것은 Eclipse용 새로운 플러그인을 구현하는 것에 관한 내용은 아니다. Eclipse용 플러그인을 구현하는 과정을 통해 Eclipse 통합 개발 환경(IDE)의 기능을 사용하는 방법을 배우는 것이 이 글의 목표이다. 또한, Eclipse IDE의 부분들을 확장하여 여러분의 플러그인에 풍부한 기능도 추가하는 방법을 소개할 것이다. 이 글이 끝나면, 여러분은 Eclipse IDE 플러그인 개발 마법사를 사용하여 새로운 플러그인을 개발할 수 있을 것이다. 또한 클래스를 확장하고 프레퍼런스 페이지, 프로퍼티 페이지, 플러그인의 드래그&드롭 지원을 추가하는데 사용할 수 있는 인터페이스도 구현할 것이다.
인터페이스 구현과 클래스 확장 같은 객체 지향 개념에 대해 잘 알고 있어야 한다. 또한, Eclipse IDE에 친숙해야 하지만, Eclipse용 플러그인 생성 방법까지 알 필요는 없다. Eclipse V3.2 또는 이후 버전이 필요하다. Eclipse의 중첩 인스턴스를 실행하여 플러그인을 테스트 해야 하므로, Eclipse의 두 개의 인스턴스들을 동시에 실행할 정도로 충분한 RAM이 있어야 한다.
플러그인 프로젝트 시작하기
Eclipse IDE는 확장성 있는 프레임웍을 기반으로 구현된다. 여기에서 IDE용 플러그인을 작성할 수 있다. 하지만 이것이 다가 아니다. Eclipse에는 Eclipse 플러그인 프로젝트를 빠르게 시작할 수 있는 템플릿도 있다.
Eclipse 프레임웍을 사용하여 플러그인을 구현하는 방법을 보여줄 수 있는 시나리오를 모색했다. 필자가 선택한 시나리오는 엔터프라이즈 코드 스니펫 플러그인이다. 이 플러그인은 사전 정의된, 범주화 되어 있는 코드 스니펫을 사용하는 기능을 제공한다. 이것은 Eclipse 밖에서 소스 검색에 사용될 수 있고 에디터로 삽입될 수도 있다.
"Example.com"용 코드 스니펫 플러그인은 다음과 같은 기능을 갖고 있다.
- 스니펫을 카테고리 별로 찾는 데 사용하는 트리 뷰
- 스니펫이 로딩되는 위치를 설정하는데 사용하는 프레퍼런스 페이지
- 스니펫을 오픈 에디터로 삽입하는데 사용하는 콘텍스트 중심 메뉴
-
${variable} 폼으로 된 템플릿 변수를 가진 스니펫과 이러한 변수들을 위한 값을 모으는 마법사
프로젝트 생성하기
플러그인을 개발할 새로운 프로젝트를 만든다. 마법사가 여러분을 인도할 것이다:
-
File > New > Project를 선택한다.
-
Plug-in Development 밑에 있는 Plug-in Project를 선택하고 Next를 클릭한다.
-
Project name 옆에서,
SnippetsPlugin을 입력하고 Next를 클릭한다.
-
Plug-in Content 스크린에서, 기본 사항들을 그대로 두고 Next를 클릭한다.
-
Templates 뷰 밑에서, Custom plug-in wizard를 선택하고 Next를 클릭한다.
- 모든 가용 템플릿 리스트에서, Deselect All 버튼을 클릭하고, 다음 템플릿을 선택한다:
-
"Hello world" Action Set
-
Popup Menu
-
Preference Page
-
View
이들 각각이 프로젝트에 어떤 기여를 하는지에 대해서는 "템플릿 이해하기"를 참조하라.
-
Sample Action Set:
-
Action Class Name을
SnippetAction으로 바꾼다.
- 계속 진행하려면 Next를 클릭한다.
-
Sample Popup Menu:
-
Name Filter를
*.*로 바꾼다.
-
Submenu Name을
Snippet Submenu로 바꾼다.
-
Action Class를
SnippetPopupAction으로 바꾼다.
- 계속 진행하려면 Next를 클릭한다.
-
Sample Preference Page:
-
Page Class Name을
SnippetPreferencePage로 바꾼다.
-
Page Name을
Snippet Preferences로 바꾼다.
- 계속 진행하려면 Next를 클릭한다.
-
Main View Settings:
-
View Class Name을
SnippetsView로 바꾼다.
-
View Name을
Example.com Snippets View로 바꾼다.
-
View Category Name을
Example.com Enterprise로 바꾼다.
- 뷰어 유형에 Tree viewer를 선택한다.
- 계속 진행하려면 Next를 클릭한다.
-
View Features 스크린에서, 모든 옵션들을 체크하고 Finish를 눌러 플러그인 프로젝트를 만든다.
Eclipse IDE가 프로세싱을 끝마치면, 새로운 플러그인 프로젝트에 많은 파일들이 생긴다. 이전 단계에서 여러분이 선택하고 설정했던 템플릿은 그 안에서 여러 패키지들과 자바™ 소스 파일들을 만들었다. 이 템플릿에 대한 자세한 내용은 "템플릿 이해하기"를 참조하라. 템플릿에 대해 이미 알고 있거나, 상세한 내용을 보고 싶지 않다면, "플러그인 테스트하기"로 바로 가도 좋다.
템플릿 이해하기
Eclipse에서 새로운 프로젝트를 만든 후에, 어떤 일이 발생하는지 궁금할 것이다. 이전 단계를 따라갔다면, 프로젝트에는 자바 소스 파일들과 기타 파일들을 포함하고 있는 패키지들이 포함되어 있을 것이다. 여러분이 플러그인 개발에 생소하다면, 이들이 무엇이고, 어떻게 생겼는지 궁금할 것이다.
본 섹션에서는 이 글에서 사용되는 템플릿을 설명할 것이다. 다양한 설정 옵션들을 다루고, 이러한 조각들을 합쳐서 플러그인을 만드는 방법을 설명한다. 다음 섹션을 읽은 후에는 여러분도 충분이 이 부분들을 구현할 수 있을 것이다.
Eclipse IDE에서, 액션 세트(action set)는 메뉴 아이템 또는 버튼용 액션 그룹이다. 이는 로컬에서 그룹핑 되어야 한다. 플러그인에는 하나의 액션 세트가 있다. 플러그인과 함께 작동하는 모든 액션들은 모두 그룹핑 되어야 한다.
"Hello world" Action Set 템플릿은 snippetssample.action 패키지에 SnippetAction.java 파일을 추가한다. 이 클래스의 이름은 Sample Action Set 설정 시 Action Class Name으로 설정된다.
그림 1. 액션 세트 설정하기
Eclipse에서 플러그인을 실행할 때, "Hello, world" Action Set는 메뉴 바에 Sample Menu라는 레이블링이 된 메뉴로서 나타난다. 이 메뉴 레이블에 사용되는 텍스트는 plugin.xml 파일에서 정의된다.
Listing 1. plugin.xml의 액션 세트
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.2"?>
<plugin>
<extension
point="org.eclipse.ui.actionSets">
<actionSet
label="Sample Action Set"
visible="true"
id="SnippetsPlugin.actionSet">
<menu
label="Sample &Menu"
id="sampleMenu">
<separator
name="sampleGroup">
</separator>
</menu>
<action
label="&Sample Action"
icon="icons/sample.gif"
class="snippetsplugin.actions.SnippetAction"
tooltip="Hello, Eclipse world"
menubarPath="sampleMenu/sampleGroup"
toolbarPath="sampleGroup"
id="snippetsplugin.actions.SnippetAction">
</action>
</actionSet>
</extension>
...
</plugin>
|
Eclipse에서 메뉴를 클릭하면, SnippetAction 클래스의 run 메소드에 있는 코드가 실행된다. 아래 보이는 템플릿 코드는 메시지 박스를 나타낸다.
Listing 2. SnippetAction run() 메소드
public void run(IAction action) {
MessageDialog.openInformation(
window.getShell(),
"SnippetsPlugin Plug-in",
"Hello, Eclipse world");
}
|
Popup Menu
Popup Menu 템플릿은 파일을 오른쪽 클릭하면 나타나는 메뉴 아이템을 제공한다. — 가끔 context-sensitive menu라고도 한다. Package Explorer에 있는 파일을 오른쪽 클릭하면, Snippet Submenu라는 레이블링이 달린 메뉴를 보게 되는데, 여기에는 New Action이라고 하는 하나의 메뉴 아이템이 포함되어 있다. Sample Popup Menu 설정은 아래와 같다.
그림 2. 팝업 메뉴 설정하기
Target Object's Class는 기본적으로 org.eclipse.core.resources.IFile을 가리키는데, 사용자가 파일을 오른쪽 클릭할 경우에만 팝업 메뉴를 사용할 것을 Eclipse에게 명령하고 있다. 여러분이 사용하게 될 기타 리소스에는 IProject와 IFolder가 있다. IProject는 사용자가 프로젝트를 오른쪽 클릭할 때에만 메뉴를 보여주고, IFolder는 사용자가 폴더를 오른쪽 클릭할 때 메뉴를 보여준다.
Name Filter는 특정 파일이 선택될 때에만 팝업 메뉴를 보여준다. 예를 들어, 필터에 *.html을 지정하면, 사용자가 .html로 끝나는 이름을 가진 파일을 오른쪽 클릭할 때에만 아이템이 나타난다. 이 값은 마법사가 프로젝트를 생성했을 때 plugin.xml에서 정의된다.
Submenu Name은 하위 메뉴를 나타내는 팝업 메뉴에 있는 메뉴 아이템의 이름이다. 프로젝트를 만든 후에, plugin.xml 파일에 있는 값을 변경할 수 있다.
Action Label은 서브 메뉴에 나타나는 메뉴 아이템의 이름이다. 이 값 역시 plugin.xml 파일에 작성되는데, 나중에 이를 변경할 수 있다.
Java Package Name은 새로운 클래스를 포함하고 있는 패키지 이름이다. 이 클래스 이름은 Action Class에 의해 정의된다. 이는 다른 값처럼 나중에 수정될 수 있지만 쉽게 수정될 수는 없다. 패키지와 클래스를 재 명명 할 경우, Eclipse의 리팩토링 기능을 사용하여 plugin.xml의 레퍼런스가 이에 따라 업데이트 되었는지를 확인해야 한다.
Listing 3은 팝업 메뉴 확장에 의해 사용되는 plugin.xml 파일의 부분들이다.
Listing 3. 팝업 메뉴 확장
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.2"?>
<plugin>
...
<extension
point="org.eclipse.ui.popupMenus">
<objectContribution
objectClass="org.eclipse.core.resources.IFile"
nameFilter="*.*"
id="SnippetsPlugin.contribution1">
<menu
label="Snippet Submenu"
path="additions"
id="SnippetsPlugin.menu1">
<separator
name="group1">
</separator>
</menu>
<action
label="New Action"
class="snippetsplugin.popup.actions.SnippetPopupAction"
menubarPath="SnippetsPlugin.menu1/group1"
enablesFor="1"
id="SnippetsPlugin.newAction">
</action>
</objectContribution>
</extension>
...
</plugin>
|
팝업 메뉴 아이템을 클릭하면, Eclipse는 SnippetPopupAction 클래스의 run() 메소드(Listing 4)의 코드를 실행한다. 기본적으로, 이 코드는 메시지 박스를 디스플레이 한다.
Listing 4. SnippetPopupAction run() 메소드
public void run(IAction action) {
Shell shell = new Shell();
MessageDialog.openInformation(
shell,
"SnippetsPlugin Plug-in",
"New Action was executed.");
} |
Preference Page
Preference Page 템플릿에는 플러그인용 프레퍼런스를 설정하는데 사용할 수 있는 페이지가 포함되어 있다. 이 페이지는 Eclipse에서 Window > Preferences 메뉴 아이템에서 볼 수 있다. 프레퍼런스 페이지 템플릿을 설정하는 마법사 페이지는 아래와 같다.
그림 3. 프레퍼런스 페이지 설정하기
Java Package Name은 프레퍼런스 페이지와 이것을 지원하는 클래스를 가져오는데 사용되는 페이지를 포함하고 있는 패키지의 이름이다. Page Class Name은 페이지 클래스의 이름이고, Page Name은 프레퍼런스 리스트에 나타나는 이름이다. Snippet Preferences로 바꾸었던 프레퍼런스 페이지의 이름은 plugin.xml 파일에서 수정될 수 있다.
SnippetPreferencePage 클래스 외에도, 이 템플릿은 두 개의 다른 지원 클래스를 생성한다. 이들은 같은 패키지에 있다. 이 중에서, PreferenceConstants에는 프레퍼런스를 구분하는데 사용되는 상수들이 포함된다. 다른 하나인 PreferenceInitializer는 프레퍼런스를 기본 값으로 초기화 한다.
Preference Page 템플릿을 사용하면 가장 좋은 점은 프레퍼런스를 저장하고 가져올 코드를 작성할 필요가 없다는 점이다. — 이는 프레퍼런스 페이지 상에서 컨트롤에 의해 자동으로 관리된다.
View
View 템플릿은 플러그인을 테스트할 때 Eclipse 아이디에서 보이는 샘플 뷰를 만든다. ("플러그인 테스트 하기")
Main View Settings 스크린은 뷰 확장 용 설정을 모은다.
그림 4. 뷰 설정하기
Java Package Name은 다른 것과 비슷하다. 새로운 클래스들이 배치될 곳에 패키지의 이름을 모은다.
View Class Name은 뷰를 가져오는데 사용되는 자바 클래스 이름이다.
View Name은 Window > Show View 메뉴에서 구분된 것처럼 뷰의 이름이다.
View Category ID는 카테고리를 구분하는 방식이고, View Category Name은 카테고리의 이름이다. 이 카테고리는 Window > Show View를 사용하여 뷰를 보여주기 위해 선택할 때 뷰를 구성하는 그룹이다.
뷰어 유형은 뷰 클래스(SnippetView)가 생성되는 방식을 바꾼다. Table viewer를 선택하면, 뷰는 아이템 리스트만 보여준다. 아이템 리스트는 getElements() 메소드에 의해 생성된다.
뷰어 유형이 Tree viewer라면, 이 뷰는 다르게 생성된다. getElements() 메소드는 TreeObject 객체 세트를 리턴하도록 변경되고, 내부 클래스 ViewContentProvider는 ITreeContentProvider 인터페이스를 구현하도록 수정된다.
뷰 클래스
View 템플릿에서 생성된 SnippetView 클래스는 이 프로젝트에 구현된 클래스들 중 가장 복잡하다. Eclipse의 Outline 뷰에서 이 클래스를 본다면, 내부 클래스가 있다는 것도 알게 된다.
-
TreeObject
- 아이템 트리의 리프 노드(leaf node)를 나타내는 클래스이다. 다른 아이템을 포함하고 있지 않다.
-
TreeParent
- 다른 아이템들을 포함할 수 있는 트리에 있는 아이템을 나타내는 클래스이다.
-
ViewContentProvider
-
IStructuredProvider와 ITreeContentProvider를 구현하고, 뷰에 디스플레이 할 콘텐트를 가져오는 클래스이다. 이 글 후반에, 이 클래스 대부분을 수정하여 코드 스니펫 상세를 뷰에 제공한다.
-
ViewLabelProvider
- 트리 아이템을 위해 아이콘 이미지를 제공하는 클래스.
-
NameSorter
- 뷰의 아이템용 커스텀 정렬 기능을 구현하기 위해 수정될 수 있는 클래스. 이 글에서는 그대로 놔두기로 한다. 자세한 정보는
ViewerSorter 클래스에 대한 Eclipse platform API 문서를 참조하라. (참고자료).
플러그인 테스트 하기
지금까지, 여러분이 선택한 템플릿에서 생성되었던 프로젝트에 많은 자바 클래스들이 생겼다. 진행하기 전에 이 플러그인을 테스트하여 템플릿이 무엇을 수행하는지 봐야 한다. 이렇게 하려면, Eclipse의 인스턴스를 실행해야 한다. Eclipse의 중첩 인스턴스를 실행하는 쉬운 방법은 Package Explorer 뷰에서 플러그인 프로젝트를 오른쪽 클릭하고 팝업 메뉴에서 Run As > Eclipse Application을 선택하는 것이다.
Eclipse의 중첩 인스턴스가 시작되면, 플러그인의 다양한 조각들이 어떤 모습인지를 보게 된다. 액션 세트 클래스를 보려면 Eclipse의 메뉴 바를 본다. Sample Menu 메뉴는 메뉴 바에 나타난다. Sample Menu
> Sample Item을 클릭하면, Eclipse는 "Hello, Eclipse world"라는 문구의 SnippetSample Plug-in이 있는 메시지 박스를 디스플레이 한다.
Eclipse의 중첩 인스턴스를 끝내고 Listing 5의 볼드체로 되어 있는 스트링 값을 수정한다. 이는 SnippetAction 클래스에 위치해 있다.
Listing 5. SnippetAction에 디스플레이 되는 메시지
public void run(IAction action) {
MessageDialog.openInformation(
window.getShell(),
"SnippetsPlugin Plug-in",
"Hello, Eclipse world");
}
|
Eclipse의 중첩 인스턴스를 다시 실행하고, 메뉴 아이템을 선택하고, 이것이 새로운 값으로 변경되었는지를 확인한다. 메뉴 또는 메뉴 아이템의 이름이 마음에 들지 않으면, plugin.xml 파일에서 바꿀 수 있다. 아래 볼드체 부분을 참조하라.
Listing 6. plugin.xml의 메뉴 아이템 이름
<menu
label="Sample &Menu"
id="sampleMenu">
<separator
name="sampleGroup">
</separator>
</menu>
<action
label="&Sample Action"
icon="icons/sample.gif"
class="snippetsplugin.actions.SnippetAction"
tooltip="Hello, Eclipse world"
menubarPath="sampleMenu/sampleGroup"
toolbarPath="sampleGroup"
id="snippetsplugin.actions.SnippetAction">
</action>
|
Eclipse의 팝업 메뉴
Eclipse에서 팝업 메뉴를 보려면, 중첩 인스턴스를 시작한다. 이것이 시작된 후에, 패러 뷰에 파일을 배치한다. 어떤 파일도 없다면, 샘플 파일을 포함하고 있는 프로젝트를 만든다. 파일을 오른쪽 클릭하면, 하나의 New Action 서브 메뉴를 가진 Snippet Submenu 메뉴를 보게 된다. 메뉴 아이템을 클릭하면 Eclipse는 "New Action was executed."라는 문구의 메시지 박스를 디스플레이 한다.
Eclipse의 프레퍼런스 페이지
Eclipse에서 프레퍼런스 페이지를 보려면, Eclipse의 중첩 인스턴스에서 Window > Preferences를 선택한다. Snippet Preferences라고 하는 프레퍼런스 카테고리를 보게 된다. 이 리스트에서 카테고리를 선택하면, 프레퍼런스 페이지가 열리고 여기에 많은 컨트롤이 있다. 이 컨트롤들은 템플릿에 자동으로 포함된다.
나중에, 이러한 프레퍼런스를 플러그인에 더 맞게 바꿀 수 있다. 지금은 어떤 값도 변경할 수 있고, 여기에 Apply 또는 OK 버튼을 사용하고, 다시 돌아가서 수정 사항이 저장 및 재 로딩 되었는지를 확인한다. 이는 커스텀 코드를 추가할 필요 없이 수행되었다.
프레퍼런스 페이지를 가져오는 코드는 아래 메소드의 SnippetPreferencePage 클래스에 있다.
Listing 7. SnippetPreferencePage createFieldEditors
()
public void createFieldEditors() {
addField(new DirectoryFieldEditor(PreferenceConstants.P_PATH,
"&Directory preference:", getFieldEditorParent()));
addField(
new BooleanFieldEditor(
PreferenceConstants.P_BOOLEAN,
"&An example of a boolean preference",
getFieldEditorParent()));
addField(new RadioGroupFieldEditor(
PreferenceConstants.P_CHOICE,
"An example of a multiple-choice preference",
1,
new String[][] { { "&Choice 1", "choice1" }, {
"C&hoice 2", "choice2" }
}, getFieldEditorParent()));
addField(
new StringFieldEditor(PreferenceConstants.P_STRING,
"A &text preference:", getFieldEditorParent()));
}
|
Eclipse의 뷰
Eclipse의 중첩 인스턴스에서 플러그인의 뷰를 보려면 Window > Show View > Other를 선택한다. 이 뷰는 Example.com Snippets 카테고리 밑에서 구성되는데, 이는 View 설정에서 지정되고(그림 4) plugin.xml에 배치된다. Snippet View를 선택하고 OK를 클릭하여 뷰를 연다.
기본적으로, 뷰에는 몇 가지 아이템들이 들어 있다. 그림 5에 보이는 것처럼 뷰에 아이템 트리가 있다.
그림 5. 뷰 배치
뷰에 있는 아이템을 더블 클릭하면, Eclipse는 "Double-click detected on XXX"라고 하는 메시지 박스를 디스플레이 하는데, 여기에서 XXX는 트리 아이템의 텍스트이다. 두 개의 추가 액션들이 이 뷰에 추가되었다. 뷰에서 아무데나 오른쪽 클릭할 때 이들은 뷰의 메뉴 바에 나타난다. 이 액션은 makeActions() 메소드에 동적으로 추가된다
Listing 8. makeActions() 메소드
private void makeActions() {
action1 = new Action() {
public void run() {
showMessage("Action 1 executed");
}
};
action1.setText("Action 1");
action1.setToolTipText("Action 1 tooltip");
action1.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().
getImageDescriptor(ISharedImages.IMG_OBJS_INFO_TSK));
action2 = new Action() {
public void run() {
showMessage("Action 2 executed");
}
};
action2.setText("Action 2");
action2.setToolTipText("Action 2 tooltip");
action2.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().
getImageDescriptor(ISharedImages.IMG_OBJS_INFO_TSK));
doubleClickAction = new Action() {
public void run() {
ISelection selection = viewer.getSelection();
Object obj = ((IStructuredSelection)selection).getFirstElement();
showMessage("Double-click detected on "+obj.toString());
}
};
}
|
SnippetProvider 구현하기
클래스의 프레임웍을 구현하여 코드 스니펫을 가져오려면, 인터페이스를 작성하고 그 작업을 수행하는 구현 클래스를 구현해야 한다. 인터페이스는 SnippetView에 의해 사용되어 디스플레이용 스니펫을 로딩한다. SnippetView 클래스는 스니펫이 로딩되는 방법, 인터페이스 사용의 중요성 등에 대해 알 필요가 없다.
SnippetProvider 인터페이스를 만드는 것으로 시작한다. File > New > Interface를 선택하고 인터페이스를 com.example.plugins.snippets.providers 패키지에 두는데, 여기에는 구현 및 팩토리 클래스가 포함된다.
SnippetProvider.java의 콘텐트는 아래와 같다.
Listing 9. SnippetProvider 인터페이스
public interface SnippetProvider {
public String[] getLanguages() throws SnippetProviderException;
public String[] getCategories(String language) throws SnippetProviderException;
public SnippetInfo[] getSnippetInfo(String language, String category)
throws SnippetProviderException;
public Snippet getSnippet(SnippetInfo info);
public void configure(Properties props) throws SnippetProviderConfigurationException;
} |
이 플러그인의 목표는 같은 종류의 저장소에서 코드 스니펫을 가져오는 것이므로, 스니펫을 저장 또는 업데이트 하기 위한 메소드가 필요 없다. 언어(자바 또는 XML), 카테고리, 스니펫에 대한 정보, 스니펫 자체를 가져오는데 사용되는 메소드들만 있다. 메소드와 이에 대한 설명은 아래 표를 참조하라.
표 1. SnippetProvider 인터페이스 관련 메소드
| 메소드 | 설명 |
|---|
configure(Properties props)
| 어댑터 설정 |
getCategories(String language)
| 각 언어(자바, XML)이 스니펫이 구성된 카테고리 세트를 갖고 있다. |
getLanguages()
| 언어 리스트 가져오기. 스니펫을 범주화 하는 고급 방식. |
getSnippet(SnippetInfo info)
| 특정 스니펫 리턴하기 |
getSnippetInfo(String language, String category)
| 뷰에 보일 수 있는 SnippetInfo 객체 어레이 가져오기. |
SnippetFileProvider 구현 클래스
인터페이스가 완성되면, 이 인터페이스를 사용하는 구현 클래스를 구현하기가 쉽다. 이 섹션에서는, 파일 시스템의 디렉토리에서 코드 스니펫을 가져오는데 사용되는 구현 클래스를 추가한다.
 |
기타 클래스들 ...
두 개의 클래스, SnippetInfo와 Snippet을 주목하라. 이 클래스들은 다운로드 섹션을 참조하라. 일부 예외 클래스(SnippetProviderException), SnippetProvider 구현 (SnippetProviderFactory)을 로딩하는데 사용되는 팩토리 클래스 등이 있다. 이러한 지원 클래스들은 유용하지만, 이 글에서는 다루지 않을 것이다.
|
|
인터페이스 메소드를 직접 구현하는 것을 새로운 클래스에 추가하는 것 보다, Eclipse를 사용하여 클래스의 시작을 구현한다. 아이디로 가능한 많은 작업을 수행하려면, com.example.plugins.snippets.providers 패키지를 오른쪽 클릭하고, New > Class를 선택한다. Name에 SnippetFileProvider를 입력하고 인터페이스 리스트 옆에 있는 Add 버튼을 클릭한다. Choose interfaces에서, SnippetFileProvider를 입력하고 OK를 클릭한다.
Eclipse 아이디는 SnippetProvider 인터페이스를 구현하는 새로운 클래스 파일을 생성한다. 모든 메소드들이 생성되고 리턴 문도 생성되기 때문에, 프로젝트는 깨끗하게 컴파일 된다.
이 새로운 클래스는 언어와 카테고리용 폴더를 사용하여 파일 시스템에서 코드 스니펫을 로딩한다. 이것은 snippets.info라고 하는 XML 파일에 있는 코드 스니펫 정보를 로딩한다. 이 파일에는 XMLEncoder 클래스를 사용하여 XML로 직렬화 된 SnippetInfo 아이템 어레이의 XML 표현이 포함되어 있다.
아래 보이는 configure() 메소드는 Properties 객체에 주어진 어댑터를 설정하는 방식을 제공한다. SnippetFileProvider의 경우, 설정되는 유일한 프로퍼티는 베이스 Uniform Resource Identifier (URI)인데, 이는 코드 스니펫 구조를 포함하고 있는 디렉토리의 기반 경로이다.
Listing 10. SnippetFileProvider configure()
public void configure(Properties props)
throws SnippetProviderConfigurationException {
this.properties = props;
String uriPath = "";
if (baseUri == null) {
try {
uriPath = properties
.getProperty("snippetFileProvider.base.directory");
if (uriPath == null || uriPath.length() == 0) {
throw new SnippetProviderConfigurationException(
"Please supply a value for property " +
"'snippetFileProvider.base.directory'");
}
baseUri = new URI(uriPath);
} catch (URISyntaxException urie) {
throw new SnippetProviderConfigurationException("URI '"
+ uriPath + "' incorrectly formatted.", urie);
}
}
} |
getLanguages() 메소드는 베이스 디렉토리 밑에서 직접 디렉토리의 이름을 가져온다. — 이들은 자바, XML, HTML — 같은 언어데 사용되는데, 코드 스니펫의 고급 목록화로서 사용된다. SnippetFileProvider 클래스에서 구현된 메소드는 아래와 같다.
Listing 11. SnippetFileProvider getLanguages()
public String[] getLanguages() throws SnippetProviderException {
/*
* The languages will be the high-level directories right underneath the
* base directory.
*/
if (languages == null) {
languages = getFormattedNamesFromLocation(getBaseUri());
}
return languages;
} |
이 메소드 호출은 getFormattedNamesFromLocation이라고 하는 정적 메소드를 호출하면서 이것을 기본 URI에 전달한다. 정적 메소드는 디렉토리에서 발견된 올바르게 포맷 된 아이템 이름을 포함하고 있는 String의 어레이를 리턴한다. 각 언어 카테고리에는 스니펫을 하위 카테고리로 분류하는데 사용되는 또 다른 폴더 세트가 포함된다. 자바 언어에 대한 한 가지 예는 예외 핸들링용 카테고리 또는 로깅용 카테고리가 될 수 있다. 카테고리 로딩에 대한 메소드는 아래와 같다.
Listing 12. SnippetFileProvider getCategories()
public String[] getCategories(String language)
throws SnippetProviderException {
try {
return getFormattedNamesFromLocation(new URI(getBaseUri().getPath()
+ "/" + language));
} catch (URISyntaxException e) {
throw new SnippetProviderException(
"Error while loading the categories", e);
}
} |
getSnippetInfo() 메소드는 카테고리 폴더에서 발견된 snippets.info 파일에서 SnippetInfo 객체의 어레이를 로딩한다. XML 파일에는 이 카테고리에서 발견된 코드 스니펫에 관한 정보가 포함된다. 이름, 디스크립션, 변수들이 극서이다. 메소드는 아래와 같다.
Listing 13. SnippetFileProvider getSnippetInfo()과 getSnippet() 메소드
public SnippetInfo[] getSnippetInfo(String language, String category)
throws SnippetProviderException {
/* Dehydrate the snippet info from the filesystem */
XMLDecoder decoder = null;
SnippetInfo[] snippetInfo = null;
try {
decoder = new XMLDecoder(new BufferedInputStream(
new FileInputStream(buildSnippetInfoPath(getBaseUri(),
language, category))));
snippetInfo = (SnippetInfo[]) decoder.readObject();
} catch (FileNotFoundException e) {
throw new SnippetProviderException(
"Could not load the snippet info index.", e);
} finally {
if (decoder != null) {
decoder.close();
}
decoder = null;
}
return snippetInfo;
}
public Snippet getSnippet(SnippetInfo info) {
Snippet snippet = null;
String snippetPath = buildSnippetPath(getBaseUri(), info);
/* Load the snippet from the file */
BufferedInputStream stream = null;
BufferedReader reader = null;
try {
stream = new BufferedInputStream(new FileInputStream(snippetPath));
reader = new BufferedReader(new InputStreamReader(
stream));
String line;
StringBuffer sb = new StringBuffer();
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append("\n");
}
snippet = new Snippet();
snippet.setContent(sb.toString());
sb = null;
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException ioe) {
// TODO Auto-generated catch block
ioe.printStackTrace();
}
finally
{
try {
if (reader != null) {
reader.close();
}
if (stream != null) {
stream.close();
}
} catch (IOException ioe) {
}
}
return snippet;
} |
마지막으로, getSnippet() 메소드는 파일 시스템에서 코드 스니펫을 로딩하고 Snippet 객체에서 이를 리턴한다. 클래스에 있는 나머지 메소드들은 퍼블릭 메소드를 지원하는데 사용되는 프라이빗(private) 메소드이다. 다운로드 섹션에서 제공하는 소스 코드에서 이를 자세히 검토하기 바란다.
뷰 채우기
파일 시스템상의 디렉토리에서 코드 스니펫을 가져오는데 필요한 프레임웍을 갖췄다면, 올바른 코드를 SnippetView 클래스에 추가하여 플러그인의 뷰에 있는 트리에 스니펫 정보를 보여줄 준비가 된 것이다. 뷰에 스니펫을 보여주려면, SnippetView에 있는 ViewContentProvider 내부 클래스의 initialize() 메소드를 수정한다.
initialize() 메소드는 뷰에 나타난 아이템의 트리를 구현한다. 클래스가 처음에 템플릿에서 만들어질 때, 많은 더미(dummy) 데이터가 포함되므로, Eclipse에서 플러그인을 실행할 때 무엇인가를 봐야 한다. 지금까지 단계를 따라왔다면, 이 메소드를 수정하여 스니펫 정보를 로딩할 때 SnippetProvider를 사용하도록 한다.
새로운 메소드는 아래와 같다.
Listing 14. 새로운 SnippetsView initialize()
private void initialize() {
invisibleRoot = new TreeParent("");
/*
* Get the high-level elements from the provider, which are the
* languages.
*/
String[] topLevelNodes;
try {
Properties properties = new Properties();
InputStream is = null;
is = SnippetProviderFactory.class
.getResourceAsStream("/SnippetProvider.properties");
properties.load(is);
if (is != null) {
try {
is.close();
} catch (IOException innerE) {
throw new SnippetProviderException(
"Could not close resource stream.", innerE);
}
}
snippetProvider = SnippetProviderFactory.createInstance();
snippetProvider.configure(properties);
topLevelNodes = snippetProvider.getLanguages();
for (int i = 0; i < topLevelNodes.length; i++) {
TreeParent parent = new TreeParent(topLevelNodes[i]);
/* Get the categories for each one of the parents */
String[] categories = snippetProvider
.getCategories(topLevelNodes[i]);
for (int j = 0; j < categories.length; j++) {
TreeParent categoryParent = new TreeParent(
categories[j]);
/* Now get the snippet names for the categories */
SnippetInfo[] info = snippetProvider.getSnippetInfo(
topLevelNodes[i], categories[j]);
for (int k = 0; k < info.length; k++) {
TreeObject leaf = new TreeObject(info[k]);
categoryParent.addChild(leaf);
}
parent.addChild(categoryParent);
}
invisibleRoot.addChild(parent);
}
} catch (SnippetProviderConfigurationException spce) {
topLevelNodes = new String[] { "Configuration error: "
+ spce.getLocalizedMessage() };
} catch (SnippetProviderException spe) {
topLevelNodes = new String[] { "Error while loading snippets" };
} catch (IOException ioe) {
topLevelNodes = new String[] { "Error loading configuration properties:"
+ ioe.getLocalizedMessage() };
}
} |
새로운 코드는 SnippetProviderFactory를 사용하여 동적으로 클래스를 로딩하고, 그 결과를 SnippetProvider 인터페이스에 즉시 보낸다. 뷰는 스니펫의 소스에 대해 알 수 없다. 공급자의 인터페이스가 획득되면, 공급자는 코드 스니펫 구조를 어디에서 얻는지를 안다. 그리고 나서, 탑 레벨(top-level) 노드가 각 언어를 위해 추가된다.
 |
사소하다고 생각하십니까?
SnippetProvider의 구현을 쉽게 볼 수 있고 각각의 언어나 카테고리를 통해 반복하여 아이템을 가져와서 여러 호출을 만들기 때문에 이것이 "사소하다고" 생각할 수 있다. 덜 사소한 구현은 전체 구조를 한번에 로딩한다. 이러한 방식은 웹 서비스 구현에 더 낫다. 구현 클래스는 메소드가 성능과 확장성에 부합할 경우 얼마든지 사용할 수 있다.
|
|
이 코드는 실행되고, 언어에서 반복되고, 각 언어에 대한 카테고리를 가져와서 이를 트리의 부모 노드에 추가한다. 각 카테고리의 경우, 뷰는 getSnippetInfo() 메소드를 호출하여 카테고리에 포함된 코드 스니펫에 대한 정보를 가져온다. 이것은 어레이를 통해 반복하고 각 SnippetInfo에 대한 새로운 TreeObject를 구현한다.
이 새로운 코드에서 알아야 할 사항이 있다. 우선, SnippetProviderFactory는 기본 구현 클래스를 로딩한다. 이것은 플러그인을 사용하여 다른 장소에서 코드 스니펫을 가져오기 전에 변경되어야 한다. 둘째, 어댑터는 파일에서 로딩된 프로퍼티로부터 설정된다. 이는 플러그인이 분산되기 때문에 바뀌어야 한다. 사용자는 프로퍼티를 변경할 수 있어야 한다. 이 두 가지는 프레퍼런스로부터 로딩된다. 프레퍼런스에서 이 값을 로딩하는 프로세스는 "사용자 프레퍼런스 추가하기"를 참조하라.
TreeObject로 바꾸기
트리 뷰에서 선택된 스니펫에 대해 여러분이 필요로 하는 모든 정보를 가져올 수 있으려면, 이름을 TreeObject로 제휴시켜야 한다. TreeObject 내부 클래스를 약간 조정하여 이 문제를 해결할 수 있다. 프라이빗 String 이름 필드를 SnippetInfo 객체를 보유하고 있는 프라이빗 필드로 대체해야 한다. 또한, 생성자와 getName() 메소드를 바꾸어야 한다.
수정된 TreeObject 클래스는 아래와 같다. 바뀐 부분은 볼드체로 표시되어 있다.
Listing 15. 수정된 TreeObject
class TreeObject implements IAdaptable {
private SnippetInfo info;
private TreeParent parent;
public TreeObject(SnippetInfo info) {
this.info = info;
}
public String getName() {
return info.getName();
}
public void setParent(TreeParent parent) {
this.parent = parent;
}
public TreeParent getParent() {
return parent;
}
public String toString() {
return getName();
}
public SnippetInfo getInfo() {
return info;
}
public Object getAdapter(Class key) {
return null;
}
} |
TreeParent 객체는 TreeObject를 확장하기 때문에, 마찬가지로 두 가지를 바꾸어야 한다. 아래와 같다.
Listing 16. 수정된 TreeParent
class TreeParent extends TreeObject {
private ArrayList children;
private String name;
public TreeParent(String name) {
super(null);
this.name = name;
children = new ArrayList();
}
@SuppressWarnings("unchecked")
public void addChild(TreeObject child) {
children.add(child);
child.setParent(this);
}
public void removeChild(TreeObject child) {
children.remove(child);
child.setParent(null);
}
@SuppressWarnings("unchecked")
public TreeObject[] getChildren() {
return (TreeObject[]) children.toArray(new TreeObject[children
.size()]);
}
public boolean hasChildren() {
return children.size() > 0;
}
@Override
public String getName() {
// TODO Auto-generated method stub
return this.name;
}
@Override
public String toString() {
return this.getName();
}
} |
getName()과 toString() 메소드를 오버라이드 하고, 생성자가 상위 생성자로 null을 보내지 못하도록 수정하고 이름을 새로운 프라이빗 String 필드에 할당하도록 수정한다.
디렉토리 구조 준비하기
Eclipse에서 새로운 스니펫 공급자 구현을 실행하기 전에, 샘플 스니펫의 저장소를 만들어야 한다. 다행히도, 파일 기반의 저장소는 만들기 쉽다. 고유의 파일 기반 구조를 구현하거나 코드에서 제공하는 예제를 사용할 수 있다.
파일 기반 저장소를 만들려면, 언어에 한 개 이상의 디렉토리를 전개한다. 그러한 디렉토리들 밑에, 카테고리에 맞게 한 개 이상의 디렉토리를 추가한다. 마지막으로, 각 카테고리에서, snippets.info라고 하는 파일을 추가한다. 그 파일에서 Listing 17의 콘텐트를 추가한다.
Listing 17. snippets.info 샘플
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.5.0_07" class="java.beans.XMLDecoder">
<array class="com.example.plugins.snippets.SnippetInfo" length="1">
<void index="0">
<object class="com.example.plugins.snippets.SnippetInfo">
<void property="category">
<string>Exception_Handling</string>
</void>
<void property="language">
<string>Java</string>
</void>
<void property="name">
<string>Exception class</string>
</void>
<void property="variables">
<array class="java.lang.String" length="3">
<void index="0">
<string>exception.classname</string>
</void>
<void index="1">
<string>author</string>
</void>
<void index="2">
<string>package.name</string>
</void>
</array>
</void>
</object>
</void>
</array>
</java>
|
각 파일을 업데이트 하여 언어와 카테고리를 매치 시켜야 한다. 파일을 추가한 후에, 각 스니펫을 포함하고 있는 파일을 추가한다. 디렉토리 구조 예제는 아래와 같다.
Listing 18. 스니펫 파일 기반 저장소 구조
+- basedir
+- Java
| +- Exception_Handling
| +- snippets.info
| +- Exception_Class.snippet
+- XML
+- Ant_Build_Files
+- snippets.info
+- Simple_File.snippet
|
변경 사항 보기
모든 지원 클래스들이 생성되고 파일 기반 코드 스니펫 저장소가 구현된 상황에서, 이제 Eclipse의 중첩 인스턴스를 시작하여 스니펫 리스팅이 어떻게 보이는지를 알 수 있다. 모든 것이 잘 작동된다면, 그림 6과 같은 모습을 보게 된다.
그림 6. 스니펫 보기
코드 스니펫 정보를 올바르게 로딩한 상태에서, 다음 단계를 준비한다. 코드를 추가하여 .snippet 파일의 콘텐트를 에디터로 가져온다.
InsertSnippetAction 클래스 작성하기
SnippetFileProvider 구현은 스니펫 정보를 뷰에 로딩했으니, 여러분은 스니펫 정보를 사용하여 공급자에게서 코드 스니펫 텍스트를 가져오고 이를 오픈 에디터에 삽입하는 코드를 구현할 준비가 되었다. 스니펫 정보는 SnippetInfo의 인스턴스에 저장되는데, 이는 Example.com Snippets 뷰에서 선택된 아이템에서 가져온 것이다.
InsertSnippetAction 내부 클래스를 구현하는 것으로 시작한다. 여기에는 스니펫 콘텐트를 가져오고 삽입을 수행하는 코드가 포함된다. 이는 Action 클래스를 확장하여 나머지 플러그인 컴포넌트에 의해 쉽게 사용될 수 있도록 한다. 이것은 SnippetsView에 대한 내부 클래스이다. 뷰에 의해 개인적으로 사용되고 뷰에 있는 객체로 쉬운 액세스를 제공한다.
새로운 InsertSnippetAction 클래스가 아래 보이고 있다. run() 메소드에 메시지 박스가 있다. 메시지 박스는 새로운 InsertSnippetAction이 올바르게 구현되었는지 올바른 장소에 추가되었는지를 확인할 수 있기 때문에 유용하다.
Listing 19. InsertSnippetAction
class InsertSnippetAction extends Action {
@Override
public void run() {
MessageDialog.openInformation(
window.getShell(),
"SnippetsSample Plug-in",
"Running Insert Action Now!");
}
} |
SnippetView로 바꾸기
SnippetView가 템플릿에서 처음 생성될 때, 여기에는 뷰 툴바와 팝업 메뉴에서 액세스 될 수 있는 두 개의 샘플 액션이 포함된다. 이들은 로컬 액션이고 plugin.xml 파일에서 확장 포인트로서 설정되지 않는다. 뷰가 이러한 옵션을 완벽히 소유하고 아이디의 다른 부분에 기여하지 않기 때문에 좋은 것이다.
InsertSnippetAction 클래스를 구현한 후에, SnippetsView 클래스를 열고 action2와 doubleClickAction을 제거한다. action1에 주목하자. — Eclipse의 리팩토링 툴을 사용하여 이것의 이름을 insertAction으로 바꾸고, 유형을 Action에서 InsertAction으로 바꾼다. InsertAction이 Action을 확장하기 때문에, 어떤 것도 바꿀 필요가 없다. Listing 20은 새로운 SnippetsView 클래스 모습이다. 바뀐 부분은 볼드체로 표시했다.
Listing 20. SnippetsView
public class SnippetsView extends ViewPart implements ISelectionListener {
private TreeViewer viewer;
private DrillDownAdapter drillDownAdapter;
private InsertSnippetAction insertAction;
private SnippetProvider snippetProvider;
class InsertSnippetAction extends Action {
// Snipped...
}
class TreeObject implements IAdaptable {
// Snipped...
}
class TreeParent extends TreeObject {
// Snipped...
}
class ViewContentProvider implements IStructuredContentProvider,
ITreeContentProvider {
private TreeParent invisibleRoot;
public void inputChanged(Viewer v, Object oldInput, Object newInput) {
}
public void dispose() {
}
public Object[] getElements(Object parent) {
if (parent.equals(getViewSite())) {
if (invisibleRoot == null)
initialize();
return getChildren(invisibleRoot);
}
return getChildren(parent);
}
public Object getParent(Object child) {
if (child instanceof TreeObject) {
return ((TreeObject) child).getParent();
}
return null;
}
public Object[] getChildren(Object parent) {
if (parent instanceof TreeParent) {
return ((TreeParent) parent).getChildren();
}
return new Object[0];
}
public boolean hasChildren(Object parent) {
if (parent instanceof TreeParent)
return ((TreeParent) parent).hasChildren();
return false;
}
private void initialize() {
// Snipped... see earlier Listing.
}
}
class ViewLabelProvider extends LabelProvider {
public String getText(Object obj) {
return obj.toString();
}
public Image getImage(Object obj) {
String imageKey = ISharedImages.IMG_OBJ_ELEMENT;
if (obj instanceof TreeParent)
imageKey = ISharedImages.IMG_OBJ_FOLDER;
return PlatformUI.getWorkbench().getSharedImages().getImage(
imageKey);
}
}
class NameSorter extends ViewerSorter {
}
/**
* The constructor.
*/
public SnippetsView() {
}
/**
* This is a callback that will allow us to create the viewer and initialize
* it.
*/
public void createPartControl(Composite parent) {
viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
drillDownAdapter = new DrillDownAdapter(viewer);
viewer.setContentProvider(new ViewContentProvider());
viewer.setLabelProvider(new ViewLabelProvider());
viewer.setSorter(new NameSorter());
viewer.setInput(getViewSite());
// add myself as a global selection listener
getSite().getPage().addSelectionListener(this);
// prime the selection
selectionChanged(null, getSite().getPage().getSelection());
makeActions();
hookContextMenu();
hookDoubleClickAction();
contributeToActionBars();
}
private void hookContextMenu() {
MenuManager menuMgr = new MenuManager("#PopupMenu");
menuMgr.setRemoveAllWhenShown(true);
menuMgr.addMenuListener(new IMenuListener() {
public void menuAboutToShow(IMenuManager manager) {
SnippetsView.this.fillContextMenu(manager);
}
});
Menu menu = menuMgr.createContextMenu(viewer.getControl());
viewer.getControl().setMenu(menu);
getSite().registerContextMenu(menuMgr, viewer);
}
private void contributeToActionBars() {
IActionBars bars = getViewSite().getActionBars();
fillLocalPullDown(bars.getMenuManager());
fillLocalToolBar(bars.getToolBarManager());
}
private void fillLocalPullDown(IMenuManager manager) {
manager.add(insertAction);
manager.add(new Separator());
}
private void fillContextMenu(IMenuManager manager) {
manager.add(insertAction);
manager.add(new Separator());
drillDownAdapter.addNavigationActions(manager);
// Other plug-ins can contribute there actions here
manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
}
private void fillLocalToolBar(IToolBarManager manager) {
manager.add(insertAction);
manager.add(new Separator());
drillDownAdapter.addNavigationActions(manager);
}
private void makeActions() {
insertAction = new InsertSnippetAction();
insertAction.setText("Insert Snippet");
insertAction.setToolTipText("Inserts the selected snippet");
insertAction.setImageDescriptor(PlatformUI.getWorkbench()
.getSharedImages().getImageDescriptor(
ISharedImages.IMG_OBJS_INFO_TSK));
}
private void hookDoubleClickAction() {
viewer.addDoubleClickListener(new IDoubleClickListener() {
public void doubleClick(DoubleClickEvent event) {
insertAction.run();
}
});
}
/**
* Passing the focus request to the viewer's control.
*/
public void setFocus() {
viewer.getControl().setFocus();
}
public void selectionChanged(IWorkbenchPart part, ISelection selection) {
}
}
|
플러그인을 테스트 할 때, Eclipse의 트리 아이템을 더블 클릭하면 "Running insert action on XXX"라는 메시지 박스가 디스플레이 된다. 여기에서 XXX는 Example.com Snippets 뷰에 선택된 아이템의 텍스트이다. 마우스 오른쪽을 클릭하여 콘텍스트 메뉴에서 Action 1을 선택하거나 뷰의 메뉴 바에서 버튼을 클릭하면 같은 액션이 실행된다.
InsertAction 끝내기
더블 클릭과 팝업 메뉴 아이템이 작동하면 나머지 코드를 추가함으로써 InsertSnippetAction 클래스를 끝낼 수 있다. 새롭고 향상된 run() 메소드가 아래 나타나 있다.
Listing 21. 완성된 InsertSnippetAction run()
class InsertSnippetAction extends Action {
@Override
public void run() {
IEditorPart target = getViewSite().getWorkbenchWindow()
.getActivePage().getActiveEditor();
if (target != null) {
ITextEditor textEditor = null;
if (target instanceof ITextEditor) {
textEditor = (ITextEditor) target;
ISelectionProvider sp = textEditor.getSelectionProvider();
ITextSelection sel = (ITextSelection) sp.getSelection();
IDocument doc = textEditor.getDocumentProvider()
.getDocument(textEditor.getEditorInput());
try {
String text = getMergedSnippetContent();
doc.replace(sel.getOffset(), sel.getLength(), text);
} catch (BadLocationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
|
run() 메소드는 getMergedSnippetContent()를 호출하는데, 이것은 아래 나타나 있다. 이 메소드 나머지는 현재 에디터의 인스턴스를 가져오는데 사용되는 코드이고, 선택된 에디터에서 텍스트를 가져오고, 선택된 텍스트를 getMergedSnippetContent()에 의해 리턴된 값으로 대체한다.
Listing 22. getMergedSnippetContent() 메소드
public String getMergedSnippetContent()
{
String result = "";
ISelection selection = viewer.getSelection();
Object obj = ((IStructuredSelection) selection).getFirstElement();
if (obj instanceof TreeObject) {
SnippetInfo info = ((TreeObject) obj).getInfo();
Snippet s = snippetProvider.getSnippet(info);
SnippetVariablesWizardPage page = new SnippetVariablesWizardPage(
"", info, s);
page.setTitle("Snippet Variables");
page.setDescription("Input the values to replace the template "
+ "variables found in this snippet.");
// create wizard
SnippetVariablesWizard wizard = new SnippetVariablesWizard(page);
// create wizard dialog & launch wizard dialog
WizardDialog dialog = new WizardDialog(viewer.getControl()
.getShell(), wizard);
dialog.open();
result = s.getMergedContent();
}
return result;
} |
이 메소드는 마법사와 마법사 페이지를 사용하여 스니펫 변수를 위해 사용자에게서 인풋을 모은다. 이 코드를 지금 바로 컴파일 한다면, 에러가 생긴다. SnippetVariablesWizardPage와 SnippetVariablesWizard는 아직 생성되지 않았기 때문이다. 다음 섹션에서 구현할 것이다.
사용자 인풋 추가하기
코드 스니펫 템플릿 변수용 사용자 인풋을 모으는 메커니즘을 구현하는 일은 놀라울 정도로 단순하다. 몇 가지 중요한 부분이 있는데, 그 중요한 부분은 Standard Widget Toolkit (SWT) 엘리먼트와 헬퍼의 형태로 되어 있고, 템플릿이 상세를 관여하지 않기 때문에 여러분은 지금까지 이 문제를 고민한 적이 없을 것이다. 하지만, 정확한 순서로 구현할 경우, 조각들이 올바르게 맞춰진다. 이 섹션에서는 스니펫 변수용 사용자 인풋을 모으기 위해 이 위에 테이블로 마법사 페이지를 구현하는 방법을 설명한다.
SnippetVariablesWizardPage
SnippetVariablesWizardPage는 WizardPage 클래스를 확장하는 클래스이다. WizardPage 클래스는 Eclipse Platform API의 일부이고 마법사 페이지의 기본 구현을 제공한다. Eclipse Platform API 문서는 참고자료 섹션을 참조하라.
WizardNewFileCreationPage 같은 WizardPage의 특별한 형태가 있는데, 이것은 새로운 파일을 만들기 위해 인풋을 모으는데 사용된다. 하지만, 이 프로젝트에서, 깨끗한 슬레이트에서 시작하기 때문에, WizardPage는 잘 작동한다. SnippetVariablesWizardPage의 전체 리스팅은 아래 나타나 있다. 약간 복잡하게 보이게 하는 두 개의 내부 클래스들이 있지만, 거의 대부분의 코드가 사용자용 프로퍼티를 가져오는 테이블을 지원한다. 이 테이블은 편집이 가능하고, 이를 구현하는데 사용되는 코드를 설명하고 있다. 이 테이블이 읽기 전용 데이터였다면, 여러분은 Table 객체를 통해 빠르게 반복하고 행을 추가할 수 있었을 것이다. SnippetsView 클래스를 보면, 마법사 페이지에 있는 것과 같은 인터페이스를 구현하는 많은 내부 클래스를 보게 될 것이다.
Listing 23. SnippetVariablesWizardPage 클래스
public class SnippetVariablesWizardPage extends WizardPage {
// Use the Table widget
// http://www.eclipse.org/swt/widgets/
// http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv
/reference/api/org/eclipse/swt/widgets/Table.html
private Table propertyTable;
private TableViewer tableViewer;
private SnippetInfo snippetInfo;
private Snippet snippet;
private static final String[] columnNames = new String[]{"property", "value"};
private SnippetVariableValue[] variables;
class SnippetVariableValue
{
// Snipped... see Listing 24
}
class SnippetVariableContentProvider implements IStructuredContentProvider {
// Snipped... see Listing 25
}
class SnippetVariableLabelProvider extends LabelProvider implements
ITableLabelProvider {
// Snipped... see Listing 26
}
class SnippetVariableCellModifier implements ICellModifier {
// Snipped... see Listing 27
}
private SnippetVariableValue[] initializeData(SnippetInfo info)
{
SnippetVariableValue[] result = new SnippetVariableValue[info.getVariables().length];
for (int i = 0; i < info.getVariables().length; i++) {
result[i] = new SnippetVariablesWizardPage.SnippetVariableValue(
info.getVariables()[i]);
}
return result;
}
public SnippetVariablesWizardPage(String pageName,
SnippetInfo info, Snippet snippet) {
super(pageName);
this.snippetInfo = info;
this.snippet = snippet;
this.variables = initializeData(this.snippetInfo);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.dialogs.IDialogPage#createControl(
* org.eclipse.swt.widgets.Composite)
*/
public void createControl(Composite parent) {
// create main composite & set layout
Composite container = new Composite(parent, SWT.NONE);
container.setLayout(new FillLayout());
container.setLayoutData(new GridData(GridData.FILL_BOTH));
// titleText = new Text(mainComposite, SWT.BORDER | SWT.SINGLE);
propertyTable = new Table(container, SWT.BORDER | SWT.SINGLE);
propertyTable.setHeaderVisible(true);
propertyTable.setLinesVisible(true);
int colWidth = 160;
TableColumn column = new TableColumn(propertyTable, SWT.LEFT, 0);
column.setText("Property Name");
column.setWidth(colWidth);
// Second column
column = new TableColumn(propertyTable, SWT.LEFT, 1);
column.setText("Value");
column.setWidth(colWidth);
// Iterate through the variables in the snippet info and add them to the
// table.
tableViewer = new TableViewer(propertyTable);
tableViewer.setUseHashlookup(true);
tableViewer.setColumnProperties(columnNames);
CellEditor[] editors = new CellEditor[columnNames.length];
TextCellEditor textEditor = new TextCellEditor(propertyTable);
((Text) textEditor.getControl()).setTextLimit(60);
editors[0] = textEditor;
textEditor = new TextCellEditor(propertyTable);
((Text) textEditor.getControl()).setTextLimit(60);
editors[1] = textEditor;
tableViewer.setCellEditors(editors);
tableViewer.setCellModifier(new SnippetVariableCellModifier());
tableViewer.setContentProvider(new SnippetVariableContentProvider());
tableViewer.setLabelProvider(new SnippetVariableLabelProvider());
tableViewer.setInput(this.variables);
// page setting
setControl(container);
setPageComplete(true);
}
public void updateSnippet()
{
/* Get the values from the table */
HashMap map = createMap(variables);
snippet.mergeContent(map);
}
@SuppressWarnings("unchecked")
private HashMap createMap(SnippetVariableValue[] variables)
{
HashMap map = new HashMap();
for (int i = 0; i < variables.length; i++)
{
map.put(variables[i].property, variables[i].value);
}
return map;
}
}
|
createControl() 메소드 내에서, Table과 함께 TableViewer 객체가 할당되고 설정된다. 테이블 그 자체는 사용자에게 나타나는 데이터를 보유할 수 있지만, TableViewer는 다른 유형의 에디터를 테이블 칼럼에 쉽게 추가할 수 있는 기능을 제공한다. TableViewer 클래스는 setInput() 메소드를 사용함으로써 테이블에 데이터를 추가할 수 있다. 유일한 단점은 여러분이 콘텐트 공급자(SnippetVariableContentProvider)를 먼저 추가해야 한다는 점이다. 장점은 이 메소드를 사용하여 컬렉션을 반복하고 값을 할당하는 것 없이 테이블을 채울 수 있다는 것이다.
SnippetVariableValue
이것은 단순한 내부 클래스로서 테이블로 가져오고 사용자에 의해 업데이트 된 데이터를 보유하고 있다. 두 개의 퍼블릭 필드를 갖고 있다. 하나는 프로퍼티 이름을 갖고 있고, 하나는 사용자가 입력한 값을 보유하고 있다. 이러한 어레이들은 마법사의 테이블에 그려진다. SnippetVariableValue 객체는 스니펫에서 발견된 모든 변수를 위해 존재한다.
Listing 24. SnippetVariableValue 클래스
class SnippetVariableValue
{
public String property;
public String value;
public SnippetVariableValue(String property)
{
this.property = property;
this.value = "";
}
}
|
SnippetVariableContentProvider
콘텐트 공급자는 IStructuredContentProvider 인터페이스를 구현하는 내부 클래스이다. 전체적인 내부 클래스는 아래와 같다.
Listing 25. SnippetVariableContentProvider 클래스
class SnippetVariableContentProvider implements IStructuredContentProvider {
public void dispose() {
}
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
}
public Object[] getElements(Object parent) {
return variables;
}
}
|
이 글에서는, 구현 코드를 포함하는 유일한 메소드가 getElements()이다. 이것은 마법사 페이지의 생성자에서 초기화 된 변수를 리턴한다.
SnippetVariableLabelProvider
레이블 공급자는 ITableLabelProvider 인터페이스를 구현하고 LabelProvider 클래스를 확장한다. 이 두 개 모두 Eclipse Platform API의 부분들이다. 레이블 공급자용 코드는 아래와 같다.
Listing 26. SnippetVariableLabelProvider 클래스
class SnippetVariableLabelProvider extends LabelProvider implements
ITableLabelProvider {
public Image getColumnImage(Object element, int columnIndex) {
return null;
}
public String getColumnText(Object element, int columnIndex) {
SnippetVariableValue variable = (SnippetVariableValue) element;
if (columnIndex == 0) {
return variable.property;
} else if (columnIndex == 1) {
return (variable.value != null) ? variable.value : "Type value";
} else {
return "";
}
}
}
|
이 테이블에는 이미지가 없고 텍스트만 있기 때문에, getColumnImage() 메소드는 어떤 것도 리턴 할 필요가 없다. 하지만, getColumnText() 메소드는 칼럼 인덱스에 정확한 텍스트를 리턴하도록 수정되어야 한다. 정확한 값을 가져오는데 필요한 로직을 하드 코딩 하는 것 보다 더 나은 솔루션이 있지만, 이 글에서는 다루지 않겠다.
SnippetVariableCellModifier
SnippetVariableCellModifier는 Eclipse Platform API에서 ICellModifier 인터페이스를 구현한다. 이것은 셀이 수정될 수 있는지 여부를 결정하고 값을 가져와서 수정할 수 있는 메소드를 제공한다. 클래스는 아래와 같다.
Listing 27. SnippetVariableCellModifier 클래스
class SnippetVariableCellModifier implements ICellModifier {
public boolean canModify(Object element, String property) {
return (!property.equals("property"));
}
public Object getValue(Object element, String property) {
Object result;
SnippetVariableValue variable = (SnippetVariableValue) element;
if (property.equals("property")) {
result = variable.property;
} else if (property.equals("value")) {
result = variable.value;
} else {
result = "";
}
return result;
}
public void modify(Object element, String property, Object value) {
TableItem item = (TableItem) element;
SnippetVariableValue variable = (SnippetVariableValue) item
.getData();
if (property.equals("value")) {
variable.value = (value != null) ? value.toString() : "";
}
tableViewer.update(variable, null);
}
} |
 |
변경 사항을 보고 싶지 않을 경우?
modify() 메소드의 한 단계는 매우 중요하다. 끝 부분에서 테이블 뷰어에 update()를 호출하는 것이다. 이것을 호출하면 테이블을 변경할 수 있지만 디스플레이 되지 않는다.
|
|
스니펫 템플릿에서 사용자가 변수의 이름을 수정하고 싶지 않을 경우 쉽게 구현할 수 있다. 코드를 canModify() 메소드에 추가하여, 칼럼의 이름이 property라면 false를 리턴하도록 하는 것이다. 편집 가능한 테이블을 구현하고 있고 사용자가 칼럼을 편집하는 것이 괜찮다면, 이 메소드를 수정하여 언제라도 true를 리턴하도록 수정할 수 있다.
SnippetVariablesWizardPage와 비교해 볼 때, SnippetVariablesWizard는 경량의 클래스이다. 전체 클래스는 아래 나타나 있다. 이것은 Eclipse Platform API에 의해 제공된 클래스인 Wizard를 확장한다.
Listing 28. SnippetVariablesWizard 클래스
public class SnippetVariablesWizard extends Wizard {
public SnippetVariablesWizard(SnippetVariablesWizardPage page)
{
addPage(page);
}
@Override
public boolean performFinish() {
SnippetVariablesWizardPage page =
(SnippetVariablesWizardPage)getPages()[0];
page.updateSnippet();
return true;
}
} |
이 생성자는 addPage() 메소드를 사용하여 마법사에 추가한다. 이것은 베이스 Wizard 클래스에서 관리를 받기 때문에 이것이 구현되는 방법은 걱정할 필요가 없다. performFinish() 메소드는 사용자가 마법사에서 Finish 버튼을 클릭하면 실행된다. 마법사가 닫히면 true를 리턴해야 한다. False를 리턴하면 마법사는 사용자에게 디스플레이 된 상태로 남아있게 된다. 장기 실행 프로세스가 가능한 대부분의 경우, IRunnableWithProgress 인터페이스를 구현하는 프로세스를 초기화 하는 것도 좋은 생각이다.
마법사를 실행하면 아래와 같은 모습이 된다.
그림 7. 마법사
마법사가 종료하면, 마법사 페이지로 전달된 스니펫은 업데이트 되어 스니펫 텍스트의 합병된 결과와 사용자가 제공한 값을 포함하게 된다. 이제, InsertSnippetAction을 실행하면, 마법사가 나타나고 왼쪽 칼럼에 있는 스니펫에 변수를 포함하게 된다. 오른쪽 칼럼에 값을 추가하고 Finish를 클릭한 후에, 병합된 결과가 액티브 텍스트 에디터에 배치된다.
새로운 이벤트에 반응하기
마법사 페이지와 클래스를 추가한 후에, 공급자를 사용하여 스니펫을 로딩하고 스니펫에 들어간 값들을 커스터마이징 해야 한다. 더 나은 플러그인을 만들기 위해서, 팝업 메뉴와 더블 클릭 이벤트에 더하여 드래그&드롭 기능을 추가할 수 있다. Eclipse Platform API에는 Example.com Snippet 뷰와 에디터 사이에 드래그&드롭을 구현하는데 사용할 수 있는 클래스가 포함된다. 트리 객체를 선택하고 이를 에디터로 가져오면, 마법사가 나타나면서 값을 보여준다. 그리고 나서, 병합된 결과가 에디터에 놓이게 된다.
SnippetDragDropListener
새로운 뷰에 드래그&드롭 지원을 추가하려면, 또 다른 내부 클래스를 추가한다. 이번에는, TransferDragSourceListener 인터페이스를 구현한다. 이 인터페이스는 세 개의 메소드를 갖고 있다. 이중 두 개를 수정하여 드래그&드롭을 구현한다. 아래를 참조하라.
Listing 29. SnippetDragDropListener 클래스
class SnippetDragDropListener implements TransferDragSourceListener {
public Transfer getTransfer() {
return TextTransfer.getInstance();
}
public void dragFinished(DragSourceEvent event) {
// There is nothing to clean up or do.
}
public void dragSetData(DragSourceEvent event) {
ISelection selection = viewer.getSelection();
Object obj = ((IStructuredSelection) selection).getFirstElement();
if (obj instanceof TreeObject) {
event.data = getMergedSnippetContent();
}
}
public void dragStart(DragSourceEvent event) {
// Always enabled, so don't do anything on the start.
}
}
|
getTransfer() 메소드는 TextTransfer의 새로운 인스턴스를 리턴한다. 이것은 Eclipse가 트랜스퍼를 핸들하는 방법을 알도록 한다.
dragSetData() 메소드는 InsertSnippetAction 클래스에 의해 사용되는 같은 프라이빗 메소드를 호출한다. ("InsertSnippetAction 클래스 작성하기" 참조) 하지만, 코드를 현재 에디터로 삽입할 필요 없이, 메소드는 event.data 값을 설정한다. IDE는 두 부분들 간 데이터 전송을 어떻게 다루는지를 알기 때문에 나머지를 책임진다.
연결하기
드래그&드롭을 사용하기 전에, 메소드를 호출하여 드래그&드롭 지원을 Example.com Snippets 뷰에 있는 TreeViewer 컴포넌트에 추가해야 한다. 이를 설정하는 코드는 아래와 같다.
Listing 30. 드래그&드롭 지원 추가하기
public void createPartControl(Composite parent) {
viewer = new TreeViewer(parent, SWT.MULTI |
SWT.H_SCROLL | SWT.V_SCROLL);
drillDownAdapter = new DrillDownAdapter(viewer);
viewer.setContentProvider(new ViewContentProvider());
viewer.setLabelProvider(new ViewLabelProvider());
viewer.setSorter(new NameSorter());
viewer.setInput(getViewSite());
// add myself as a global selection listener
getSite().getPage().addSelectionListener(this);
// prime the selection
selectionChanged(null, getSite().getPage().getSelection());
makeActions();
hookContextMenu();
hookDoubleClickAction();
contributeToActionBars();
DelegatingDragAdapter dragAdapter = new DelegatingDragAdapter();
SnippetDragDropListener dragDropListener =
new SnippetDragDropListener();
dragAdapter.addDragSourceListener(
(TransferDragSourceListener) dragDropListener);
viewer.addDragSupport(DND.DROP_COPY | DND.DROP_MOVE,
dragAdapter.getTransfers(), dragAdapter);
} |
새로운 프레퍼런스 추가하기
플러그인이 뷰에 코드 스니펫을 디스플레이 하고 스니펫을 다양한 액션을 통해 에디터로 삽입할 수 있다. 마지막 단계는 프레퍼런스 페이지를 업데이트 하여 사용자가 베이스 디렉토리와 SnippetProvider 구현 클래스 이름을 설정할 수 있도록 하는 것이다.
SnippetsPreferencePage 수정하기
SnippetsPreferencePage와 PreferenceConstants 클래스를 열어서 수정한다. StringFieldEditor를 제외한 모든 기본 템플릿 필드를 제거할 수 있다. 원치 않는 필드를 제거한 후에, 나머지 에디터에 대한 레이블을 조정하고 스니펫 저장소 위치를 로딩하는 것을 추가할 수 있다.
프레퍼런스 페이지 createFieldEditors() 메소드의 마지막 코드는 아래와 같다.
Listing 31. SnippetsPreferencePage createFieldEditors() 메소드
public void createFieldEditors() {
addField(new StringFieldEditor(PreferenceConstants.P_CLASS,
"Snippet &Implementation Class:", getFieldEditorParent()));
addField(new StringFieldEditor(PreferenceConstants.P_SNIPPET_REPOS_LOC,
"&Snippet Repository Path:", getFieldEditorParent()));
}
|
PreferenceConstants 클래스의 상수 이름을 업데이트 하여 의미를 부여한다. 또한 PreferenceInitializer 클래스의 initializeDefaultPreferences() 메소드를 업데이트하여 프레퍼런스를 기본 값에 설정한다. 예를 들어, 기본 스니펫 공급자 구현 클래스 이름을 실제의 유효한 클래스로 설정하고, 저장소 위치를 유효한 저장소 경로로 설정한다.
요약
Eclipse Platform API를 통해 인터페이스와 클래스를 사용함으로써, Eclipse를 확장하여 고유의 풍부한 플러그인을 추가할 수 있다. 고유의 플러그인 구현은 Eclipse에 포함된 템플릿을 사용한다면 훨씬 쉬워진다. 인터페이스를 구현하거나 Action 클래스 같은 클래스를 확장함으로써 Eclipse에 커스텀 액션을 추가할 수 있다. 이러한 액션들을 사용하여, 팝업 메뉴, 툴바 버튼, 더블 클릭 이벤트로 연결된 커스텀 작동을 만들 수 있다. 새로운 커스텀 뷰를 Eclipse에 추가하고 여기에 고유의 정보를 채울 수 있다.
TransferDragSourceListener 를 확장하는 클래스는 커스텀 뷰들과 Eclipse IDE의 부분들간 드래그&드롭 작동을 추가하는데 사용된다. Wizard와 WizardPage 클래스를 확장함으로써, 한 개 이상의 마법사 페이지로 커스텀 마법사를 추가할 수 있다. 이러한 마법사에서, 사용자로부터 정보를 모으고 그 정보를 사용하여 커스텀 프로세스를 실행한다. 마지막으로, Eclipse에서 Eclipse의 중첩 인스턴스에서 플러그인들을 실행하여 이를 테스트 할 수 있다. 플러그인이 어떤 모습인지, 어떻게 작동하는지를 바로 볼 수 있다.
다운로드 하십시오 |