2014-12-06 32 views
1

我正在用Java語法突出顯示編寫我自己的文本編輯器,目前它每次用戶輸入單個字符時都會簡單地解析並突出顯示當前行。雖然大概不是最有效的方式,但它足夠好並且不會引起任何明顯的性能問題。在僞Java中,這將是我的代碼的核心概念:如何在現場語法熒光筆中處理多行註釋?

public void textUpdated(String wholeText, int updateOffset, int updateLength) { 
    int lineStart = getFirstLineStart(wholeText, updateOffset); 
    int lineEnd = getLastLineEnd(wholeText, updateOffset + updateLength); 

    List<Token> foundTokens = tokenizeText(wholeText, lineStart, lineEnd); 

    for(Token token : foundTokens) { 
     highlightText(token.offset, token.length, token.tokenType); 
    } 
} 

真正的問題在於多行註釋。要檢查輸入的字符是否在多行註釋中,程序將需要解析回最近一次出現的「/ *」,同時也要知道這個事件是在文字還是其他註釋中。如果文本數量很少,這不會成爲問題,但如果文本包含20,000行代碼,則可能需要在每次按鍵時掃描並(重新)突出顯示20,000行代碼,這將非常低效。

所以我最終的問題是:我該如何處理多行令牌/註釋的語法高亮,同時保持它有效?

回答

2

一種常用的方法是在每行開始時保存詞法分析器狀態。 (通常,詞法分析器狀態將是一個小整數或枚舉;對於類Java語言,它可能會限制爲三個值:普通,多行註釋內部和多行內部常量內部)。

更改爲a行可能會改變下一行開始時的詞法分析狀態,但它不能改變當前行開始時的狀態,因此可以從行的開頭進行行的retocuisation,使用當前行的詞法狀態作爲起始條件。保持每行詞法分析器狀態可以很容易地處理光標移動到另一行的情況,可能相當遠一些。

如果編輯改變在該行(這是說下一行的開始),你可以重新掃描文件的其餘部分的結束詞法分析器狀態。但是,立即這樣做對於用戶來說是非常煩人的,因爲這意味着每次鍵入引號時,整個scrern都會被重新繪製,因爲它已經成爲多行字符串的一部分(例如)。由於大部分時間,用戶將關閉字符串(或註釋),通常延遲重新掃描會更好。例如,您可能要等到用戶移動光標或完成詞彙元素或其他此類信號。另一個comon方法是在光標處插入一個「鬼魂」關閉符號,這將保持lex同步。如果用戶明確鍵入,或者明確刪除,則該ghost將被刪除。

您似乎將整個程序保存爲單個字符串。恕我直言,最好將其保存爲行列表,以避免在插入或刪除字符時不得不復制整個字符串。否則,編輯非常長的文件變得非常煩人。

最後,您不應該標記不可見的文本。避免這會限制大規模回覆的損害。

2

大約10年前(或更多),我試圖做到這一點(爲了好玩)。由於代碼太舊,我不記得代碼中的所有細節和代碼中的邏輯條件。這裏的所有代碼基本上都是一個強力解決方案。它決不會試圖保持rici所建議的每條線的狀態。

我會盡力解釋代碼的高級概念。希望其中的一些對你有意義。

目前它每次用戶輸入單個字符時都會簡單地解析並突出顯示當前行。

這也是我的代碼的基本前提。但是,它也處理粘貼多行代碼。

如何在保持效率的同時在語法高亮顯示中處理多行標記/註釋?

在我的解決方案,當你進入"/*"啓動多行註釋,我會評論的代碼下面所有的行,直到我找到註釋的結尾或另一個多行註釋的開始或文檔的結尾。當您輸入匹配的"*/"以結束多行註釋時,我將重新突出顯示以下行,直到下一個多行註釋或文檔結束。

因此,突出顯示的數量取決於多行註釋之間的代碼量。

這是它如何工作的快速概述。我懷疑這是100%準確的,因爲我只玩過一點點。應該指出,這段代碼是在我剛剛學習Java時編寫的,所以我絕不會認爲這是最好的方法,只是我當時知道的最好的方法。

這裏是您娛樂:)

只要運行該代碼,並點擊按鈕即可開始的代碼。

import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 
import java.net.*; 
import java.util.*; 
import javax.swing.*; 
import javax.swing.event.*; 
import javax.swing.text.*; 

class SyntaxDocument extends DefaultStyledDocument 
{ 
    private DefaultStyledDocument doc; 
    private Element rootElement; 

    private boolean multiLineComment; 
    private MutableAttributeSet normal; 
    private MutableAttributeSet keyword; 
    private MutableAttributeSet comment; 
    private MutableAttributeSet quote; 

    private Set<String> keywords; 

    private int lastLineProcessed = -1; 

    public SyntaxDocument() 
    { 
     doc = this; 
     rootElement = doc.getDefaultRootElement(); 
     putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n"); 

     normal = new SimpleAttributeSet(); 
     StyleConstants.setForeground(normal, Color.black); 

     comment = new SimpleAttributeSet(); 
     StyleConstants.setForeground(comment, Color.gray); 
     StyleConstants.setItalic(comment, true); 

     keyword = new SimpleAttributeSet(); 
     StyleConstants.setForeground(keyword, Color.blue); 

     quote = new SimpleAttributeSet(); 
     StyleConstants.setForeground(quote, Color.red); 

     keywords = new HashSet<String>(); 
     keywords.add("abstract"); 
     keywords.add("boolean"); 
     keywords.add("break"); 
     keywords.add("byte"); 
     keywords.add("byvalue"); 
     keywords.add("case"); 
     keywords.add("cast"); 
     keywords.add("catch"); 
     keywords.add("char"); 
     keywords.add("class"); 
     keywords.add("const"); 
     keywords.add("continue"); 
     keywords.add("default"); 
     keywords.add("do"); 
     keywords.add("double"); 
     keywords.add("else"); 
     keywords.add("extends"); 
     keywords.add("false"); 
     keywords.add("final"); 
     keywords.add("finally"); 
     keywords.add("float"); 
     keywords.add("for"); 
     keywords.add("future"); 
     keywords.add("generic"); 
     keywords.add("goto"); 
     keywords.add("if"); 
     keywords.add("implements"); 
     keywords.add("import"); 
     keywords.add("inner"); 
     keywords.add("instanceof"); 
     keywords.add("int"); 
     keywords.add("interface"); 
     keywords.add("long"); 
     keywords.add("native"); 
     keywords.add("new"); 
     keywords.add("null"); 
     keywords.add("operator"); 
     keywords.add("outer"); 
     keywords.add("package"); 
     keywords.add("private"); 
     keywords.add("protected"); 
     keywords.add("public"); 
     keywords.add("rest"); 
     keywords.add("return"); 
     keywords.add("short"); 
     keywords.add("static"); 
     keywords.add("super"); 
     keywords.add("switch"); 
     keywords.add("synchronized"); 
     keywords.add("this"); 
     keywords.add("throw"); 
     keywords.add("throws"); 
     keywords.add("transient"); 
     keywords.add("true"); 
     keywords.add("try"); 
     keywords.add("var"); 
     keywords.add("void"); 
     keywords.add("volatile"); 
     keywords.add("while"); 
    } 

    /* 
    * Override to apply syntax highlighting after the document has been updated 
    */ 
    public void insertString(int offset, String str, AttributeSet a) throws BadLocationException 
    { 
     if (str.equals("{")) 
      str = addMatchingBrace(offset); 

     super.insertString(offset, str, a); 
     processChangedLines(offset, str.length()); 
    } 

    /* 
    * Override to apply syntax highlighting after the document has been updated 
    */ 
    public void remove(int offset, int length) throws BadLocationException 
    { 
     super.remove(offset, length); 
     processChangedLines(offset, 0); 
    } 

    /* 
    * Determine how many lines have been changed, 
    * then apply highlighting to each line 
    */ 
    public void processChangedLines(int offset, int length) 
     throws BadLocationException 
    { 
     String content = doc.getText(0, doc.getLength()); 

     // The lines affected by the latest document update 

     int startLine = rootElement.getElementIndex(offset); 
     int endLine = rootElement.getElementIndex(offset + length); 

     if (startLine > endLine) 
      startLine = endLine; 

     // Make sure all comment lines prior to the start line are commented 
     // and determine if the start line is still in a multi line comment 

     if (startLine != lastLineProcessed 
     && startLine != lastLineProcessed + 1) 
     { 
      setMultiLineComment(commentLinesBefore(content, startLine)); 
     } 

     // Do the actual highlighting 

     for (int i = startLine; i <= endLine; i++) 
     { 
      applyHighlighting(content, i); 
     } 

     // Resolve highlighting to the next end multi line delimiter 

     if (isMultiLineComment()) 
      commentLinesAfter(content, endLine); 
     else 
      highlightLinesAfter(content, endLine); 

    } 

    /* 
    * Highlight lines when a multi line comment is still 'open' 
    * (ie. matching end delimiter has not yet been encountered) 
    */ 
    private boolean commentLinesBefore(String content, int line) 
    { 
     int offset = rootElement.getElement(line).getStartOffset(); 

     // Start of comment not found, nothing to do 

     int startDelimiter = lastIndexOf(content, getStartDelimiter(), offset - 2); 

     if (startDelimiter < 0) 
      return false; 

     // Matching start/end of comment found, nothing to do 

     int endDelimiter = indexOf(content, getEndDelimiter(), startDelimiter); 

     if (endDelimiter < offset & endDelimiter != -1) 
      return false; 

     // End of comment not found, highlight the lines 

     doc.setCharacterAttributes(startDelimiter, offset - startDelimiter + 1, comment, false); 
     return true; 
    } 

    /* 
    * Highlight comment lines to matching end delimiter 
    */ 
    private void commentLinesAfter(String content, int line) 
    { 
     int offset = rootElement.getElement(line).getStartOffset(); 

     // End of comment and Start of comment not found 
     // highlight until the end of the Document 

     int endDelimiter = indexOf(content, getEndDelimiter(), offset); 

     if (endDelimiter < 0) 
     { 
      endDelimiter = indexOf(content, getStartDelimiter(), offset + 2); 

      if (endDelimiter < 0) 
      { 
       doc.setCharacterAttributes(offset, content.length() - offset + 1, comment, false); 
       return; 
      } 
     } 

     // Matching start/end of comment found, comment the lines 

     int startDelimiter = lastIndexOf(content, getStartDelimiter(), endDelimiter); 

     if (startDelimiter < 0 || startDelimiter >= offset) 
     { 
      doc.setCharacterAttributes(offset, endDelimiter - offset + 1, comment, false); 
     } 
    } 

    /* 
    * Highlight lines to start or end delimiter 
    */ 
    private void highlightLinesAfter(String content, int line) 
     throws BadLocationException 
    { 
     int offset = rootElement.getElement(line).getEndOffset(); 

     // Start/End delimiter not found, nothing to do 

     int startDelimiter = indexOf(content, getStartDelimiter(), offset); 
     int endDelimiter = indexOf(content, getEndDelimiter(), offset); 

     if (startDelimiter < 0) 
      startDelimiter = content.length(); 

     if (endDelimiter < 0) 
      endDelimiter = content.length(); 

     int delimiter = Math.min(startDelimiter, endDelimiter); 

     if (delimiter < offset) 
      return; 

     // Start/End delimiter found, reapply highlighting 

     int endLine = rootElement.getElementIndex(delimiter); 

     for (int i = line + 1; i <= endLine; i++) 
     { 
      Element branch = rootElement.getElement(i); 
      Element leaf = doc.getCharacterElement(branch.getStartOffset()); 
      AttributeSet as = leaf.getAttributes(); 

      if (as.isEqual(comment)) 
      { 
       applyHighlighting(content, i); 
      } 
     } 
    } 

    /* 
    * Parse the line to determine the appropriate highlighting 
    */ 
    private void applyHighlighting(String content, int line) 
     throws BadLocationException 
    { 
     lastLineProcessed = line; 

     int startOffset = rootElement.getElement(line).getStartOffset(); 
     int endOffset = rootElement.getElement(line).getEndOffset() - 1; 

     int lineLength = endOffset - startOffset; 
     int contentLength = content.length(); 

     if (endOffset >= contentLength) 
      endOffset = contentLength - 1; 

     // check for multi line comments 
     // (always set the comment attribute for the entire line) 

     if (endingMultiLineComment(content, startOffset, endOffset) 
     || isMultiLineComment() 
     || startingMultiLineComment(content, startOffset, endOffset)) 
     { 
      doc.setCharacterAttributes(startOffset, endOffset - startOffset + 1, comment, false); 
      lastLineProcessed = -1; 
      return; 
     } 

     // set normal attributes for the line 

     doc.setCharacterAttributes(startOffset, lineLength, normal, true); 

     // check for single line comment 

     int index = content.indexOf(getSingleLineDelimiter(), startOffset); 

     if ((index > -1) && (index < endOffset)) 
     { 
      doc.setCharacterAttributes(index, endOffset - index + 1, comment, false); 
      endOffset = index - 1; 
     } 

     // check for tokens 

     checkForTokens(content, startOffset, endOffset); 
    } 

    /* 
    * Does this line contain the start delimiter 
    */ 
    private boolean startingMultiLineComment(String content, int startOffset, int endOffset) 
     throws BadLocationException 
    { 
     int index = indexOf(content, getStartDelimiter(), startOffset); 

     if ((index < 0) || (index > endOffset)) 
      return false; 
     else 
     { 
      setMultiLineComment(true); 
      return true; 
     } 
    } 

    /* 
    * Does this line contain the end delimiter 
    */ 
    private boolean endingMultiLineComment(String content, int startOffset, int endOffset) 
     throws BadLocationException 
    { 
     int index = indexOf(content, getEndDelimiter(), startOffset); 

     if ((index < 0) || (index > endOffset)) 
      return false; 
     else 
     { 
      setMultiLineComment(false); 
      return true; 
     } 
    } 

    /* 
    * We have found a start delimiter 
    * and are still searching for the end delimiter 
    */ 
    private boolean isMultiLineComment() 
    { 
     return multiLineComment; 
    } 

    private void setMultiLineComment(boolean value) 
    { 
     multiLineComment = value; 
    } 

    /* 
    * Parse the line for tokens to highlight 
    */ 
    private void checkForTokens(String content, int startOffset, int endOffset) 
    { 
     while (startOffset <= endOffset) 
     { 
      // skip the delimiters to find the start of a new token 

      while (isDelimiter(content.substring(startOffset, startOffset + 1))) 
      { 
       if (startOffset < endOffset) 
        startOffset++; 
       else 
        return; 
      } 

      // Extract and process the entire token 

      if (isQuoteDelimiter(content.substring(startOffset, startOffset + 1))) 
       startOffset = getQuoteToken(content, startOffset, endOffset); 
      else 
       startOffset = getOtherToken(content, startOffset, endOffset); 
     } 
    } 

    /* 
    * 
    */ 
    private int getQuoteToken(String content, int startOffset, int endOffset) 
    { 
     String quoteDelimiter = content.substring(startOffset, startOffset + 1); 
     String escapeString = getEscapeString(quoteDelimiter); 

     int index; 
     int endOfQuote = startOffset; 

     // skip over the escape quotes in this quote 

     index = content.indexOf(escapeString, endOfQuote + 1); 

     while ((index > -1) && (index < endOffset)) 
     { 
      endOfQuote = index + 1; 
      index = content.indexOf(escapeString, endOfQuote); 
     } 

     // now find the matching delimiter 

     index = content.indexOf(quoteDelimiter, endOfQuote + 1); 

     if ((index < 0) || (index > endOffset)) 
      endOfQuote = endOffset; 
     else 
      endOfQuote = index; 

     doc.setCharacterAttributes(startOffset, endOfQuote - startOffset + 1, quote, false); 

     return endOfQuote + 1; 
    } 

    /* 
    * 
    */ 
    private int getOtherToken(String content, int startOffset, int endOffset) 
    { 
     int endOfToken = startOffset + 1; 

     while (endOfToken <= endOffset) 
     { 
      if (isDelimiter(content.substring(endOfToken, endOfToken + 1))) 
       break; 

      endOfToken++; 
     } 

     String token = content.substring(startOffset, endOfToken); 

     if (isKeyword(token)) 
     { 
      doc.setCharacterAttributes(startOffset, endOfToken - startOffset, keyword, false); 
     } 

     return endOfToken + 1; 
    } 

    /* 
    * Assume the needle will be found at the start/end of the line 
    */ 
    private int indexOf(String content, String needle, int offset) 
    { 
     int index; 

     while ((index = content.indexOf(needle, offset)) != -1) 
     { 
      String text = getLine(content, index).trim(); 

      if (text.startsWith(needle) || text.endsWith(needle)) 
       break; 
      else 
       offset = index + 1; 
     } 

     return index; 
    } 

    /* 
    * Assume the needle will the found at the start/end of the line 
    */ 
    private int lastIndexOf(String content, String needle, int offset) 
    { 
     int index; 

     while ((index = content.lastIndexOf(needle, offset)) != -1) 
     { 
      String text = getLine(content, index).trim(); 

      if (text.startsWith(needle) || text.endsWith(needle)) 
       break; 
      else 
       offset = index - 1; 
     } 

     return index; 
    } 

    private String getLine(String content, int offset) 
    { 
     int line = rootElement.getElementIndex(offset); 
     Element lineElement = rootElement.getElement(line); 
     int start = lineElement.getStartOffset(); 
     int end = lineElement.getEndOffset(); 
     return content.substring(start, end - 1); 
    } 

    /* 
    * Override for other languages 
    */ 
    protected boolean isDelimiter(String character) 
    { 
     String operands = ";:{}()[]+-/%<=>!&|^~*"; 

     if (Character.isWhitespace(character.charAt(0)) 
     || operands.indexOf(character) != -1) 
      return true; 
     else 
      return false; 
    } 

    /* 
    * Override for other languages 
    */ 
    protected boolean isQuoteDelimiter(String character) 
    { 
     String quoteDelimiters = "\"'"; 

     if (quoteDelimiters.indexOf(character) < 0) 
      return false; 
     else 
      return true; 
    } 

    /* 
    * Override for other languages 
    */ 
    protected boolean isKeyword(String token) 
    { 
     return keywords.contains(token); 
    } 

    /* 
    * Override for other languages 
    */ 
    protected String getStartDelimiter() 
    { 
     return "/*"; 
    } 

    /* 
    * Override for other languages 
    */ 
    protected String getEndDelimiter() 
    { 
     return "*/"; 
    } 

    /* 
    * Override for other languages 
    */ 
    protected String getSingleLineDelimiter() 
    { 
     return "//"; 
    } 

    /* 
    * Override for other languages 
    */ 
    protected String getEscapeString(String quoteDelimiter) 
    { 
     return "\\" + quoteDelimiter; 
    } 

    /* 
    * 
    */ 
    protected String addMatchingBrace(int offset) throws BadLocationException 
    { 
     StringBuffer whiteSpace = new StringBuffer(); 
     int line = rootElement.getElementIndex(offset); 
     int i = rootElement.getElement(line).getStartOffset(); 

     while (true) 
     { 
      String temp = doc.getText(i, 1); 

      if (temp.equals(" ") || temp.equals("\t")) 
      { 
       whiteSpace.append(temp); 
       i++; 
      } 
      else 
       break; 
     } 

     return "{\n" + whiteSpace.toString() + "\t\n" + whiteSpace.toString() + "}"; 
    } 
/* 
    public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace) 
    { 
     super.setCharacterAttributes(offset, length, s, replace); 
    } 
*/ 


    public static void main(String a[]) 
    { 

     EditorKit editorKit = new StyledEditorKit() 
     { 
      public Document createDefaultDocument() 
      { 
       return new SyntaxDocument(); 
      } 
     }; 

//  final JEditorPane edit = new JEditorPane() 
     final JTextPane edit = new JTextPane(); 
//  LinePainter painter = new LinePainter(edit, Color.cyan); 
//  LinePainter2 painter = new LinePainter2(edit, Color.cyan); 
//  edit.setEditorKitForContentType("text/java", editorKit); 
//  edit.setContentType("text/java"); 
     edit.setEditorKit(editorKit); 

     JButton button = new JButton("Load SyntaxDocument.java"); 
     button.addActionListener(new ActionListener() 
     { 
      public void actionPerformed(ActionEvent e) 
      { 
       try 
       { 
        long startTime = System.currentTimeMillis(); 
        FileReader fr = new FileReader("SyntaxDocument.java"); 
//     FileReader fr = new FileReader("C:\\Java\\j2sdk1.4.2\\src\\javax\\swing\\JComponent.java"); 

        BufferedReader br = new BufferedReader(fr); 
        edit.read(br, null); 

        System.out.println("Load: " + (System.currentTimeMillis() - startTime)); 
        System.out.println("Document contains: " + edit.getDocument().getLength() + " characters"); 
        edit.requestFocus(); 
       } 
       catch(Exception e2) {} 
      } 
     }); 

     JFrame frame = new JFrame("Syntax Highlighting"); 
     frame.getContentPane().add(new JScrollPane(edit)); 
     frame.getContentPane().add(button, BorderLayout.SOUTH); 
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
     frame.setSize(800,300); 
     frame.setVisible(true); 
    } 
} 

注意:此代碼不檢查註釋分隔符是否在文字內,因此需要對其進行改進。

我並不真的希望你使用這段代碼,但我認爲這可能會讓你瞭解使用暴力方法時可能獲得的性能。