繼 Document 分析視圖後,本文指導開發 `StructuredModelView`,可視化 `IStructuredModel` 支撐的 `IDOMDocument` DOM 樹,支援與編輯器雙向同步,並通過對比語法樹與語義樹的差異,徹底釐清「為什麼高級功能需要語義模型」的核心認知。
| 能力 | Document 視圖 | Model 視圖 (本文) |
|---|---|---|
| 展示模型 | IStructuredDocument 節點樹 | IDOMDocument DOM 樹 (W3C DOM) |
| 節點類型 | ELEMENT, TEXT, COMMENT... (語法通用) | Element, Attr, Text, CDATASection, EntityReference... (DOM 標準) |
| 屬性信息 | 字符串鍵值對 | 類型化屬性 (ID, IDREF, CDATA, NMTOKEN...)、Schema 衍生類型 |
| 命名空間 | 不明確 | 完整命名空間 URI、前綴映射、默認命名空間 |
| 語義驗證 | 無 | 節點標記錯誤/警告 (若驗證器已運行) |
// 1. 獲取編輯器的 StructuredModel (句柄機制)
IEditorPart editor = ...;
if (editor instanceof StructuredTextEditor) {
IStructuredModel model = ((StructuredTextEditor) editor).getModel(); // 關鍵:獲取模型句柄並加載
if (model != null) {
// 2. 適配到 IDOMDocument
IDOMDocument domDoc = (IDOMDocument) model.getAdapter(IDOMDocument.class);
if (domDoc != null) {
// 3. 獲取 DOM 根元素
Element root = domDoc.getDocumentElement();
// 4. 構建樹視圖輸入
treeViewer.setInput(root);
}
}
}
// 模型可能處於 UNLOADED/LOADING 狀態,需監聽加載完成
if (model.getModelState() != IStructuredModel.LOADED) {
model.addModelListener(new ModelListener() {
public void modelChanged(ModelEvent event) {
if (event.getType() == ModelEvent.LOADED) {
// 異步在 UI 線程刷新視圖
Display.getDefault().asyncExec(() -> refreshViewer(model));
model.removeModelListener(this);
}
}
});
} else {
refreshViewer(model);
}
class DOMContentProvider implements ITreeContentProvider {
public Object[] getChildren(Object parent) {
if (parent instanceof Node) {
NodeList nl = ((Node) parent).getChildNodes();
Object[] arr = new Object[nl.getLength()];
for (int i = 0; i < nl.getLength(); i++) arr[i] = nl.item(i);
return arr;
}
return new Object[0];
}
// ... getParent, hasChildren, getElements 類似
}
class DOMLabelProvider extends LabelProvider {
public String getText(Object obj) {
if (obj instanceof Element) {
Element el = (Element) obj;
String ns = el.getNamespaceURI();
String prefix = el.getPrefix();
String local = el.getLocalName();
StringBuilder sb = new StringBuilder();
if (ns != null) sb.append("{").append(ns).append("}");
if (prefix != null) sb.append(prefix).append(":");
sb.append(local != null ? local : el.getTagName());
// 顯示 ID 屬性
if (el.hasAttribute("id")) sb.append(" #").append(el.getAttribute("id"));
// 顯示 Schema 類型 (若可用)
TypeInfo ti = el.getSchemaTypeInfo();
if (ti != null) sb.append(" [").append(ti.getTypeName()).append("]");
return sb.toString();
} else if (obj instanceof Attr) {
Attr attr = (Attr) obj;
return "@" + attr.getName() + "=\"" + attr.getValue() + "\"";
} else if (obj instanceof Text) {
String txt = ((Text) obj).getData().trim();
return txt.isEmpty() ? "#text (whitespace)" : "#text: " + txt.substring(0, Math.min(30, txt.length()));
}
return super.getText(obj);
}
}
editor.getSelectionProvider().addSelectionChangedListener(event -> {
ISelection sel = event.getSelection();
if (sel instanceof ITextSelection) {
int offset = ((ITextSelection) sel).getOffset();
// 關鍵:通過 Document 找語法節點,再映射到 Model 節點
IStructuredDocument sdoc = model.getDocument();
IStructuredNode syntaxNode = sdoc.findNodeAtOffset(offset);
if (syntaxNode != null) {
// 語法節點 → DOM 節點映射 (WTP 內部維護雙向映射表)
Node domNode = syntaxNode.getAdapter(Node.class); // 關鍵適配
if (domNode != null) {
treeViewer.setSelection(new StructuredSelection(domNode), true);
}
}
}
});
treeViewer.addDoubleClickListener(event -> {
IStructuredSelection sel = (IStructuredSelection) event.getSelection();
Node domNode = (Node) sel.getFirstElement();
if (domNode != null && editor instanceof StructuredTextEditor) {
// DOM 節點 → 語法節點 → 偏移量
IStructuredNode syntaxNode = (IStructuredNode) domNode.getAdapter(IStructuredNode.class);
if (syntaxNode != null) {
((StructuredTextEditor) editor).selectAndReveal(
syntaxNode.getStartOffset(), syntaxNode.getEndOffset() - syntaxNode.getStartOffset());
}
}
});
打開同一 JSP 文件,並排開啟 Document 視圖與 Model 視圖,觀察:
| 場景 | Document 視圖 (語法) | Model 視圖 (語義) | 差異本質 |
|---|---|---|---|
<jsp:include page="x.jsp"/> | 單個 ELEMENT 節點,類型 "jsp:include" | Element 節點,命名空間 "https://java.sun.com/JSP/Page",局部名 "include",Schema 類型標註 | 語義模型理解標籤**身份與契約** |
<%@ page import="java.util.*" %> | JSP_DIRECTIVE 區域節點,屬性字符串 | Directive 節點,屬性 import 類型化為 NMTOKENS,可驗證合法性 | 語義模型理解**屬性語義與約束** |
<div id="main">text</div> | ELEMENT(div) 含 TEXT 子節點 | Element(div) 有 ID 屬性 (ID 類型),文本節點規範化 | 語義模型建立**ID 索引**,支援 getElementById |
錯誤嵌套 <div><span></div></span> | 按解析順序構建樹,不報錯 | 驗證器運行後,節點標記錯誤標註 | 語義模型支援**驗證與錯誤標記** |
Java 代碼 <% int x=1; %> | JSP_SCRIPTLET 區域,不解析內部 | CDATASection 或特定 Scriptlet 節點,**無 Java AST** | Java 語義由 **JDT** 獨立管理,WTP 不處理 |
六篇文章建立了完整認知棧:
下一階段:功能定制實戰
掌握模型,才能馭功能。兩大分析視圖將伴隨你完成所有定制開發,成為理解 WTP 內部運行機制的核心儀表盤。