github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/workspace_store.go (about) 1 package langserver 2 3 import ( 4 "fmt" 5 "strings" 6 "sync" 7 8 "github.com/thought-machine/please/tools/build_langserver/lsp" 9 ) 10 11 type workspaceStore struct { 12 rootURI lsp.DocumentURI 13 mu sync.Mutex 14 15 documents map[lsp.DocumentURI]*document 16 } 17 18 type document struct { 19 // text content of the document from the last time when saved 20 text []string 21 // test content of the document while in editing(not been saved) 22 textInEdit []string 23 version int 24 } 25 26 func newWorkspaceStore(rootURI lsp.DocumentURI) *workspaceStore { 27 return &workspaceStore{ 28 rootURI: rootURI, 29 documents: make(map[lsp.DocumentURI]*document), 30 } 31 } 32 33 // Store method is generally used to correspond to "textDocument/didOpen", 34 // this stores the initial state of the document when opened 35 func (ws *workspaceStore) Store(uri lsp.DocumentURI, content string, version int) { 36 text := SplitLines(content, true) 37 ws.documents[uri] = &document{ 38 text: text, 39 textInEdit: text, 40 version: version, 41 } 42 } 43 44 // Update method corresponds to "textDocument/didSave". 45 // This updates the existing uri document in the store 46 func (ws *workspaceStore) Update(uri lsp.DocumentURI, content string) error { 47 text := SplitLines(content, true) 48 if _, ok := ws.documents[uri]; !ok { 49 return fmt.Errorf("document %s did not open", uri) 50 } 51 ws.documents[uri].text = text 52 ws.documents[uri].textInEdit = text 53 54 return nil 55 } 56 57 // Close method corresponds to "textDocument/didClose" 58 // This removes the document stored in workspaceStore 59 func (ws *workspaceStore) Close(uri lsp.DocumentURI) error { 60 if _, ok := ws.documents[uri]; !ok { 61 return fmt.Errorf("document %s did not open", uri) 62 } 63 64 ws.mu.Lock() 65 defer ws.mu.Unlock() 66 67 delete(ws.documents, uri) 68 69 return nil 70 } 71 72 // TrackEdit tracks the changes of the content for the targeting uri, and update the corresponding 73 func (ws *workspaceStore) TrackEdit(uri lsp.DocumentURI, contentChanges []lsp.TextDocumentContentChangeEvent, version int) error { 74 doc, ok := ws.documents[uri] 75 if !ok { 76 log.Error("document '%s' is not opened, edit did not apply", uri) 77 return nil 78 } 79 80 ws.mu.Lock() 81 defer ws.mu.Unlock() 82 83 for _, change := range contentChanges { 84 newText, err := ws.applyChange(doc.textInEdit, change) 85 if err != nil { 86 return err 87 } 88 doc.textInEdit = newText 89 } 90 doc.version = version 91 92 return nil 93 } 94 95 func (ws *workspaceStore) applyChange(text []string, change lsp.TextDocumentContentChangeEvent) ([]string, error) { 96 if change.Range == nil && change.RangeLength == 0 { 97 return text, nil // new full content 98 } 99 100 changeText := change.Text 101 102 startLine := change.Range.Start.Line 103 endLine := change.Range.End.Line 104 startCol := change.Range.Start.Character 105 endCol := change.Range.End.Character 106 107 var newText string 108 109 for i, line := range text { 110 if i < startLine || i > endLine { 111 newText += line 112 continue 113 } 114 115 if i == startLine { 116 newText += line[:startCol] + changeText 117 } 118 119 if i == endLine { 120 // Apparently, when you delete a whole line, intellij plugin sometimes sends the range like so: 121 // {startline: deletedline_index, startcol: 0}, {endline: nextline, endcol: len_of_deleted_line}... 122 if len(line)-1 < endCol && (len(text) != 1 && len(text[i-1])-1 == endCol) { 123 newText += line 124 } else { 125 newText += line[endCol:] 126 } 127 } 128 } 129 130 return SplitLines(newText, true), nil 131 } 132 133 // SplitLines splits a content with \n characters, and returns a slice of string 134 // if keepEnds is true, all lines will keep it's original splited character 135 func SplitLines(content string, keepEnds bool) []string { 136 splited := strings.Split(content, "\n") 137 if !keepEnds { 138 return splited 139 } 140 141 for i := range splited { 142 // Do not add endline character on the last empty line 143 if (i == len(splited)-1 && splited[i] == "") || len(splited) <= 1 { 144 continue 145 } 146 splited[i] += "\n" 147 } 148 149 return splited 150 } 151 152 // JoinLines concatenate a slice of string, removes the trailing "\n" if hadEnds is true 153 func JoinLines(text []string, hasEnds bool) string { 154 if !hasEnds { 155 concat := strings.Join(text, "\n") 156 return concat 157 } 158 159 newText := make([]string, len(text)) 160 for i := range text { 161 if i == len(text)-1 && text[i] == "" { 162 newText[i] = text[i] 163 continue 164 } 165 newText[i] = strings.TrimSuffix(text[i], "\n") 166 } 167 168 return strings.Join(newText, "\n") 169 }