1 module dls.util.document; 2 3 import dls.util.uri : Uri; 4 5 class Document 6 { 7 import dls.protocol.definitions : Position, Range, TextDocumentIdentifier, 8 TextDocumentItem, VersionedTextDocumentIdentifier; 9 import dls.protocol.interfaces : TextDocumentContentChangeEvent; 10 import std.utf : codeLength, toUTF8; 11 12 private static Document[string] _documents; 13 private wstring[] _lines; 14 15 static Document opIndex(in Uri uri) 16 { 17 return uri.path in _documents ? _documents[uri.path] : null; 18 } 19 20 @property static auto uris() 21 { 22 import std.algorithm : map; 23 24 return _documents.keys.map!(path => Uri.fromPath(path)); 25 } 26 27 static void open(in TextDocumentItem textDocument) 28 { 29 auto path = Uri.getPath(textDocument.uri); 30 31 if (path in _documents) 32 { 33 _documents.remove(path); 34 } 35 36 _documents[path] = new Document(textDocument); 37 } 38 39 static void close(in TextDocumentIdentifier textDocument) 40 { 41 auto path = Uri.getPath(textDocument.uri); 42 43 if (path in _documents) 44 { 45 _documents.remove(path); 46 } 47 } 48 49 static void change(in VersionedTextDocumentIdentifier textDocument, 50 TextDocumentContentChangeEvent[] events) 51 { 52 auto path = Uri.getPath(textDocument.uri); 53 54 if (path in _documents) 55 { 56 _documents[path].change(events); 57 } 58 } 59 60 @property const(wstring[]) lines() const 61 { 62 return _lines; 63 } 64 65 this(in TextDocumentItem textDocument) 66 { 67 _lines = getText(textDocument.text); 68 } 69 70 override string toString() const 71 { 72 import std.range : join; 73 74 return _lines.join().toUTF8(); 75 } 76 77 size_t byteAtPosition(in Position position) 78 { 79 import std.algorithm : reduce; 80 import std.range : iota; 81 82 const linesBytes = reduce!((s, i) => s + codeLength!char(_lines[i]))(cast(size_t) 0, 83 iota(position.line)); 84 const characterBytes = codeLength!char(_lines[position.line][0 .. position.character]); 85 return linesBytes + characterBytes; 86 } 87 88 Range wordRangeAtByte(size_t bytePosition) 89 { 90 import std.algorithm : min; 91 92 size_t i; 93 size_t bytes; 94 95 while (bytes <= bytePosition && i < _lines.length) 96 { 97 bytes += codeLength!char(_lines[i]); 98 ++i; 99 } 100 101 const lineNumber = i - 1; 102 const line = _lines[lineNumber]; 103 bytes -= codeLength!char(line); 104 return wordRangeAtLineAndByte(lineNumber, min(bytePosition - bytes, line.length)); 105 } 106 107 Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition) 108 { 109 import std.regex : matchAll, regex; 110 import std.utf : UTFException, validate; 111 112 const line = _lines[lineNumber]; 113 size_t startCharacter; 114 const lineSlice = line.toUTF8()[0 .. bytePosition]; 115 116 try 117 { 118 validate(lineSlice); 119 startCharacter = codeLength!wchar(lineSlice); 120 } 121 catch (UTFException e) 122 { 123 // TODO: properly use document buffers instead of on-disc files 124 } 125 126 auto word = matchAll(line[startCharacter .. $], regex(`\w+|.`w)); 127 return new Range(new Position(lineNumber, startCharacter), 128 new Position(lineNumber, startCharacter + (word ? word.hit.length : 0))); 129 } 130 131 private void change(in TextDocumentContentChangeEvent[] events) 132 { 133 foreach (event; events) 134 { 135 if (event.range.isNull) 136 { 137 _lines = getText(event.text); 138 } 139 else 140 { 141 with (event.range) 142 { 143 auto linesBefore = _lines[0 .. start.line]; 144 auto linesAfter = _lines[end.line + 1 .. $]; 145 146 auto lineStart = _lines[start.line][0 .. start.character]; 147 auto lineEnd = _lines[end.line][end.character .. $]; 148 149 auto newLines = getText(event.text); 150 151 if (newLines.length) 152 { 153 newLines[0] = lineStart ~ newLines[0]; 154 newLines[$ - 1] = newLines[$ - 1] ~ lineEnd; 155 } 156 else 157 { 158 newLines = [lineStart ~ lineEnd]; 159 } 160 161 _lines = linesBefore ~ newLines ~ linesAfter; 162 } 163 } 164 } 165 } 166 167 private wstring[] getText(in string text) const 168 { 169 import std.algorithm : endsWith; 170 import std.array : array, replaceFirst; 171 import std.encoding : getBOM; 172 import std.string : splitLines; 173 import std.typecons : Yes; 174 import std.utf : toUTF16; 175 176 auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text) 177 .sequence, "").toUTF16().splitLines(Yes.keepTerminator); 178 179 if (!lines.length || lines[$ - 1].endsWith('\r', '\n')) 180 { 181 lines ~= ""; 182 } 183 184 return lines; 185 } 186 }