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 : DocumentUri, Position, Range, 27 TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier; 28 import dls.protocol.interfaces : TextDocumentContentChangeEvent; 29 import std.json : JSONValue; 30 31 private static Document[string] _documents; 32 private DocumentUri _uri; 33 private wstring[] _lines; 34 private JSONValue _version; 35 36 @property static auto uris() 37 { 38 import std.algorithm : map; 39 40 return _documents.byValue.map!(doc => new Uri(doc._uri)); 41 } 42 43 static Document get(const Uri uri) 44 { 45 import std.file : readText; 46 47 return uri.path in _documents ? _documents[uri.path] : new Document(uri, readText(uri.path)); 48 } 49 50 static bool open(const TextDocumentItem textDocument) 51 { 52 auto uri = new Uri(textDocument.uri); 53 54 if (uri.path !in _documents) 55 { 56 _documents[uri.path] = new Document(uri, textDocument.text); 57 _documents[uri.path]._version = textDocument.version_; 58 return true; 59 } 60 else 61 { 62 return false; 63 } 64 } 65 66 static bool close(const TextDocumentIdentifier textDocument) 67 { 68 auto uri = new Uri(textDocument.uri); 69 70 if (uri.path in _documents) 71 { 72 _documents.remove(uri.path); 73 return true; 74 } 75 else 76 { 77 return false; 78 } 79 } 80 81 static bool change(const VersionedTextDocumentIdentifier textDocument, 82 TextDocumentContentChangeEvent[] events) 83 { 84 auto uri = new Uri(textDocument.uri); 85 86 if (uri.path in _documents) 87 { 88 _documents[uri.path].change(events); 89 _documents[uri.path]._version = textDocument.version_; 90 return true; 91 } 92 else 93 { 94 return false; 95 } 96 } 97 98 @property const(wstring[]) lines() const 99 { 100 return _lines; 101 } 102 103 @property JSONValue version_() const 104 { 105 return _version; 106 } 107 108 private this(const Uri uri, const string text) 109 { 110 _uri = uri; 111 _lines = getText(text); 112 } 113 114 override string toString() const 115 { 116 import std.range : join; 117 import std.utf : toUTF8; 118 119 return _lines.join().toUTF8(); 120 } 121 122 void validatePosition(const Position position) const 123 { 124 import dls.protocol.errors : InvalidParamsException; 125 import std.format : format; 126 127 if (position.line >= _lines.length || position.character > _lines[position.line].length) 128 { 129 throw new InvalidParamsException(format!"invalid position: %s %s,%s"(_uri, 130 position.line, position.character)); 131 } 132 } 133 134 size_t byteAtPosition(const Position position) const 135 { 136 import std.algorithm : reduce; 137 import std.range : iota; 138 import std.utf : codeLength; 139 140 if (position.line >= _lines.length) 141 { 142 return 0; 143 } 144 145 immutable linesBytes = reduce!((s, i) => s + codeLength!char(_lines[i]))( 146 cast(size_t) 0, iota(position.line)); 147 148 if (position.character > _lines[position.line].length) 149 { 150 return 0; 151 } 152 153 immutable characterBytes = codeLength!char(_lines[position.line][0 .. position.character]); 154 return linesBytes + characterBytes; 155 } 156 157 Position positionAtByte(size_t bytePosition) const 158 { 159 import std.algorithm : min; 160 import std.utf : codeLength, toUCSindex; 161 162 size_t i; 163 size_t bytes; 164 165 while (bytes <= bytePosition && i < _lines.length) 166 { 167 bytes += codeLength!char(_lines[i]); 168 ++i; 169 } 170 171 immutable lineNumber = minusOne(i); 172 immutable line = _lines[lineNumber]; 173 bytes -= codeLength!char(line); 174 immutable columnNumber = toUCSindex(line, min(bytePosition - bytes, line.length)); 175 return new Position(lineNumber, columnNumber); 176 } 177 178 Range wordRangeAtPosition(const Position position) const 179 { 180 import std.algorithm : min; 181 182 immutable line = _lines[min(position.line, $ - 1)]; 183 immutable middleIndex = min(position.character, line.length); 184 size_t startIndex = middleIndex; 185 size_t endIndex = middleIndex; 186 187 static bool isIdentifierChar(wchar c) 188 { 189 import std.ascii : isAlphaNum; 190 191 return c == '_' || isAlphaNum(c); 192 } 193 194 while (startIndex > 0 && isIdentifierChar(line[minusOne(startIndex)])) 195 { 196 --startIndex; 197 } 198 199 while (endIndex < line.length && isIdentifierChar(line[endIndex])) 200 { 201 ++endIndex; 202 } 203 204 return new Range(new Position(position.line, startIndex), 205 new Position(position.line, endIndex)); 206 } 207 208 Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition) const 209 { 210 import std.utf : toUCSindex; 211 212 return wordRangeAtPosition(new Position(lineNumber, 213 toUCSindex(_lines[lineNumber], bytePosition))); 214 } 215 216 Range wordRangeAtByte(size_t bytePosition) const 217 { 218 return wordRangeAtPosition(positionAtByte(bytePosition)); 219 } 220 221 private void change(const TextDocumentContentChangeEvent[] events) 222 { 223 foreach (event; events) 224 { 225 if (event.range.isNull) 226 { 227 _lines = getText(event.text); 228 } 229 else 230 { 231 with (event.range) 232 { 233 auto linesBefore = _lines[0 .. start.line]; 234 auto linesAfter = _lines[end.line + 1 .. $]; 235 236 auto lineStart = _lines[start.line][0 .. start.character]; 237 auto lineEnd = _lines[end.line][end.character .. $]; 238 239 auto newLines = getText(event.text); 240 241 if (newLines.length) 242 { 243 newLines[0] = lineStart ~ newLines[0]; 244 newLines[$ - 1] = newLines[$ - 1] ~ lineEnd; 245 } 246 else 247 { 248 newLines = [lineStart ~ lineEnd]; 249 } 250 251 _lines = linesBefore ~ newLines ~ linesAfter; 252 } 253 } 254 } 255 } 256 257 private wstring[] getText(const string text) const 258 { 259 import std.algorithm : endsWith; 260 import std.array : replaceFirst; 261 import std.encoding : getBOM; 262 import std..string : splitLines; 263 import std.typecons : Yes; 264 import std.utf : toUTF16; 265 266 auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text) 267 .sequence, "").toUTF16().splitLines(Yes.keepTerminator); 268 269 if (!lines.length || lines[$ - 1].endsWith('\r', '\n')) 270 { 271 lines ~= ""; 272 } 273 274 return lines; 275 } 276 } 277 278 size_t minusOne(size_t i) 279 { 280 return i > 0 ? i - 1 : 0; 281 }