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 : min; 137 import std.utf : codeLength; 138 139 size_t result; 140 141 foreach (i, ref line; _lines) 142 { 143 if (i < position.line) 144 { 145 result += codeLength!char(line); 146 } 147 else 148 { 149 result += codeLength!char(line[0 .. min(position.character, $)]); 150 break; 151 } 152 } 153 154 return result; 155 } 156 157 Position positionAtByte(size_t bytePosition) const 158 { 159 import std.algorithm : min; 160 import std.utf : codeLength, toUTF8; 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 columnByte = min(bytePosition - bytes, line.length); 175 immutable columnNumber = codeLength!wchar(line.toUTF8()[0 .. columnByte]); 176 return new Position(lineNumber, columnNumber); 177 } 178 179 Range wordRangeAtPosition(const Position position) const 180 { 181 import std.algorithm : min; 182 183 immutable line = _lines[min(position.line, $ - 1)]; 184 immutable middleIndex = min(position.character, line.length); 185 size_t startIndex = middleIndex; 186 size_t endIndex = middleIndex; 187 188 static bool isIdentifierChar(wchar c) 189 { 190 import std.ascii : isPunctuation, isWhite; 191 192 return !isWhite(c) && (!isPunctuation(c) || c == '_'); 193 } 194 195 while (startIndex > 0 && isIdentifierChar(line[minusOne(startIndex)])) 196 { 197 --startIndex; 198 } 199 200 while (endIndex < line.length && isIdentifierChar(line[endIndex])) 201 { 202 ++endIndex; 203 } 204 205 return new Range(new Position(position.line, startIndex), 206 new Position(position.line, endIndex)); 207 } 208 209 Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition) const 210 { 211 import std.algorithm : min; 212 import std.utf : codeLength, toUTF8; 213 214 return wordRangeAtPosition(new Position(lineNumber, 215 codeLength!wchar(_lines[lineNumber].toUTF8()[0 .. min(bytePosition, $)]))); 216 } 217 218 Range wordRangeAtByte(size_t bytePosition) const 219 { 220 return wordRangeAtPosition(positionAtByte(bytePosition)); 221 } 222 223 private void change(const TextDocumentContentChangeEvent[] events) 224 { 225 foreach (event; events) 226 { 227 if (event.range.isNull) 228 { 229 _lines = getText(event.text); 230 } 231 else 232 { 233 with (event.range.get) 234 { 235 auto linesBefore = _lines[0 .. start.line]; 236 auto linesAfter = _lines[end.line + 1 .. $]; 237 238 auto lineStart = _lines[start.line][0 .. start.character]; 239 auto lineEnd = _lines[end.line][end.character .. $]; 240 241 auto newLines = getText(event.text); 242 243 if (newLines.length) 244 { 245 newLines[0] = lineStart ~ newLines[0]; 246 newLines[$ - 1] = newLines[$ - 1] ~ lineEnd; 247 } 248 else 249 { 250 newLines = [lineStart ~ lineEnd]; 251 } 252 253 _lines = linesBefore ~ newLines ~ linesAfter; 254 } 255 } 256 } 257 } 258 259 private wstring[] getText(const string text) const 260 { 261 import std.algorithm : endsWith; 262 import std.array : replaceFirst; 263 import std.encoding : getBOM; 264 import std..string : splitLines; 265 import std.typecons : Yes; 266 import std.utf : toUTF16; 267 268 auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text) 269 .sequence, "").toUTF16().splitLines(Yes.keepTerminator); 270 271 if (!lines.length || lines[$ - 1].endsWith('\r', '\n')) 272 { 273 lines ~= ""; 274 } 275 276 return lines; 277 } 278 } 279 280 size_t minusOne(size_t i) 281 { 282 return i > 0 ? i - 1 : 0; 283 }