1 /* 2 *Copyright (C) 2018 Laurent Tréguier 3 * 4 *This file is part of DLS. 5 * 6 *DLS is free software: you can redistribute it and/or modify 7 *it under the terms of the GNU General Public License as published by 8 *the Free Software Foundation, either version 3 of the License, or 9 *(at your option) any later version. 10 * 11 *DLS is distributed in the hope that it will be useful, 12 *but WITHOUT ANY WARRANTY; without even the implied warranty of 13 *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 *GNU General Public License for more details. 15 * 16 *You should have received a copy of the GNU General Public License 17 *along with DLS. If not, see <http://www.gnu.org/licenses/>. 18 * 19 */ 20 21 module dls.util.document; 22 23 class Document 24 { 25 import dls.util.uri : Uri; 26 import dls.protocol.definitions : Position, Range, TextDocumentIdentifier, 27 TextDocumentItem, VersionedTextDocumentIdentifier; 28 import dls.protocol.interfaces : TextDocumentContentChangeEvent; 29 import std.json : JSONValue; 30 31 private static Document[string] _documents; 32 private wstring[] _lines; 33 private JSONValue _version; 34 35 @property static auto uris() 36 { 37 import std.algorithm : map; 38 39 return _documents.keys.map!(path => Uri.fromPath(path)); 40 } 41 42 static Document get(in Uri uri) 43 { 44 import std.file : readText; 45 46 return uri.path in _documents ? _documents[uri.path] : new Document(readText(uri.path)); 47 } 48 49 static void open(in TextDocumentItem textDocument) 50 { 51 auto uri = new Uri(textDocument.uri); 52 53 if (uri.path !in _documents) 54 { 55 _documents[uri.path] = new Document(textDocument.text); 56 _documents[uri.path]._version = textDocument.version_; 57 } 58 } 59 60 static void close(in TextDocumentIdentifier textDocument) 61 { 62 auto uri = new Uri(textDocument.uri); 63 64 if (uri.path in _documents) 65 { 66 _documents.remove(uri.path); 67 } 68 } 69 70 static void change(in VersionedTextDocumentIdentifier textDocument, 71 TextDocumentContentChangeEvent[] events) 72 { 73 auto uri = new Uri(textDocument.uri); 74 75 if (uri.path in _documents) 76 { 77 _documents[uri.path].change(events); 78 _documents[uri.path]._version = textDocument.version_; 79 } 80 } 81 82 @property const(wstring[]) lines() const 83 { 84 return _lines; 85 } 86 87 @property JSONValue version_() const 88 { 89 return _version; 90 } 91 92 private this(in string text) 93 { 94 _lines = getText(text); 95 } 96 97 override string toString() const 98 { 99 import std.range : join; 100 import std.utf : toUTF8; 101 102 return _lines.join().toUTF8(); 103 } 104 105 bool validatePosition(in Position position) const 106 { 107 return position.line < _lines.length && position.character <= _lines[position.line].length; 108 } 109 110 size_t byteAtPosition(in Position position) const 111 { 112 import std.algorithm : reduce; 113 import std.range : iota; 114 import std.utf : codeLength; 115 116 if (position.line >= _lines.length) 117 { 118 return 0; 119 } 120 121 const linesBytes = reduce!((s, i) => s + codeLength!char(_lines[i]))(cast(size_t) 0, 122 iota(position.line)); 123 124 if (position.character >= _lines[position.line].length) 125 { 126 return 0; 127 } 128 129 const characterBytes = codeLength!char(_lines[position.line][0 .. position.character]); 130 return linesBytes + characterBytes; 131 } 132 133 Position positionAtByte(size_t bytePosition) const 134 { 135 import std.algorithm : min; 136 import std.utf : codeLength, toUCSindex; 137 138 size_t i; 139 size_t bytes; 140 141 while (bytes <= bytePosition && i < _lines.length) 142 { 143 bytes += codeLength!char(_lines[i]); 144 ++i; 145 } 146 147 const lineNumber = i - 1; 148 const line = _lines[lineNumber]; 149 bytes -= codeLength!char(line); 150 const columnNumber = toUCSindex(line, min(bytePosition - bytes, line.length)); 151 return new Position(lineNumber, columnNumber); 152 } 153 154 Range wordRangeAtByte(size_t bytePosition) const 155 { 156 return wordRangeAtPosition(positionAtByte(bytePosition)); 157 } 158 159 Range wordRangeAtPosition(in Position position) const 160 { 161 import std.algorithm : min; 162 import std.regex : matchAll, regex; 163 164 const line = _lines[min(position.line, $ - 1)]; 165 const startCharacter = min(position.character, line.length); 166 auto word = matchAll(line[startCharacter .. $], regex(`\w+|.`w)); 167 return new Range(new Position(position.line, startCharacter), 168 new Position(position.line, startCharacter + (word ? word.hit.length : 0))); 169 } 170 171 Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition) const 172 { 173 import std.utf : toUCSindex; 174 175 return wordRangeAtPosition(new Position(lineNumber, 176 toUCSindex(_lines[lineNumber], bytePosition))); 177 } 178 179 private void change(in TextDocumentContentChangeEvent[] events) 180 { 181 foreach (event; events) 182 { 183 if (event.range.isNull) 184 { 185 _lines = getText(event.text); 186 } 187 else 188 { 189 with (event.range) 190 { 191 auto linesBefore = _lines[0 .. start.line]; 192 auto linesAfter = _lines[end.line + 1 .. $]; 193 194 auto lineStart = _lines[start.line][0 .. start.character]; 195 auto lineEnd = _lines[end.line][end.character .. $]; 196 197 auto newLines = getText(event.text); 198 199 if (newLines.length) 200 { 201 newLines[0] = lineStart ~ newLines[0]; 202 newLines[$ - 1] = newLines[$ - 1] ~ lineEnd; 203 } 204 else 205 { 206 newLines = [lineStart ~ lineEnd]; 207 } 208 209 _lines = linesBefore ~ newLines ~ linesAfter; 210 } 211 } 212 } 213 } 214 215 private wstring[] getText(in string text) const 216 { 217 import std.algorithm : endsWith; 218 import std.array : replaceFirst; 219 import std.encoding : getBOM; 220 import std..string : splitLines; 221 import std.typecons : Yes; 222 import std.utf : toUTF16; 223 224 auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text) 225 .sequence, "").toUTF16().splitLines(Yes.keepTerminator); 226 227 if (!lines.length || lines[$ - 1].endsWith('\r', '\n')) 228 { 229 lines ~= ""; 230 } 231 232 return lines; 233 } 234 }