實現簡易的代碼編輯器

分類:技術 時間:2017-01-13

最近在 iOS 平臺做一個簡單的代碼編輯器,最后的效果如圖:

這篇文章簡單總結一下技術方案和一些考慮的點。

# 目標

一個可被接受的代碼編輯器,需要具備下面幾個基本條件:

  • 實時代碼高亮

  • 自動折行

  • 自動保持縮進

  • 自動插入花括號

  • 方便輸入符號

# 技術方案

實現代碼高亮,通常有以下幾種技術方案可選:

  • WebView contentEditable
  • CoreText
  • JavaScriptCore TextKit

其中 WebView 的方案是最簡單的,但是實踐下來卻發現體驗和要求相去甚遠,例如自動折行很難實現,要知道如果是在手持設備上左右滑動,是非常不好的體驗。另一方面性能上也是差強人意,其他格式化方面的需求就更難做到。

如果是用 CoreText/TextKit 的話,理論上可以完美達到任何文本編輯器的效果,但是這個工作量太大了,即便是有一些 CoreText 的封裝,要做到這樣的效果還是難以接受,如果我們并不是以做一個代碼編輯器為主要任務的話,實在是沒有必要的一件事情。

所以最后選擇了類似于 這個項目 的方案:JavaScriptCore TextKit。

這個項目很聰明,他利用 JavaScriptCore 來解析一個 js 庫(他用的 highlight.js),然后把生成的 HTML 轉換成一個 NSAttributedString,再用這個 AttributedString 去設置 UITextView

由于是 UITextView 實現的,你直接擁有了自動折行的功能,不用在一行很長的時候左右移動了。

如果你只需要渲染一次的話,只需要轉換的部分就夠了,如果需要實時編輯,則需要通過 NSTextStorageprocessEditing 去動態的做這個事情。Highlightr 這個項目提供了非常好的思路,但我沒有直接使用它,主要是他的代碼太亂了,我選擇了自己實現了一份。

# Highlighter 的核心邏輯

以通過 JavaScriptCore 調用 highlightJS 為例,主要要實現以下幾個步驟:

  • 解析主題 css 文件,將 css 屬性轉換成內設置到 NSAttributeString 里面的配置

  • 加載 highlight.js,調用 highlight 方法將要渲染的代碼轉換成 HTML

  • 遍歷每個 lt;spangt;,將 HTML 轉換成 NSAttributedString

  • 實現 NSTextStorage 的 processEditing 讓上述過程在編輯過程中保持實時

# 自動保持縮進

移動設備上的輸入成本是很高的,所以保持縮進是很重要的一件事情。保持縮進的邏輯是這樣的:當用戶輸入一個換行符的時候,要自動添加空格(我是 2 spaces 黨,不是 tab 黨),方法是通過實現這個代理:

- (BOOL)shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    if ([text isEqualToString:@quot;\nquot;]) {
        [self reindent];
        return NO;
    }
    return YES;
}

檢測到用戶輸入換行符的時候,做一次 reindent ,reindent 的邏輯很簡單,從 selectedRange 往前找空格數和 tab 數,直到碰到換行符,看起來大概是這樣:

int spaceZero = bracket ? 2 : 0;
int spaces = spaceZero;
int tabZero = bracket ? 1 : 0;
int tabs = tabZero;

for (int i=location-1; igt;=0; --i) {
    unichar ch = [self.text characterAtIndex:i];
    if (ch == ' ') { // space
          spaces;
    } else if (ch == '\t') {
          tabs;
    } else if (ch == '\n') { // return
        break;
    } else { // other
        spaces = spaceZero;
        tabs = tabZero;
    }
}

BOOL spaceBased = spaces gt;= 2;
NSMutableString *refill;

if (spaceBased) { // space based
    refill = [NSMutableString stringWithFormat:@quot;\n%@quot;, [NSString stringWithSpaces:spaces]];
} else { // tab based
    refill = [NSMutableString stringWithFormat:@quot;\n%@quot;, [NSString stringWithTabs:tabs]];
}

NSMutableString *text = self.text.mutableCopy;
[text insertString:refill atIndex:selectedRange.location];
self.text = text;
self.selectedRange = NSMakeRange(selectedRange.location   spaces   1, 0);

這里還順便照顧了一下 tab 黨,雖然我極不情愿這樣做。

# 自動插入花括號

iOS 鍵盤上面輸入花括號對的成本很高,所以要支持自動輸入花括號,具體表現是,當用戶輸入一個 { 的時候按回車,另外一個 } 要自動出現并且格式化,光標跑到正確的位置。這件事情還是在上面的 reindent 方法里面做,首先要找出換行前最后一個非空白字符是不是左花括號:

BOOL bracket = NO;
for (int i=location-1; igt;=0; --i) {
    unichar ch = [self.text characterAtIndex:i];
    if (ch == '{') {
        bracket = YES;
        break;
    } else if (ch == ' ' || ch == '\t') {
        continue;
    } else {
        break;
    }
}

當 bracket 為 YES 的時候,說明需要進行括號配對,這個效果有很多種實現方案,這里講一種比 Xcode 的效果好一點點的(Xcode 可能會出現括號無法匹配的情況),就是我們真的去做一次括號匹配算法,復雜度是 O(len) ,len 是文本長度,括號匹配算法是一個典型的棧應用,可以在網上隨意找到:

- (BOOL)bracketsBlanced {

    NSMutableArray *stack = [NSMutableArray array];

    for (int i=0; ilt;self.length;   i) {
        unichar ch = [self characterAtIndex:i];
        if (ch == '{') {
            [stack addObject:@(ch)];
        } else if (ch == '}') {
            if (stack.count == 0) { // error
                return NO;
            } else {
                unichar top = [stack.lastObject charValue];
                if (top == '{' amp;amp; ch == '}') {
                    [stack removeLastObject];
                } else {
                    return NO;
                }
            }
        }
    }
    return stack.count == 0;
}

這里只考慮花括號的情況,當發現括號不平衡的時候,主動在上面插入右括號以保持平衡:

if (bracket amp;amp; !self.text.bracketsBlanced) {
    if (spaceBased) {
        [refill appendFormat:@quot;\n%@}quot;, [NSString stringWithSpaces:spaces - spaceZero]];
    } else {
        [refill appendFormat:@quot;\n%@}quot;, [NSString stringWithTabs:tabs - tabZero]];
    }
}

這個 reindent 暫時就這樣了,沒有特別復雜的邏輯。

# 符號面板

還是那個問題,移動設備上輸入符號比較困難,所以要做一個方面輸入符號的面板,方案很多,我的實現是使用 inputAccessoryView 來實現,好處是他會自動貼緊鍵盤的邊緣,不用去做監聽鍵盤高度等瑣事。我的 inputAccessoryView 包含了一個可上下滾動的 UIScrollView,用于三種輸入:

  • 撤銷和重做
  • 直接插入一個符號
  • 插入一對符號,例如括號和引號

撤銷和重做 ,直接調用 UITextView.undoManagerundoredo 方法,并且可以根據 canUndocanRedo 來改變按鈕是否可以點擊。

直接插入一個符號 非常簡單,使用:

[self replaceRange:self.selectedTextRange withText:text];

千萬別替換整個 text,這樣重新渲染的速度很慢,可能造成閃爍。

插入一對符號 ,在之前的基礎上,再增加一個把光標往前移動一格的操作,讓用戶可以直接在中間輸入:

self.selectedRange = NSMakeRange(MAX(0, self.selectedRange.location - 1), 0);

# 光標移動

這部分尚未實現,主要是針對沒有 3D Touch 的一個優化。眾所周知的是,有 3D Touch 的設備用力按鍵盤的時候,可以快速移動光標, 第二次用力按甚至可以選擇文本,像是這樣:

非 3D Touch 的設備沒有這樣的福利。而編寫代碼的時候移動光標是非常高頻的一個操作,所以很有必要在實現一個快速移動光標的功能。

移動光標可以通過 setSelectedRange 實現,交互上可以使用 UIPanGestureRecognizer ,效果會比 3D Touch 稍差,這里就不在贅述細節了。

# 定制 highlightjs

由于我在寫一個基于 JavaScriptCore 的 js 擴展 library,所以有必要讓用戶在輸入我自定義的一些 JSExport 的時候,也會有高亮顯示。例如截圖中的 appwidget 就是兩個需要額外高亮的關鍵字。這一部分很簡單,需要稍稍理解一下 highlightjs 是如何工作的。highlightjs 通過這樣一組 JSON 來定義關鍵字和內置函數的高亮:

{
  quot;keywordquot;: quot;in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from asquot;,
  quot;literalquot;: quot;true false null undefined NaN Infinityquot;,
  quot;built_inquot;: quot;eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promisequot;
}

根據你自己的需求,在需要的語言配置上面, keyword 或者 built_in 這里增加你要高亮的關鍵字即可。

另外就是去掉你不需要的部分,比如不需要換主題功能的話,去掉多余的 css,不需要其他語言(例如我只要 js)的話,就去掉其他語言的配置, http:// highlightjs.org 上面可以定制,你也可以根據自己的需求把 js 和 css 文件壓的更小,最后我的加起來只有 11KB

以上就是一個簡易可用的代碼編輯器的一些思考,感謝觀看。

- EOF -


Tags: iOS開發 highlight.js

文章來源:https://zhuanlan.zhihu.com/p/24855274


ads
ads

相關文章
ads

相關文章

ad